# HG changeset patch # User Tero Marttila # Date 1361042479 -7200 # Node ID 91153d8b499c1d9e4d6fe5bed7ad0e7bfa0d1f89 # Parent f96289ad970c183e3fc254d126f1438ab7f13831 tweak pvl.backup-rsync: multiple --restrict-paths, explicit --allow-remote, minor bugfixes diff -r f96289ad970c -r 91153d8b499c bin/pvl.backup-rsync --- a/bin/pvl.backup-rsync Tue Jun 19 11:35:08 2012 +0300 +++ b/bin/pvl.backup-rsync Sat Feb 16 21:21:19 2013 +0200 @@ -1,9 +1,11 @@ #!/usr/bin/python """ - SSH authorized_keys command="..." wrapper for rsync. + SSH authorized_keys command="..." wrapper for rsync sender/server, with additional support for LVM snapshots. - Testing goes something like: + Testing: + PYTHONPATH=. ./bin/pvl.backup-rsync -- -rlptx /etc/apache2 test/foo + sudo PYTHONPATH=. ./bin/pvl.backup-rsync --command 'rsync --server --sender -ax . lvm:asdf:test' -vD sudo sh -c "PYTHONPATH=. rsync -e './bin/pvl.backup-rsync --debug -C --' -ax testing:lvm:asdf:test test/tmp" @@ -51,7 +53,7 @@ help="rsync command to execute") parser.add_option('-C', '--given-command', action='store_true', default=False, - help="use given command in `rsync -e %prog` format") + help="use given command in `rsync -e '%prog -C --' ...` format") parser.add_option('-n', '--noop', action='store_true', default=False, help="Parse command, but do not execute") @@ -59,9 +61,12 @@ parser.add_option('-R', '--readonly', action='store_true', default=False, help="restrict to read/source mode") - parser.add_option('-P', '--restrict-path', metavar='PATH', default=False, + parser.add_option('-P', '--restrict-path', metavar='PATH', action='append', help="restrict to given path prefix") + parser.add_option('--allow-remote', action='store_true', default=False, + help="Allow remote rsync sources") + # lvm options parser.add_option('-L', '--snapshot-size', metavar='SIZE', default=lvm.LVM_SNAPSHOT_SIZE, help="create snapshot with given LV size (used to store writes during backup)") @@ -76,6 +81,8 @@ parser.set_defaults( debug_for = [], loglevel = logging.INFO, + + restrict_path = [], ) # parse @@ -93,9 +100,9 @@ return options, args -def rsync_wrapper (command, options, local=False) : +def rsync_wrapper (options, command, local=False) : """ - Wrap given rsync command. + Wrap given rsync command, parsing options/path, determining source, and running rsync in the source. Parses the command, the source path, and then executes rsync within the source path (which may be a special pseudo-path with additional handling). @@ -120,8 +127,13 @@ try : # parse the source path as given by the client, may be a real path or pseudo-path source = rsync.parse_source(path, - restrict_path = options.restrict_path, - lvm_opts = dict(size=options.snapshot_size, wait=options.snapshot_wait, retry=options.snapshot_retry), + restrict_paths = options.restrict_path, + allow_remote = options.allow_remote, + lvm_opts = dict( + size = options.snapshot_size, + wait = options.snapshot_wait, + retry = options.snapshot_retry, + ), ) except RSyncCommandFormatError, e: @@ -135,14 +147,13 @@ # execute try : - # run rsync within the source (may perform additional stuff like snapshotting...) + # run rsync within the source (may perform additional stuff like snapshot...) source.execute(rsync_options, srcdst) except InvokeError, e: log.error("%s failed: %d", e.cmd, e.exit) return e.exit - # ok return 0 @@ -182,7 +193,7 @@ # run try : - return rsync_wrapper(command_parts, options, local) + return rsync_wrapper(options, command_parts, local=local) except Exception, e: log.error("Internal error:", exc_info=e) diff -r f96289ad970c -r 91153d8b499c pvl/backup/rsync.py --- a/pvl/backup/rsync.py Tue Jun 19 11:35:08 2012 +0300 +++ b/pvl/backup/rsync.py Sat Feb 16 21:21:19 2013 +0200 @@ -68,7 +68,7 @@ src = src or path dst = dst or path - log.info("rsync %ss %s %s", ' '.join(options), src, dst) + log.info("rsync %s %s %s", ' '.join(options), src, dst) try : # invoke directly, no option-handling, nor stdin/out redirection @@ -187,8 +187,9 @@ Returns: (cmd, options, path, (source, dest)) - - path -> the real source path + + options -> list of -options + path -> real source path (source, dest) -> combination of None for path, and the real source/dest """ @@ -212,6 +213,8 @@ elif dest is None : dest = part + log.debug("%s: %s", cmd, options) + # options have_server = ('--server' in options) have_sender = ('--sender' in options) @@ -261,10 +264,12 @@ # ok return cmd, options, path, (source, dest) -def parse_source (path, restrict_path=False, lvm_opts={}) : +def parse_source (path, restrict_paths=None, allow_remote=True, lvm_opts={}) : """ Figure out source to rsync from, based on pseudo-path given in rsync command. - + + restrict_paths - raise RsyncCommandFormatError if source path is not under any of the given sources. + allow_remote - allow remote backups? lvm_opts - dict of **opts for RSyncLVMServer """ @@ -279,13 +284,17 @@ path += '/' # verify path - if restrict_path : - if not path.startswith(restrict_path) : - raise RSyncCommandFormatError("Restricted path ({restrict})".format(restrict=restrict_path)) + if restrict_paths : + for restrict_path in restrict_paths : + if path.startswith(restrict_path) : + # ok + break + else : + # fail + raise RSyncCommandFormatError("Restricted path".format()) if path.startswith('/') : # direct filesystem path - # XXX: how to handle= log.debug("filesystem: %s", path) return RSyncFSServer(path) @@ -318,7 +327,7 @@ return RSyncLVMServer(volume, **lvm_opts) - elif ':' in path : + elif ':' in path and allow_remote : host, path = path.split(':', 1) # remote host