| #!/usr/bin/env python |
| # -*- coding: utf-8 -*- |
| |
| # Copyright (c) 2015-2016, Mirantis, Inc. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| # not use this file except in compliance with the License. You may obtain |
| # a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations |
| # under the License. |
| |
| import os |
| |
| |
| from trsync.objects import RemoteLock |
| from trsync.objects import RsyncOps |
| from trsync.objects import RsyncUrl |
| from trsync.objects import TRsync |
| |
| |
| class TRSyncApi(object): |
| |
| def _success(self, server, message=None, force=False): |
| if force: |
| server['success'] = True |
| server['messages'].append(message) |
| |
| def _fail(self, server, message=None): |
| server['success'] = False |
| server['errors'].append(message) |
| |
| def _init_serverdata(self, dests): |
| """Make a serverdata object for each destiantion""" |
| return [{'dest': dest, |
| 'remote': None, |
| 'lock': None, |
| 'success': True, |
| 'messages': [], |
| 'errors': []} |
| for dest in dests] |
| |
| def _extract_stats(self, servers): |
| return dict((s['dest'], {'success': s['success'], |
| 'messages': s['messages'], |
| 'errors': s['errors']}) |
| for s in servers) |
| |
| def _connect_and_lock(self, servers, repo_name, **kwargs): |
| """Connect and lock each destination""" |
| for s in servers: |
| try: |
| remote = TRsync(s['dest'], **kwargs) |
| path = remote.get_repo_path(repo_name) |
| lock = RemoteLock(remote, path=path, force=False) |
| lock.acquire() |
| s['lock'] = lock |
| s['remote'] = remote |
| except Exception as e: |
| self._fail(s, 'Locking %s on %s: FAILED' % (path, s['dest']) + |
| "\n" + str(e)) |
| |
| def _do_push(self, servers, source_url, repo_name, symlinks=[]): |
| """Rsync stuff to all servers""" |
| for s in servers: |
| if s['remote'] is not None: |
| try: |
| s['remote'].push(source_url, repo_name, |
| symlinks=symlinks) |
| self._success(s, 'Push %s to %s: SUCCESS' |
| % (source_url, s)) |
| except Exception as e: |
| self._fail(s, 'Push %s to %s: FAILED' |
| % (source_url, s['dest']) + |
| "\n" + str(e)) |
| |
| def _release_locks(self, servers): |
| """Release locks on all destinations""" |
| for s in servers: |
| if s['lock'] is not None: |
| s['lock'].release() |
| |
| def _check_status(self, servers): |
| """Check if all went well""" |
| for s in servers: |
| if s['success'] is False: |
| return False |
| return True |
| |
| def _rollback(self, servers, repo_name): |
| """Undo the damage""" |
| for s in servers: |
| if s['remote'] is not None: |
| s['remote'].undo_push(repo_name) |
| |
| def _commit(self, servers, repo_name, symlinks=[]): |
| """Symlink the synced dir to where it belongs""" |
| for s in servers: |
| try: |
| s['remote'].symlink(repo_name, symlinks=symlinks) |
| self._success(s, "Symlinking SUCCESS") |
| except Exception as e: |
| self._fail(s, "Symlinking FAILED\n" + str(e)) |
| |
| def _delete_old_snapshots(self, servers, repo_name): |
| for s in servers: |
| s['remote'].remove_old_snapshots(repo_name) |
| |
| def push(self, source_url, dests, **kwargs): |
| symlinks = kwargs.pop('symlinks', []) |
| snapshot_name = kwargs.pop('snapshot_name', '') |
| source = RsyncOps(source_url) |
| source_url = source.url.url_dir() |
| snapshot_name = snapshot_name.strip(' /') or \ |
| os.path.basename(source.url.path) |
| if not snapshot_name: |
| raise RuntimeError("Could not infer snapshot name from source url" |
| " and snapshot_name was not provided.") |
| |
| servers = self._init_serverdata(dests) |
| |
| self._connect_and_lock(servers, snapshot_name, **kwargs) |
| self._do_push(servers, source_url, snapshot_name, symlinks) |
| |
| all_ok = self._check_status(servers) |
| if all_ok: |
| self._commit(servers, snapshot_name, symlinks) |
| self._delete_old_snapshots(servers, snapshot_name) |
| else: |
| self._rollback(servers, snapshot_name) |
| self._release_locks(servers) |
| |
| return self._extract_stats(servers) |
| |
| def symlink(self, dests, symlinks, target, update=False, |
| rsync_extra_params=""): |
| for symlink in symlinks: |
| if symlink.startswith('/') or symlink.startswith('../'): |
| raise RuntimeError('Symlink points outside the root url: {}' |
| .format(symlink)) |
| |
| servers = self._init_serverdata(dests) |
| for s in servers: |
| try: |
| s['remote'] = RsyncOps(s['dest'], |
| rsync_extra_params=rsync_extra_params) |
| for symlink in symlinks: |
| s['remote'].symlink(symlink, target, update=update) |
| self._success(s, 'Creating symlinks %s targeted to %s on %s: ' |
| 'SUCCESS' % |
| (str(symlinks), target, s['dest'])) |
| except Exception as e: |
| self._fail(s, 'Creating symlinks %s targeted to %s on %s: ' |
| 'FAILED' % (str(symlinks), target, s['dest']) + |
| "\n" + str(e)) |
| return self._extract_stats(servers) |
| |
| def remove(self, dests, path, rsync_extra_params=""): |
| servers = self._init_serverdata(dests) |
| for s in servers: |
| try: |
| s['remote'] = RsyncOps(s['dest'], |
| rsync_extra_params=rsync_extra_params) |
| s['remote'].rm_all(path) |
| self._success(s, 'Remove %s: SUCCESS' % (path)) |
| except Exception as e: |
| self._fail(s, 'Remove %s: FAILED' % (path) + |
| "\n" + str(e)) |
| return self._extract_stats(servers) |
| |
| def get_target(self, symlink_url, recursive=False): |
| url = RsyncUrl(symlink_url) |
| remote = RsyncOps(url.root) |
| return remote.symlink_target(url.path, recursive=recursive) |