blob: 05991f9a978071d1fad43d982bef74a2f4ece4ef [file] [log] [blame]
Max Rasskazov7f3c53c2014-12-09 14:19:17 +03001#!/usr/bin/env python
2#-*- coding: utf-8 -*-
3
4
5import datetime
6import os
7import subprocess
8import tempfile
9
10
11now = datetime.datetime.now()
12staging_snapshot_stamp = \
13 '{:04}-{:02}-{:02}-{:02}{:02}{:02}'.format(
14 now.year, now.month, now.day, now.hour, now.minute, now.second)
15
16
17class RemoteRsyncStaging(object):
18 def __init__(self,
19 mirror_name,
20 host,
21 module='mirror-sync',
22 root_path='fwm',
23 files_dir='files',
24 save_last_days=61,
25 rsync_extra_params='-v',
26 staging_postfix='staging'):
27 self.mirror_name = mirror_name
28 self.host = host
29 self.module = module
30 self.root_path = root_path
31 self.files_dir = files_dir
32 self.save_last_days = save_last_days
33 self.rsync_extra_params = rsync_extra_params
34 self.staging_snapshot_stamp = staging_snapshot_stamp
35 self.staging_postfix = staging_postfix
36
37 @property
38 def url(self):
39 return '{}::{}'.format(self.host, self.module)
40
41 @property
42 def root_url(self):
43 return '{}/{}'.format(self.url, self.root_path)
44
45 @property
46 def files_path(self):
47 return '{}/{}'.format(self.root_path, self.files_dir)
48
49 @property
50 def files_url(self):
51 return '{}/{}'.format(self.root_url, self.files_dir)
52
53 def http_url(self, path):
54 return 'http://{}/{}'.format(self.host, path)
55
56 def html_link(self, path, link_name):
57 return '<a href="{}">{}</a>'.format(self.http_url(path), link_name)
58
59 @property
60 def staging_dir(self):
61 return '{}-{}'.format(self.mirror_name, self.staging_snapshot_stamp)
62
63 @property
64 def staging_dir_path(self):
65 return '{}/{}'.format(self.files_path, self.staging_dir)
66
67 @property
68 def staging_dir_url(self):
69 return '{}/{}'.format(self.url, self.staging_dir_path)
70
71 @property
72 def staging_link(self):
73 return '{}-{}'.format(self.mirror_name, self.staging_postfix)
74
75 @property
76 def staging_link_path(self):
77 return '{}/{}'.format(self.files_path, self.staging_link)
78
79 @property
80 def staging_link_url(self):
81 return '{}/{}'.format(self.url, self.staging_link_path)
82
83 @property
84 def empty_dir(self):
85 if self.__dict__.get('_empty_dir') is None:
86 self._empty_dir = tempfile.mkdtemp()
87 return self._empty_dir
88
89 def symlink_to(self, target):
90 linkname = tempfile.mktemp()
91 os.symlink(target, linkname)
92 return linkname
93
94 def _shell(self, cmd, raise_error=True):
95 print cmd
96 process = subprocess.Popen(cmd,
97 stdin=subprocess.PIPE,
98 stdout=subprocess.PIPE,
99 stderr=subprocess.PIPE,
100 shell=True)
101 out, err = process.communicate()
102 exitcode = process.returncode
103 if process.returncode != 0 and raise_error:
104 msg = '"{cmd}" failed. Exit code == {exitcode}'\
105 '\n\nSTDOUT: \n{out}'\
106 '\n\nSTDERR: \n{err}'\
107 .format(**(locals()))
108 raise RuntimeError(msg)
109 return exitcode, out, err
110
111 def _do_rsync(self, source='', dest=None, opts='', extra=None):
112 if extra is None:
113 extra = self.rsync_extra_params
114 cmd = 'rsync {opts} {extra} {source} {dest}'.format(**(locals()))
115 return self._shell(cmd)
116
117 def rsync_ls(self, dirname=None):
118 if dirname is None:
119 dirname = '{}/'.format(self.root_path)
120 dest = '{}/{}'.format(self.url, dirname)
121 opts = '-l'
122 extra = self.rsync_extra_params + ' --no-v'
123 exitcode, out, err = self._do_rsync(dest=dest, opts=opts, extra=extra)
124 out = [_ for _ in out.splitlines() if _.split()[-1] != '.']
125 return exitcode, out, err
126
127 def rsync_ls_dirs(self, dirname):
128 exitcode, out, err = self.rsync_ls(dirname)
129 out = [_.split()[-1] for _ in out if _.startswith('d')]
130 return exitcode, out, err
131
132 def rsync_ls_symlinks(self, dirname):
133 exitcode, out, err = self.rsync_ls(dirname)
134 out = [_.split()[-3:] for _ in out if _.startswith('l')]
135 out = [[_[0], _[-1]] for _ in out]
136 return exitcode, out, err
137
138 def rsync_delete_file(self, filename):
139 dirname, filename = os.path.split(filename)
140 source = '{}/'.format(self.empty_dir)
141 dest = '{}/{}/'.format(self.url, dirname)
142 opts = "-r --delete --include={} '--exclude=*'".format(filename)
143 return self._do_rsync(source=source, dest=dest, opts=opts)
144
145 def rsync_delete_dir(self, dirname):
146 source = '{}/'.format(self.empty_dir)
147 dest = '{}/{}/'.format(self.url, dirname)
148 opts = "-a --delete"
149 exitcode, out, err = self._do_rsync(source=source,
150 dest=dest,
151 opts=opts)
152 return self.rsync_delete_file(dirname)
153
154 def rsync_staging_transfer(self, source, tgt_symlink_name=None):
155 if tgt_symlink_name is None:
156 tgt_symlink_name = self.mirror_name
157 opts = '--archive --force --ignore-errors '\
158 '--delete-excluded --no-owner --no-group --delete '\
159 '--link-dest=/{}'.format(self.staging_link_path)
160 try:
161 exitcode, out, err = self._do_rsync(source=source,
162 dest=self.staging_dir_url,
163 opts=opts)
164 self.rsync_delete_file(self.staging_link_path)
165 self._do_rsync(source=self.symlink_to(self.staging_dir),
166 dest=self.staging_link_url,
167 opts='-l')
168 # cleaning of old snapshots
169 return exitcode, out, err
170 except RuntimeError as e:
171 print e.message
172 self.rsync_delete_dir(self.staging_dir_path)
173 raise