tweak pvl.backup-rsync: multiple --restrict-paths, explicit --allow-remote, minor bugfixes
authorTero Marttila <terom@paivola.fi>
Sat, 16 Feb 2013 21:21:19 +0200
changeset 67 91153d8b499c
parent 66 f96289ad970c
child 68 adc190def3b1
tweak pvl.backup-rsync: multiple --restrict-paths, explicit --allow-remote, minor bugfixes
bin/pvl.backup-rsync
pvl/backup/rsync.py
--- 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