tweak pvl.backup-rsync: multiple --restrict-paths, explicit --allow-remote, minor bugfixes
--- 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)
--- 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