--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/backup/rsync.py Tue Feb 14 18:57:21 2012 +0200
@@ -0,0 +1,200 @@
+"""
+ rsync handling.
+
+ Apologies for the 'RSync' nomenclature
+"""
+
+from pvl.backup.invoke import invoke
+from pvl.backup.lvm import LVM, LVMVolume, LVMSnapshot
+from pvl.backup.mount import mount
+
+import shlex
+import os.path
+
+import logging
+
+log = logging.getLogger('pvl.backup.rsync')
+
+
+class RSyncCommandFormatError (Exception) :
+ """
+ Improper rsync command
+ """
+
+ pass
+
+class RSyncSource (object) :
+ RSYNC = '/usr/bin/rsync'
+
+ def _execute (self, options, path) :
+ """
+ Underlying rsync just reads from filesystem.
+ """
+
+ invoke(self.RSYNC, options + [path, '.'], data=False)
+
+class RSyncFSSource (RSyncSource) :
+ """
+ Normal filesystem backup.
+ """
+
+ def __init__ (self, path) :
+ RSyncSource.__init__(self)
+
+ self.path = path
+
+ def execute (self, options) :
+ return self._execute(options, self.path)
+
+class RSyncLVMSource (RSyncSource) :
+ """
+ Backup LVM LV by snapshotting + mounting it.
+ """
+
+ def __init__ (self, volume) :
+ RSyncSource.__init__(self)
+
+ self.volume = volume
+
+ def execute (self, options) :
+ """
+ Snapshot, mount, execute
+ """
+
+ # backup target from LVM command
+ lvm = self.volume.lvm
+ volume = self.volume
+
+ # XXX: generate
+ path = '/mnt'
+
+ # snapshot
+ log.info("Open snapshot...")
+
+ # XXX: generate snapshot nametag to be unique?
+ with lvm.snapshot(volume, tag='backup') as snapshot:
+ log.info("Snapshot opened: %s", snapshot.lvm_path)
+
+ # mount
+ log.info("Mounting snapshot: %s -> %s", snapshot, path)
+
+ with mount(snapshot.dev_path, path) as mountpoint:
+ log.info("Mounted snapshot: %s", mountpoint)
+
+ # rsync!
+ log.info("Running rsync: ...")
+
+ return self._execute(options, mountpoint.path)
+
+ # cleanup
+ # cleanup
+
+def parse_command (command, restrict_server=True, restrict_readonly=True) :
+ """
+ Parse given rsync server command into bits.
+
+ command - the command-string sent by rsync
+ restrict_server - restrict to server-mode
+ restrict_readonly - restrict to read/send-mode
+
+ Returns:
+
+ (cmd, options, source, dest)
+ """
+
+ # split
+ parts = shlex.split(command)
+
+ cmd = None
+ options = []
+ source = None
+ dest = None
+
+ # parse
+ for part in parts :
+ if cmd is None :
+ cmd = part
+
+ elif part.startswith('-') :
+ options.append(part)
+
+ elif source is None :
+ source = part
+
+ elif dest is None :
+ dest = part
+
+ # options
+ have_server = ('--server' in options)
+ have_sender = ('--sender' in options)
+
+ # verify
+ if not have_server :
+ raise RSyncCommandFormatError("Missing --server")
+
+ if restrict_readonly and not have_sender :
+ raise RSyncCommandFormatError("Missing --sender for readonly")
+
+ # parse path
+ if have_sender :
+ # read
+ # XXX: which way does the dot go?
+ if source != '.' :
+ raise RSyncCommandFormatError("Invalid dest for sender")
+
+ path = dest
+
+ else :
+ # write
+ if source != '.' :
+ raise RSyncCommandFormatError("Invalid source for reciever")
+
+ path = dest
+
+ # ok
+ return cmd, options, source, dest
+
+
+def parse_source (path, restrict_path=False) :
+ """
+ Figure out source to rsync from, based on pseudo-path given in rsync command.
+ """
+
+ # normalize
+ path = os.path.normpath(path)
+
+ # verify path
+ if restrict_path :
+ if not path.startswith(restrict_path) :
+ raise RSyncCommandFormatError("Restricted path ({restrict})".format(restrict=restrict_path))
+
+ if path.startswith('/') :
+ # direct filesystem path
+ # XXX: how to handle=
+ log.info("filesystem: %s", path)
+
+ return RSyncFSSource(path)
+
+ elif path.startswith('lvm:') :
+ # LVM LV
+ try :
+ lvm, vg, lv = path.split(':')
+
+ except ValueError, e:
+ raise RSyncCommandFormatError("Invalid lvm pseudo-path: {error}".format(error=e))
+
+ # XXX: validate
+
+ log.info("LVM: %s/%s", vg, lv)
+
+ # open
+ lvm = LVM(vg)
+ volume = lvm.volume(lv)
+
+ return RSyncLVMSource(volume)
+
+ else :
+ # invalid
+ raise RSyncCommandFormatError("Unrecognized backup path")
+
+