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 |
| 7 | import subprocess |
| 8 | import tempfile |
| 9 | |
| 10 | |
| 11 | now = datetime.datetime.now() |
| 12 | staging_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 | |
| 17 | class 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 |