Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 1 | #-*- coding: utf-8 -*- |
| 2 | |
| 3 | import os |
| 4 | import re |
| 5 | |
| 6 | import utils |
| 7 | |
| 8 | from tempfiles import TempFiles |
| 9 | from shell import Shell |
| 10 | from rsync_url import RsyncUrl |
| 11 | |
| 12 | |
| 13 | class RsyncRemote(object): |
| 14 | def __init__(self, |
| 15 | rsync_url, |
| 16 | rsync_extra_params='-v --no-owner --no-group', |
| 17 | ): |
| 18 | # TODO: retry parameters for rsync |
| 19 | self.logger = utils.logger.getChild('RsyncRemote.' + rsync_url) |
| 20 | self.tmp = TempFiles() |
| 21 | self.shell = Shell(self.logger) |
Max Rasskazov | d3f57d8 | 2015-06-05 15:56:53 +0300 | [diff] [blame] | 22 | self.url = RsyncUrl(rsync_url) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 23 | self.rsync_extra_params = rsync_extra_params |
| 24 | |
Max Rasskazov | 32798e3 | 2015-07-13 21:02:04 +0300 | [diff] [blame] | 25 | def _rsync_push(self, source='', dest=None, opts='', extra=None): |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 26 | # TODO: retry for rsync |
| 27 | # TODO: locking: |
| 28 | # https://review.openstack.org/#/c/147120/4/utils/simple_http_daemon.py |
| 29 | # create lock-files on remotes during operations |
Max Rasskazov | 6d7b5c8 | 2015-07-13 13:31:58 +0300 | [diff] [blame] | 30 | # symlink dir-timestamp.lock -> dir-timestamp |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 31 | # for reading and writing |
| 32 | # special option for ignore lock-files (for manual fixing) |
| 33 | # all high-level functions (like ls) specify type of lock(read or |
Max Rasskazov | 32798e3 | 2015-07-13 21:02:04 +0300 | [diff] [blame] | 34 | # write), and _rsync_push creates special lock file on remote. |
| 35 | # also _rsync_push uses retry for waiting wnen resource will be |
| 36 | # unlocked |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 37 | # TODO: check for url compatibility (local->remote, remote->local, |
| 38 | # local->local) |
Max Rasskazov | d3f57d8 | 2015-06-05 15:56:53 +0300 | [diff] [blame] | 39 | dest = self.url.urljoin(dest) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 40 | allextra = self.rsync_extra_params |
| 41 | if extra is not None: |
| 42 | allextra = ' '.join((allextra, extra)) |
| 43 | cmd = 'rsync {opts} {allextra} {source} {dest}'.format(**(locals())) |
| 44 | return self.shell.shell(cmd)[1] |
| 45 | |
Max Rasskazov | 9d82e20 | 2015-07-13 21:03:32 +0300 | [diff] [blame] | 46 | def _rsync_pull(self, source='', dest=None, opts='', extra=None): |
| 47 | source = self.url.urljoin(self.symlink_target(source)) |
| 48 | #opts = '--archive --force --ignore-errors --delete --copy-dirlinks' |
| 49 | opts = '--archive --force --ignore-errors --delete' |
| 50 | # TODO: if dest is dir - detect dest on this alhorithm |
| 51 | # or don't touch it if it file |
| 52 | if dest is None: |
| 53 | raise RuntimeError('There are no "dest" specified for pull {}' |
| 54 | ''.format(source)) |
| 55 | dest = self.url.a_file(dest, os.path.split(self.url.path)[-1]) |
| 56 | allextra = self.rsync_extra_params |
| 57 | if extra is not None: |
| 58 | allextra = ' '.join((allextra, extra)) |
| 59 | cmd = 'rsync {opts} {allextra} {source} {dest}'.format(**(locals())) |
| 60 | return self.shell.shell(cmd)[1] |
| 61 | |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 62 | def _rsync_ls(self, dirname=None, pattern=r'.*', opts=''): |
| 63 | extra = '--no-v' |
Max Rasskazov | 32798e3 | 2015-07-13 21:02:04 +0300 | [diff] [blame] | 64 | out = self._rsync_push(dest=dirname, opts=opts, extra=extra) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 65 | regexp = re.compile(pattern) |
| 66 | out = [_ for _ in out.splitlines() |
| 67 | if (_.split()[-1] != '.') and |
| 68 | (regexp.match(_.split()[-1]) is not None)] |
| 69 | return out |
| 70 | |
| 71 | def ls(self, dirname=None, pattern=r'.*'): |
| 72 | self.logger.debug('ls on "{}", pattern="{}"'.format(dirname, pattern)) |
| 73 | out = self._rsync_ls(dirname, pattern=pattern) |
| 74 | out = [_.split()[-1] for _ in out] |
| 75 | return out |
| 76 | |
| 77 | def ls_dirs(self, dirname=None, pattern=r'.*'): |
| 78 | self.logger.debug('ls dirs on "{}", pattern="{}"' |
| 79 | ''.format(dirname, pattern)) |
| 80 | out = self._rsync_ls(dirname, pattern=pattern) |
| 81 | out = [_.split()[-1] for _ in out if _.startswith('d')] |
| 82 | return out |
| 83 | |
| 84 | def ls_symlinks(self, dirname=None, pattern=r'.*'): |
| 85 | self.logger.debug('ls symlinks on "{}", pattern="{}"' |
| 86 | ''.format(dirname, pattern)) |
| 87 | out = self._rsync_ls(dirname, pattern=pattern, opts='-l') |
| 88 | out = [_.split()[-3:] for _ in out if _.startswith('l')] |
| 89 | out = [[_[0], _[-1]] for _ in out] |
| 90 | return out |
| 91 | |
Max Rasskazov | 20643b4 | 2015-06-22 20:36:48 +0300 | [diff] [blame] | 92 | def symlink_target(self, symlink): |
| 93 | target = symlink |
| 94 | while True: |
| 95 | try: |
| 96 | target_path = os.path.split(target)[0] |
| 97 | target = self.ls_symlinks(target)[0][1] |
| 98 | target = os.path.join(target_path, target) |
| 99 | except: |
| 100 | return target |
| 101 | |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 102 | def rmfile(self, filename): |
| 103 | '''Removes file on rsync_url.''' |
| 104 | report_name = filename |
| 105 | dirname, filename = os.path.split(filename) |
Max Rasskazov | 46cc073 | 2015-06-05 19:23:24 +0300 | [diff] [blame] | 106 | dirname = self.url.a_dir(dirname) |
| 107 | source = self.url.a_dir(self.tmp.empty_dir) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 108 | opts = "-r --delete --include={} '--exclude=*'".format(filename) |
| 109 | self.logger.info('Removing file "{}"'.format(report_name)) |
Max Rasskazov | 32798e3 | 2015-07-13 21:02:04 +0300 | [diff] [blame] | 110 | return self._rsync_push(source=source, dest=dirname, opts=opts) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 111 | |
| 112 | def cleandir(self, dirname): |
| 113 | '''Removes directories (recursive) on rsync_url''' |
Max Rasskazov | 46cc073 | 2015-06-05 19:23:24 +0300 | [diff] [blame] | 114 | dirname = self.url.a_dir(dirname) |
| 115 | source = self.url.a_dir(self.tmp.empty_dir) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 116 | opts = "-a --delete" |
| 117 | self.logger.info('Cleaning directory "{}"'.format(dirname)) |
Max Rasskazov | 32798e3 | 2015-07-13 21:02:04 +0300 | [diff] [blame] | 118 | return self._rsync_push(source=source, dest=dirname, opts=opts) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 119 | |
| 120 | def rmdir(self, dirname): |
| 121 | '''Removes directories (recursive) on rsync_url''' |
| 122 | self.logger.info('Removing directory "{}"'.format(dirname)) |
| 123 | self.cleandir(dirname) |
Max Rasskazov | 46cc073 | 2015-06-05 19:23:24 +0300 | [diff] [blame] | 124 | return self.rmfile(self.url.a_file(dirname)) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 125 | |
| 126 | def mkdir(self, dirname): |
| 127 | '''Creates directories (recirsive, like mkdir -p) on rsync_url''' |
Max Rasskazov | 46cc073 | 2015-06-05 19:23:24 +0300 | [diff] [blame] | 128 | source = self.url.a_dir(self.tmp.get_temp_dir(dirname)) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 129 | opts = "-a" |
| 130 | self.logger.info('Creating directory "{}"'.format(dirname)) |
Max Rasskazov | 32798e3 | 2015-07-13 21:02:04 +0300 | [diff] [blame] | 131 | return self._rsync_push(source=source, opts=opts) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 132 | |
Max Rasskazov | eb72c93 | 2015-07-13 21:25:34 +0300 | [diff] [blame] | 133 | def symlink(self, symlink, target, |
| 134 | create_target_file=True, store_history=True): |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 135 | '''Creates symlink targeted to target''' |
Max Rasskazov | 96bb029 | 2015-06-23 15:22:05 +0300 | [diff] [blame] | 136 | |
Max Rasskazov | 46cc073 | 2015-06-05 19:23:24 +0300 | [diff] [blame] | 137 | symlink = self.url.a_file(symlink) |
Max Rasskazov | 96bb029 | 2015-06-23 15:22:05 +0300 | [diff] [blame] | 138 | if create_target_file is True: |
Max Rasskazov | eb72c93 | 2015-07-13 21:25:34 +0300 | [diff] [blame] | 139 | infofile = '{}.target.txt'.format(symlink) |
| 140 | if store_history is True: |
| 141 | temp_dir = self.tmp.get_temp_dir() |
| 142 | source = '{}/{}'.format(temp_dir, os.path.split(infofile)[-1]) |
| 143 | try: |
| 144 | self._rsync_pull(source=infofile, dest=source) |
| 145 | with open(source, 'r') as inf: |
| 146 | content = '{}\n{}'.format(target, inf.read()) |
| 147 | except RuntimeError: |
| 148 | content = target |
| 149 | with open(source, 'w') as outf: |
| 150 | outf.write(content) |
| 151 | else: |
| 152 | source = self.tmp.get_file(content='{}'.format(target)) |
| 153 | temp_dir = self.tmp.last_temp_dir |
Max Rasskazov | 96bb029 | 2015-06-23 15:22:05 +0300 | [diff] [blame] | 154 | self.rmfile(infofile) |
| 155 | self.logger.info('Creating informaion file "{}"'.format(infofile)) |
Max Rasskazov | 32798e3 | 2015-07-13 21:02:04 +0300 | [diff] [blame] | 156 | self._rsync_push(source=source, dest=infofile) |
Max Rasskazov | 96bb029 | 2015-06-23 15:22:05 +0300 | [diff] [blame] | 157 | else: |
| 158 | temp_dir = self.tmp.get_temp_dir() |
| 159 | |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 160 | opts = "-l" |
Max Rasskazov | 96bb029 | 2015-06-23 15:22:05 +0300 | [diff] [blame] | 161 | source = self.tmp.get_symlink_to(target, temp_dir=temp_dir) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 162 | self.rmfile(symlink) |
| 163 | self.logger.info('Creating symlink "{}" -> "{}"' |
| 164 | ''.format(symlink, target)) |
Max Rasskazov | 32798e3 | 2015-07-13 21:02:04 +0300 | [diff] [blame] | 165 | return self._rsync_push(source=source, dest=symlink, opts=opts) |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 166 | |
Max Rasskazov | a9f8973 | 2015-06-17 01:25:23 +0300 | [diff] [blame] | 167 | def push(self, source, repo_name=None, extra=None): |
Max Rasskazov | 8608943 | 2015-06-04 22:07:20 +0300 | [diff] [blame] | 168 | '''Push source to destination''' |
| 169 | opts = '--archive --force --ignore-errors --delete' |
Max Rasskazov | a9f8973 | 2015-06-17 01:25:23 +0300 | [diff] [blame] | 170 | self.logger.info('Push "{}" to "{}"'.format(source, repo_name)) |
Max Rasskazov | 32798e3 | 2015-07-13 21:02:04 +0300 | [diff] [blame] | 171 | return self._rsync_push(source=source, |
| 172 | dest=repo_name, |
| 173 | opts=opts, |
| 174 | extra=extra) |