Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # -*- coding: utf-8 -*- |
| 3 | |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 4 | # Copyright (c) 2015-2016, Mirantis, Inc. |
| 5 | # |
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 7 | # not use this file except in compliance with the License. You may obtain |
| 8 | # a copy of the License at |
| 9 | # |
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | # |
| 12 | # Unless required by applicable law or agreed to in writing, software |
| 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 15 | # License for the specific language governing permissions and limitations |
| 16 | # under the License. |
| 17 | |
| 18 | import logging |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 19 | import os |
| 20 | import sys |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 21 | |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 22 | from cliff import app |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 23 | from cliff import command |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 24 | from cliff import commandmanager |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 25 | |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 26 | import trsync |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 27 | |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 28 | from trsync.objects import rsync_mirror |
| 29 | |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 30 | |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 31 | class PushCmd(command.Command): |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 32 | log = logging.getLogger(__name__) |
| 33 | |
| 34 | def get_description(self): |
| 35 | return "push SRC to several DST with snapshots" |
| 36 | |
| 37 | def get_parser(self, prog_name): |
| 38 | parser = super(PushCmd, self).get_parser(prog_name) |
| 39 | parser.add_argument('source', help='Source path') |
| 40 | parser.add_argument('mirror_name', help='Mirror name') |
| 41 | parser.add_argument('-d', '--dest', |
| 42 | nargs='+', |
| 43 | required=True, |
| 44 | help='Destination rsync url') |
| 45 | parser.add_argument('-t', '--timestamp', |
| 46 | required=False, |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 47 | help='Specified timestamp will be used for ' |
| 48 | 'snapshot. Format:yyyy-mm-dd-hhMMSS') |
Max Rasskazov | 9f5e54e | 2016-06-08 23:40:52 +0300 | [diff] [blame^] | 49 | parser.add_argument('--snapshots-dir', '--snapshot-dir', |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 50 | required=False, |
| 51 | default='snapshots', |
Max Rasskazov | 9f5e54e | 2016-06-08 23:40:52 +0300 | [diff] [blame^] | 52 | help='Directory name for snapshots relative ' |
| 53 | '"destination". "snapshots" by default') |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 54 | parser.add_argument('--init-directory-structure', |
| 55 | action='store_true', |
| 56 | required=False, |
| 57 | default=False, |
| 58 | help='It specified, all directories including' |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 59 | '"snapshots-dir" will be created on remote ' |
| 60 | 'location') |
Max Rasskazov | 9f5e54e | 2016-06-08 23:40:52 +0300 | [diff] [blame^] | 61 | parser.add_argument('--snapshot-lifetime', '--save-latest-days', |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 62 | required=False, |
| 63 | default=61, |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 64 | help='Snapshots for specified number of days will ' |
| 65 | 'be saved. All older will be removed. 61 by ' |
| 66 | 'default. 0 mean that old snapshots will not be ' |
| 67 | 'deleted, "None" mean that all snapshots ' |
| 68 | 'excluding latest will be deleted') |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 69 | parser.add_argument('--latest-successful-postfix', |
| 70 | required=False, |
| 71 | default='latest', |
| 72 | help='Postfix for symlink to latest successfully ' |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 73 | 'synced snapshot. Also used as --link-dest ' |
| 74 | 'target. "latest" by default.') |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 75 | parser.add_argument('-s', '--symlinks', |
| 76 | nargs='+', |
| 77 | required=False, |
| 78 | default=[], |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 79 | help='Update additional symlinks relative ' |
| 80 | 'destination') |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 81 | parser.add_argument('--extra', |
| 82 | required=False, |
| 83 | default='', |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 84 | help='String with additional rsync parameters. ' |
| 85 | 'For example it may be "\--dry-run ' |
| 86 | '--any-rsync-option".Use "\\" to disable ' |
| 87 | 'argparse to parse extra value.') |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 88 | |
| 89 | return parser |
| 90 | |
| 91 | def take_action(self, parsed_args): |
| 92 | properties = vars(parsed_args) |
| 93 | source_dir = properties.pop('source', None) |
Max Rasskazov | 7a50223 | 2016-04-12 09:37:49 +0300 | [diff] [blame] | 94 | mirror_name = properties.pop('mirror_name', None).strip('/') |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 95 | symlinks = properties.pop('symlinks', None) |
| 96 | servers = properties.pop('dest', None) |
| 97 | if properties['extra'].startswith('\\'): |
| 98 | properties['extra'] = properties['extra'][1:] |
| 99 | properties['rsync_extra_params'] = properties.pop('extra') |
Max Rasskazov | 9f5e54e | 2016-06-08 23:40:52 +0300 | [diff] [blame^] | 100 | properties['snapshot_lifetime'] = \ |
| 101 | None if properties['snapshot_lifetime'] == 'None' \ |
| 102 | else int(properties['snapshot_lifetime']) |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 103 | |
| 104 | failed = list() |
| 105 | for server in servers: |
| 106 | source_dir = os.path.realpath(source_dir) |
| 107 | if not source_dir.endswith('/'): |
| 108 | source_dir += '/' |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 109 | remote = rsync_mirror.TRsync(server, **properties) |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 110 | try: |
| 111 | remote.push(source_dir, mirror_name, symlinks=symlinks) |
| 112 | except Exception as e: |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 113 | print(e.message) |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 114 | failed.append(server) |
| 115 | |
| 116 | if failed: |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 117 | print("Failed to push to {}".format(str(failed))) |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 118 | sys.exit(1) |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 119 | # self.app.stdout.write(parsed_args.arg + "\n") |
| 120 | |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 121 | |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 122 | class RemoveCmd(command.Command): |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 123 | log = logging.getLogger(__name__) |
| 124 | |
| 125 | def get_description(self): |
| 126 | return "remove all specified paths from several DST recursively" |
| 127 | |
| 128 | def get_parser(self, prog_name): |
| 129 | parser = super(RemoveCmd, self).get_parser(prog_name) |
| 130 | |
| 131 | parser.add_argument('path', |
| 132 | nargs='+', |
| 133 | help='Path to remove') |
| 134 | parser.add_argument('-d', '--dest', |
| 135 | nargs='+', |
| 136 | required=True, |
| 137 | help='Destination rsync url') |
| 138 | parser.add_argument('--extra', |
| 139 | required=False, |
| 140 | default='', |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 141 | help='String with additional rsync parameters. ' |
| 142 | 'For example it may be "\--dry-run ' |
| 143 | '--any-rsync-option". Use "\\" to disable ' |
| 144 | 'argparse to parse extra value.') |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 145 | return parser |
| 146 | |
| 147 | def take_action(self, parsed_args): |
| 148 | properties = vars(parsed_args) |
| 149 | servers = properties.pop('dest', None) |
| 150 | path = properties.pop('path', None) |
| 151 | if properties['extra'].startswith('\\'): |
| 152 | properties['extra'] = properties['extra'][1:] |
| 153 | properties['init_directory_structure'] = False |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 154 | properties['rsync_extra_params'] = properties.pop('extra') |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 155 | |
| 156 | failed = list() |
| 157 | for server in servers: |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 158 | remote = rsync_mirror.TRsync(server, **properties) |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 159 | try: |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 160 | print("Removing items {}".format(str(path))) |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 161 | remote.rm_all(path) |
| 162 | except Exception as e: |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 163 | print(e.message) |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 164 | failed.append(server) |
| 165 | |
| 166 | if failed: |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 167 | print("Failed to remove {}".format(str(failed))) |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 168 | sys.exit(1) |
| 169 | |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 170 | |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 171 | class TRsyncApp(app.App): |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 172 | log = logging.getLogger(__name__) |
| 173 | |
| 174 | def __init__(self): |
| 175 | super(TRsyncApp, self).__init__( |
| 176 | description='TRsync', |
| 177 | version=trsync.__version__, |
Alexey Golubev | 4997bc0 | 2016-03-31 15:33:01 +0300 | [diff] [blame] | 178 | command_manager=commandmanager.CommandManager('trsync'), |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 179 | deferred_help=True, |
| 180 | ) |
| 181 | |
Max Rasskazov | 9ae40e5 | 2016-06-09 12:22:58 +0300 | [diff] [blame] | 182 | |
Alexey Golubev | 6f55d7e | 2016-03-23 16:16:29 +0300 | [diff] [blame] | 183 | def main(argv=sys.argv[1:]): |
| 184 | app = TRsyncApp() |
| 185 | return app.run(argv) |
| 186 | |
| 187 | if __name__ == '__main__': |
| 188 | sys.exit(main(sys.argv[1:])) |