Max Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | #-*- coding: utf-8 -*- |
| 3 | |
| 4 | |
| 5 | import datetime |
| 6 | import os |
Max Rasskazov | e99d837 | 2015-01-13 20:07:01 +0300 | [diff] [blame] | 7 | import re |
Max Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 8 | import subprocess |
| 9 | import tempfile |
| 10 | |
| 11 | |
Max Rasskazov | 3fe4271 | 2015-01-15 15:34:22 +0300 | [diff] [blame^] | 12 | now = datetime.datetime.utcnow() |
| 13 | staging_snapshot_stamp_format = r'%Y-%m-%d-%H%M%S' |
| 14 | staging_snapshot_stamp_regexp = r'[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}' |
| 15 | staging_snapshot_stamp = now.strftime(staging_snapshot_stamp_format) |
Max Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 16 | |
| 17 | |
| 18 | class 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 Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 35 | self.staging_postfix = staging_postfix |
Max Rasskazov | 3fe4271 | 2015-01-15 15:34:22 +0300 | [diff] [blame^] | 36 | 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 Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 49 | |
| 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 Rasskazov | e99d837 | 2015-01-13 20:07:01 +0300 | [diff] [blame] | 130 | def _rsync_ls(self, dirname=None, pattern=r'.*', opts=''): |
Max Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 131 | if dirname is None: |
| 132 | dirname = '{}/'.format(self.root_path) |
| 133 | dest = '{}/{}'.format(self.url, dirname) |
Max Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 134 | extra = self.rsync_extra_params + ' --no-v' |
| 135 | exitcode, out, err = self._do_rsync(dest=dest, opts=opts, extra=extra) |
Max Rasskazov | e99d837 | 2015-01-13 20:07:01 +0300 | [diff] [blame] | 136 | regexp = re.compile(pattern) |
| 137 | out = [_ for _ in out.splitlines() |
| 138 | if (_.split()[-1] != '.') and |
| 139 | (regexp.match(_.split()[-1]) is not None)] |
Max Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 140 | return exitcode, out, err |
| 141 | |
Max Rasskazov | e99d837 | 2015-01-13 20:07:01 +0300 | [diff] [blame] | 142 | 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 Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 149 | out = [_.split()[-1] for _ in out if _.startswith('d')] |
| 150 | return exitcode, out, err |
| 151 | |
Max Rasskazov | e99d837 | 2015-01-13 20:07:01 +0300 | [diff] [blame] | 152 | def rsync_ls_symlinks(self, dirname, pattern=r'.*'): |
| 153 | exitcode, out, err = self._rsync_ls(dirname, |
| 154 | pattern=pattern, |
| 155 | opts='-l') |
Max Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 156 | 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 Rasskazov | 3fe4271 | 2015-01-15 15:34:22 +0300 | [diff] [blame^] | 191 | self._remove_old_snapshots() |
Max Rasskazov | 7f3c53c | 2014-12-09 14:19:17 +0300 | [diff] [blame] | 192 | 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 Rasskazov | 3fe4271 | 2015-01-15 15:34:22 +0300 | [diff] [blame^] | 197 | |
| 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) |