blob: 61977b8684175fcb5a5a10d58d1043ebbf006522 [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
Max Rasskazove99d8372015-01-13 20:07:01 +03007import re
Max Rasskazov7f3c53c2014-12-09 14:19:17 +03008import subprocess
9import tempfile
10
11
Max Rasskazov3fe42712015-01-15 15:34:22 +030012now = datetime.datetime.utcnow()
13staging_snapshot_stamp_format = r'%Y-%m-%d-%H%M%S'
14staging_snapshot_stamp_regexp = r'[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}'
15staging_snapshot_stamp = now.strftime(staging_snapshot_stamp_format)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +030016
17
18class RemoteRsyncStaging(object):
19 def __init__(self,
20 mirror_name,
21 host,
22 module='mirror-sync',
23 root_path='fwm',
24 files_dir='files',
25 save_last_days=61,
26 rsync_extra_params='-v',
27 staging_postfix='staging'):
28 self.mirror_name = mirror_name
29 self.host = host
30 self.module = module
31 self.root_path = root_path
32 self.files_dir = files_dir
33 self.save_last_days = save_last_days
34 self.rsync_extra_params = rsync_extra_params
Max Rasskazov7f3c53c2014-12-09 14:19:17 +030035 self.staging_postfix = staging_postfix
Max Rasskazov3fe42712015-01-15 15:34:22 +030036 self.staging_snapshot_stamp = staging_snapshot_stamp
37 self.staging_snapshot_stamp_format = staging_snapshot_stamp_format
38 if re.match(staging_snapshot_stamp_regexp,
39 self.staging_snapshot_stamp) \
40 is not None:
41 self.staging_snapshot_stamp_regexp = staging_snapshot_stamp_regexp
42 else:
43 raise RuntimeError('Wrong regexp for staging_snapshot_stamp\n'
44 'staging_snapshot_stamp = "{}"\n'
45 'staging_snapshot_stamp_regexp = "{}"'.
46 format(staging_snapshot_stamp,
47 staging_snapshot_stamp_regexp)
48 )
Max Rasskazov7f3c53c2014-12-09 14:19:17 +030049
50 @property
51 def url(self):
52 return '{}::{}'.format(self.host, self.module)
53
54 @property
55 def root_url(self):
56 return '{}/{}'.format(self.url, self.root_path)
57
58 @property
59 def files_path(self):
60 return '{}/{}'.format(self.root_path, self.files_dir)
61
62 @property
63 def files_url(self):
64 return '{}/{}'.format(self.root_url, self.files_dir)
65
66 def http_url(self, path):
67 return 'http://{}/{}'.format(self.host, path)
68
69 def html_link(self, path, link_name):
70 return '<a href="{}">{}</a>'.format(self.http_url(path), link_name)
71
72 @property
73 def staging_dir(self):
74 return '{}-{}'.format(self.mirror_name, self.staging_snapshot_stamp)
75
76 @property
77 def staging_dir_path(self):
78 return '{}/{}'.format(self.files_path, self.staging_dir)
79
80 @property
81 def staging_dir_url(self):
82 return '{}/{}'.format(self.url, self.staging_dir_path)
83
84 @property
85 def staging_link(self):
86 return '{}-{}'.format(self.mirror_name, self.staging_postfix)
87
88 @property
89 def staging_link_path(self):
90 return '{}/{}'.format(self.files_path, self.staging_link)
91
92 @property
93 def staging_link_url(self):
94 return '{}/{}'.format(self.url, self.staging_link_path)
95
96 @property
97 def empty_dir(self):
98 if self.__dict__.get('_empty_dir') is None:
99 self._empty_dir = tempfile.mkdtemp()
100 return self._empty_dir
101
102 def symlink_to(self, target):
103 linkname = tempfile.mktemp()
104 os.symlink(target, linkname)
105 return linkname
106
107 def _shell(self, cmd, raise_error=True):
108 print cmd
109 process = subprocess.Popen(cmd,
110 stdin=subprocess.PIPE,
111 stdout=subprocess.PIPE,
112 stderr=subprocess.PIPE,
113 shell=True)
114 out, err = process.communicate()
115 exitcode = process.returncode
116 if process.returncode != 0 and raise_error:
117 msg = '"{cmd}" failed. Exit code == {exitcode}'\
118 '\n\nSTDOUT: \n{out}'\
119 '\n\nSTDERR: \n{err}'\
120 .format(**(locals()))
121 raise RuntimeError(msg)
122 return exitcode, out, err
123
124 def _do_rsync(self, source='', dest=None, opts='', extra=None):
125 if extra is None:
126 extra = self.rsync_extra_params
127 cmd = 'rsync {opts} {extra} {source} {dest}'.format(**(locals()))
128 return self._shell(cmd)
129
Max Rasskazove99d8372015-01-13 20:07:01 +0300130 def _rsync_ls(self, dirname=None, pattern=r'.*', opts=''):
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300131 if dirname is None:
132 dirname = '{}/'.format(self.root_path)
133 dest = '{}/{}'.format(self.url, dirname)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300134 extra = self.rsync_extra_params + ' --no-v'
135 exitcode, out, err = self._do_rsync(dest=dest, opts=opts, extra=extra)
Max Rasskazove99d8372015-01-13 20:07:01 +0300136 regexp = re.compile(pattern)
137 out = [_ for _ in out.splitlines()
138 if (_.split()[-1] != '.') and
139 (regexp.match(_.split()[-1]) is not None)]
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300140 return exitcode, out, err
141
Max Rasskazove99d8372015-01-13 20:07:01 +0300142 def rsync_ls(self, dirname, pattern=r'.*'):
143 exitcode, out, err = self._rsync_ls(dirname, pattern=pattern)
144 out = [_.split()[-1] for _ in out]
145 return exitcode, out, err
146
147 def rsync_ls_dirs(self, dirname, pattern=r'.*'):
148 exitcode, out, err = self._rsync_ls(dirname, pattern=pattern)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300149 out = [_.split()[-1] for _ in out if _.startswith('d')]
150 return exitcode, out, err
151
Max Rasskazove99d8372015-01-13 20:07:01 +0300152 def rsync_ls_symlinks(self, dirname, pattern=r'.*'):
153 exitcode, out, err = self._rsync_ls(dirname,
154 pattern=pattern,
155 opts='-l')
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300156 out = [_.split()[-3:] for _ in out if _.startswith('l')]
157 out = [[_[0], _[-1]] for _ in out]
158 return exitcode, out, err
159
160 def rsync_delete_file(self, filename):
161 dirname, filename = os.path.split(filename)
162 source = '{}/'.format(self.empty_dir)
163 dest = '{}/{}/'.format(self.url, dirname)
164 opts = "-r --delete --include={} '--exclude=*'".format(filename)
165 return self._do_rsync(source=source, dest=dest, opts=opts)
166
167 def rsync_delete_dir(self, dirname):
168 source = '{}/'.format(self.empty_dir)
169 dest = '{}/{}/'.format(self.url, dirname)
170 opts = "-a --delete"
171 exitcode, out, err = self._do_rsync(source=source,
172 dest=dest,
173 opts=opts)
174 return self.rsync_delete_file(dirname)
175
176 def rsync_staging_transfer(self, source, tgt_symlink_name=None):
177 if tgt_symlink_name is None:
178 tgt_symlink_name = self.mirror_name
179 opts = '--archive --force --ignore-errors '\
180 '--delete-excluded --no-owner --no-group --delete '\
181 '--link-dest=/{}'.format(self.staging_link_path)
182 try:
183 exitcode, out, err = self._do_rsync(source=source,
184 dest=self.staging_dir_url,
185 opts=opts)
186 self.rsync_delete_file(self.staging_link_path)
187 self._do_rsync(source=self.symlink_to(self.staging_dir),
188 dest=self.staging_link_url,
189 opts='-l')
190 # cleaning of old snapshots
Max Rasskazov3fe42712015-01-15 15:34:22 +0300191 self._remove_old_snapshots()
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300192 return exitcode, out, err
193 except RuntimeError as e:
194 print e.message
195 self.rsync_delete_dir(self.staging_dir_path)
196 raise
Max Rasskazov3fe42712015-01-15 15:34:22 +0300197
198 def _remove_old_snapshots(self, save_last_days=None):
199 if save_last_days is None:
200 save_last_days = self.save_last_days
201 if save_last_days is None \
202 or save_last_days is False \
203 or save_last_days == 0:
204 # skipping deletion if save_last_days == None or False or 0
205 print 'Skip deletion of old snapshots because of '\
206 'save_last_days == {}'.format(save_last_days)
207 return
208 warn_date = now - datetime.timedelta(days=save_last_days)
209 warn_date = datetime.datetime.combine(warn_date, datetime.time(0))
210 dirs = self.rsync_ls_dirs(
211 '{}/'.format(self.files_path),
212 pattern='^{}-{}'.format(self.mirror_name,
213 self.staging_snapshot_stamp_regexp)
214 )[1]
215 links = self.rsync_ls_symlinks('{}/'.format(self.root_path))[1]
216 links += self.rsync_ls_symlinks('{}/'.format(self.files_path))[1]
217 for d in dirs:
218 dir_date = datetime.datetime.strptime(
219 d,
220 '{}-{}'.format(self.mirror_name,
221 self.staging_snapshot_stamp_format)
222 )
223 dir_date = datetime.datetime.combine(dir_date, datetime.time(0))
224 dir_path = '{}/{}'.format(self.files_path, d)
225 if dir_date < warn_date:
226 dir_links = [_[0] for _ in links
227 if _[1] == d
228 or _[1].endswith('/{}'.format(d))
229 ]
230 if not dir_links:
231 self.rsync_delete_dir(dir_path)
232 else:
233 print 'Skip deletion of "{}" because there are '\
234 'symlinks found: {}'.format(d, dir_links)