blob: 944b74bb040635c21eb6ca94e4faaacbee82924b [file] [log] [blame]
Max Rasskazov7f3c53c2014-12-09 14:19:17 +03001#!/usr/bin/env python
2#-*- coding: utf-8 -*-
3
4
5import datetime
Max Rasskazov11653ab2015-01-15 15:45:16 +03006import logging
Max Rasskazov7f3c53c2014-12-09 14:19:17 +03007import os
Max Rasskazove99d8372015-01-13 20:07:01 +03008import re
Max Rasskazov7f3c53c2014-12-09 14:19:17 +03009import subprocess
10import tempfile
11
12
Max Rasskazov11653ab2015-01-15 15:45:16 +030013logging.basicConfig(level=logging.INFO)
14logger = logging.getLogger('rsync_staging')
15
Max Rasskazov3fe42712015-01-15 15:34:22 +030016now = datetime.datetime.utcnow()
17staging_snapshot_stamp_format = r'%Y-%m-%d-%H%M%S'
18staging_snapshot_stamp_regexp = r'[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}'
19staging_snapshot_stamp = now.strftime(staging_snapshot_stamp_format)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +030020
21
22class RemoteRsyncStaging(object):
23 def __init__(self,
24 mirror_name,
25 host,
26 module='mirror-sync',
27 root_path='fwm',
28 files_dir='files',
29 save_last_days=61,
30 rsync_extra_params='-v',
31 staging_postfix='staging'):
32 self.mirror_name = mirror_name
33 self.host = host
34 self.module = module
35 self.root_path = root_path
36 self.files_dir = files_dir
37 self.save_last_days = save_last_days
38 self.rsync_extra_params = rsync_extra_params
Max Rasskazov7f3c53c2014-12-09 14:19:17 +030039 self.staging_postfix = staging_postfix
Max Rasskazov3fe42712015-01-15 15:34:22 +030040 self.staging_snapshot_stamp = staging_snapshot_stamp
41 self.staging_snapshot_stamp_format = staging_snapshot_stamp_format
42 if re.match(staging_snapshot_stamp_regexp,
43 self.staging_snapshot_stamp) \
44 is not None:
45 self.staging_snapshot_stamp_regexp = staging_snapshot_stamp_regexp
46 else:
47 raise RuntimeError('Wrong regexp for staging_snapshot_stamp\n'
48 'staging_snapshot_stamp = "{}"\n'
49 'staging_snapshot_stamp_regexp = "{}"'.
50 format(staging_snapshot_stamp,
51 staging_snapshot_stamp_regexp)
52 )
Max Rasskazov7f3c53c2014-12-09 14:19:17 +030053
54 @property
55 def url(self):
56 return '{}::{}'.format(self.host, self.module)
57
58 @property
59 def root_url(self):
60 return '{}/{}'.format(self.url, self.root_path)
61
62 @property
63 def files_path(self):
64 return '{}/{}'.format(self.root_path, self.files_dir)
65
66 @property
67 def files_url(self):
68 return '{}/{}'.format(self.root_url, self.files_dir)
69
70 def http_url(self, path):
71 return 'http://{}/{}'.format(self.host, path)
72
73 def html_link(self, path, link_name):
74 return '<a href="{}">{}</a>'.format(self.http_url(path), link_name)
75
76 @property
77 def staging_dir(self):
78 return '{}-{}'.format(self.mirror_name, self.staging_snapshot_stamp)
79
80 @property
81 def staging_dir_path(self):
82 return '{}/{}'.format(self.files_path, self.staging_dir)
83
84 @property
85 def staging_dir_url(self):
86 return '{}/{}'.format(self.url, self.staging_dir_path)
87
88 @property
89 def staging_link(self):
90 return '{}-{}'.format(self.mirror_name, self.staging_postfix)
91
92 @property
93 def staging_link_path(self):
94 return '{}/{}'.format(self.files_path, self.staging_link)
95
96 @property
97 def staging_link_url(self):
98 return '{}/{}'.format(self.url, self.staging_link_path)
99
100 @property
101 def empty_dir(self):
102 if self.__dict__.get('_empty_dir') is None:
103 self._empty_dir = tempfile.mkdtemp()
104 return self._empty_dir
105
106 def symlink_to(self, target):
107 linkname = tempfile.mktemp()
108 os.symlink(target, linkname)
109 return linkname
110
111 def _shell(self, cmd, raise_error=True):
Max Rasskazov11653ab2015-01-15 15:45:16 +0300112 logger.info(cmd)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300113 process = subprocess.Popen(cmd,
114 stdin=subprocess.PIPE,
115 stdout=subprocess.PIPE,
116 stderr=subprocess.PIPE,
117 shell=True)
118 out, err = process.communicate()
Max Rasskazov11653ab2015-01-15 15:45:16 +0300119 logger.debug(out)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300120 exitcode = process.returncode
121 if process.returncode != 0 and raise_error:
122 msg = '"{cmd}" failed. Exit code == {exitcode}'\
123 '\n\nSTDOUT: \n{out}'\
124 '\n\nSTDERR: \n{err}'\
125 .format(**(locals()))
Max Rasskazov11653ab2015-01-15 15:45:16 +0300126 logger.error(msg)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300127 raise RuntimeError(msg)
128 return exitcode, out, err
129
130 def _do_rsync(self, source='', dest=None, opts='', extra=None):
131 if extra is None:
132 extra = self.rsync_extra_params
133 cmd = 'rsync {opts} {extra} {source} {dest}'.format(**(locals()))
134 return self._shell(cmd)
135
Max Rasskazove99d8372015-01-13 20:07:01 +0300136 def _rsync_ls(self, dirname=None, pattern=r'.*', opts=''):
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300137 if dirname is None:
138 dirname = '{}/'.format(self.root_path)
139 dest = '{}/{}'.format(self.url, dirname)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300140 extra = self.rsync_extra_params + ' --no-v'
141 exitcode, out, err = self._do_rsync(dest=dest, opts=opts, extra=extra)
Max Rasskazove99d8372015-01-13 20:07:01 +0300142 regexp = re.compile(pattern)
143 out = [_ for _ in out.splitlines()
144 if (_.split()[-1] != '.') and
145 (regexp.match(_.split()[-1]) is not None)]
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300146 return exitcode, out, err
147
Max Rasskazove99d8372015-01-13 20:07:01 +0300148 def rsync_ls(self, dirname, pattern=r'.*'):
149 exitcode, out, err = self._rsync_ls(dirname, pattern=pattern)
150 out = [_.split()[-1] for _ in out]
151 return exitcode, out, err
152
153 def rsync_ls_dirs(self, dirname, pattern=r'.*'):
154 exitcode, out, err = self._rsync_ls(dirname, pattern=pattern)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300155 out = [_.split()[-1] for _ in out if _.startswith('d')]
156 return exitcode, out, err
157
Max Rasskazove99d8372015-01-13 20:07:01 +0300158 def rsync_ls_symlinks(self, dirname, pattern=r'.*'):
159 exitcode, out, err = self._rsync_ls(dirname,
160 pattern=pattern,
161 opts='-l')
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300162 out = [_.split()[-3:] for _ in out if _.startswith('l')]
163 out = [[_[0], _[-1]] for _ in out]
164 return exitcode, out, err
165
166 def rsync_delete_file(self, filename):
167 dirname, filename = os.path.split(filename)
168 source = '{}/'.format(self.empty_dir)
169 dest = '{}/{}/'.format(self.url, dirname)
170 opts = "-r --delete --include={} '--exclude=*'".format(filename)
171 return self._do_rsync(source=source, dest=dest, opts=opts)
172
173 def rsync_delete_dir(self, dirname):
174 source = '{}/'.format(self.empty_dir)
175 dest = '{}/{}/'.format(self.url, dirname)
176 opts = "-a --delete"
177 exitcode, out, err = self._do_rsync(source=source,
178 dest=dest,
179 opts=opts)
180 return self.rsync_delete_file(dirname)
181
182 def rsync_staging_transfer(self, source, tgt_symlink_name=None):
183 if tgt_symlink_name is None:
184 tgt_symlink_name = self.mirror_name
185 opts = '--archive --force --ignore-errors '\
186 '--delete-excluded --no-owner --no-group --delete '\
187 '--link-dest=/{}'.format(self.staging_link_path)
188 try:
189 exitcode, out, err = self._do_rsync(source=source,
190 dest=self.staging_dir_url,
191 opts=opts)
192 self.rsync_delete_file(self.staging_link_path)
193 self._do_rsync(source=self.symlink_to(self.staging_dir),
194 dest=self.staging_link_url,
195 opts='-l')
196 # cleaning of old snapshots
Max Rasskazov3fe42712015-01-15 15:34:22 +0300197 self._remove_old_snapshots()
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300198 return exitcode, out, err
199 except RuntimeError as e:
Max Rasskazov11653ab2015-01-15 15:45:16 +0300200 logger.error(e.message)
Max Rasskazov7f3c53c2014-12-09 14:19:17 +0300201 self.rsync_delete_dir(self.staging_dir_path)
202 raise
Max Rasskazov3fe42712015-01-15 15:34:22 +0300203
204 def _remove_old_snapshots(self, save_last_days=None):
205 if save_last_days is None:
206 save_last_days = self.save_last_days
207 if save_last_days is None \
208 or save_last_days is False \
209 or save_last_days == 0:
210 # skipping deletion if save_last_days == None or False or 0
Max Rasskazov11653ab2015-01-15 15:45:16 +0300211 logger.info('Skip deletion of old snapshots because of '
212 'save_last_days == {}'.format(save_last_days))
Max Rasskazov3fe42712015-01-15 15:34:22 +0300213 return
214 warn_date = now - datetime.timedelta(days=save_last_days)
215 warn_date = datetime.datetime.combine(warn_date, datetime.time(0))
216 dirs = self.rsync_ls_dirs(
217 '{}/'.format(self.files_path),
218 pattern='^{}-{}'.format(self.mirror_name,
219 self.staging_snapshot_stamp_regexp)
220 )[1]
221 links = self.rsync_ls_symlinks('{}/'.format(self.root_path))[1]
222 links += self.rsync_ls_symlinks('{}/'.format(self.files_path))[1]
223 for d in dirs:
224 dir_date = datetime.datetime.strptime(
225 d,
226 '{}-{}'.format(self.mirror_name,
227 self.staging_snapshot_stamp_format)
228 )
229 dir_date = datetime.datetime.combine(dir_date, datetime.time(0))
230 dir_path = '{}/{}'.format(self.files_path, d)
231 if dir_date < warn_date:
232 dir_links = [_[0] for _ in links
233 if _[1] == d
234 or _[1].endswith('/{}'.format(d))
235 ]
236 if not dir_links:
237 self.rsync_delete_dir(dir_path)
238 else:
Max Rasskazov11653ab2015-01-15 15:45:16 +0300239 logger.info('Skip deletion of "{}" because there are '
240 'symlinks found: {}'.format(d, dir_links))