--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Tue Feb 14 18:57:21 2012 +0200
@@ -0,0 +1,6 @@
+syntax: regexp
+
+\.sw[op]$
+\.pyc$
+
+^misc/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/__init__.py Tue Feb 14 18:57:21 2012 +0200
@@ -0,0 +1,7 @@
+# Namespace package
+# http://docs.python.org/library/pkgutil.html#pkgutil.extend_path
+
+from pkgutil import extend_path
+
+# magic to support further pvl.foo packages
+__path__ == extend_path(__path__, __name__)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/backup/__init__.py Tue Feb 14 18:57:21 2012 +0200
@@ -0,0 +1,5 @@
+"""
+ rsync-based backups.
+
+ Supports LVM w/ snapshots.
+"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/backup/invoke.py Tue Feb 14 18:57:21 2012 +0200
@@ -0,0 +1,74 @@
+"""
+ Invoke external commands.
+"""
+
+import subprocess
+import logging
+
+log = logging.getLogger('pvl.backup.invoke')
+
+class InvokeError (Exception) :
+ def __init__ (self, cmd, exit) :
+ self.cmd = cmd
+ self.exit = exit
+
+ def __str__ (self) :
+ return "{cmd} failed: {exit}".format(cmd=self.cmd, exit=self.exit)
+
+def invoke (cmd, args, data=None) :
+ """
+ Invoke a command directly.
+
+ data: data to pass in on stdin, returning stdout.
+ if given as False, passes through our process stdin/out
+
+ Doesn't give any data on stdin, and keeps process stderr.
+ Returns stdout.
+ """
+
+ log.debug("cmd={cmd}, args={args}".format(cmd=cmd, args=args))
+
+ if data is False :
+ # keep process stdin/out
+ io = None
+ else :
+ io = subprocess.PIPE
+
+ p = subprocess.Popen([cmd] + args, stdin=io, stdout=io)
+
+ # get output
+ stdout, stderr = p.communicate(input=data)
+
+ if p.returncode :
+ # failed
+ raise InvokeError(cmd, p.returncode)
+
+ return stdout
+
+def optargs (*args, **kwargs) :
+ """
+ Convert args/options into command-line format
+ """
+
+ # process
+ opts = [('--{opt}'.format(opt=opt), value if value != True else None) for opt, value in kwargs.iteritems() if value]
+
+ # flatten
+ opts = [str(opt_part) for opt_parts in opts for opt_part in opt_parts if opt_part]
+
+ args = [str(arg) for arg in args if arg]
+
+ return opts + args
+
+def command (cmd, *args, **opts) :
+ """
+ Invoke a command with options/arguments, given via Python arguments/keyword arguments.
+
+ Return stdout.
+ """
+
+ log.debug("{cmd} {opts} {args}".format(cmd=cmd, args=args, opts=opts))
+
+ # invoke
+ return invoke(cmd, optargs(*args, **opts))
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/backup/lvm.py Tue Feb 14 18:57:21 2012 +0200
@@ -0,0 +1,226 @@
+"""
+ Simple /sbin/lvm wrapper for handling snapshots.
+"""
+
+from pvl.backup.invoke import invoke, optargs, InvokeError
+
+import contextlib
+import os.path
+import logging
+
+log = logging.getLogger('pvl.backup.lvm')
+
+class LVMError (Exception) :
+ pass
+
+class LVM (object) :
+ """
+ LVM VolumeGroup
+ """
+
+ # path to lvm2 binary
+ LVM = '/sbin/lvm'
+
+
+ # VG name
+ name = None
+
+ def __init__ (self, name) :
+ self.name = name
+
+ def lv_name (self, lv) :
+ """
+ vg/lv name.
+ """
+
+ return '{vg}/{lv}'.format(vg=self.name, lv=lv)
+
+ def lv_path (self, lv) :
+ """
+ /dev/vg/lv path.
+ """
+
+ return '/dev/{vg}/{lv}'.format(vg=self.name, lv=lv)
+
+ def command (self, cmd, *args, **opts) :
+ """
+ Invoke a command with options/arguments, given via Python arguments/keyword arguments
+ """
+
+ log.debug("{cmd} {opts} {args}".format(cmd=cmd, args=args, opts=opts))
+
+ # invoke
+ invoke(self.LVM, [cmd] + optargs(*args, **opts))
+
+ def volume (self, name) :
+ """
+ Return an LVMVolume for given named LV.
+ """
+
+ return LVMVolume(self, name)
+
+ @contextlib.contextmanager
+ def snapshot (self, base, **kwargs) :
+ """
+ A Context Manager for handling an LVMSnapshot.
+
+ See LVMSnapshot.create()
+
+ with lvm.snapshot(lv) as snapshot : ...
+ """
+
+ log.debug("creating snapshot from {base}: {opts}".format(base=base, opts=kwargs))
+ snapshot = LVMSnapshot.create(self, base, **kwargs)
+
+ try :
+ log.debug("got: {0}".format(snapshot))
+ yield snapshot
+
+ finally:
+ # cleanup
+ # XXX: there's some timing bug with an umount leaving the LV open, do we need to wait for it to get closed after mount?
+ log.debug("cleanup: {0}".format(snapshot))
+ snapshot.close()
+
+ def __repr__ (self) :
+ return "LVM(name={name})".format(name=repr(self.name))
+
+class LVMVolume (object) :
+ """
+ LVM Logical Volume.
+ """
+
+ # VG
+ lvm = None
+
+ # name
+ name = None
+
+ def __init__ (self, lvm, name) :
+ self.lvm = lvm
+ self.name = name
+
+ @property
+ def lvm_path (self) :
+ return self.lvm.lv_name(self.name)
+
+ @property
+ def dev_path (self) :
+ return self.lvm.lv_path(self.name)
+
+ def verify_exists (self) :
+ """
+ Verify that the LV exists.
+
+ Raises an LVMError otherwise.
+ """
+
+ # lvdisplay
+ try :
+ self.lvm.command('lvs', self.lvm_path)
+
+ except InvokeError :
+ raise LVMError("Unable to lvdisplay LV: {path}".format(path=self.lvm_path))
+
+ # dev
+ if not self.test_dev() :
+ raise LVMError("LV dev does not exist: {path}".format(path=self.dev_path))
+
+ def verify_missing (self) :
+ """
+ Verify that the LV does NOT exist.
+
+ Raises an LVMError otherwise.
+ """
+
+ if self.test_dev() :
+ raise Exception("LV already exists: {path}".format(path=self.dev_path))
+
+ def test_dev (self) :
+ """
+ Tests for existance of device file, returning True/False.
+ """
+
+ return os.path.exists(self.dev_path)
+
+ def __repr__ (self) :
+ return "LVMVolume(lvm={lvm}, name={name})".format(
+ lvm = repr(self.lvm),
+ name = repr(self.name),
+ )
+
+class LVMSnapshot (LVMVolume) :
+ """
+ LVM snapshot
+ """
+
+ # default snapshot size
+ LVM_SNAPSHOT_SIZE = '5G'
+
+ # base lv
+ base = None
+
+ @classmethod
+ def create (cls, lvm, base, tag, size=LVM_SNAPSHOT_SIZE) :
+ """
+ Create a new LVM snapshot of the given LV.
+
+ Returns a (snapshot_name, dev_path) tuple.
+ """
+
+ # snapshot name
+ name = '{name}-{tag}'.format(name=base.name, tag=tag)
+
+ # snapshot instance
+ snapshot = cls(lvm, base, name)
+
+ ## verify
+ # base should exist
+ base.verify_exists()
+
+ # snapshot should not
+ snapshot.verify_missing()
+
+ ## create
+ snapshot.open()
+
+ # verify
+ if not snapshot.test_dev() :
+ raise LVMError("Failed to find new snapshot LV device: {path}".format(path=snapshot.dev_path))
+
+ # yay
+ return snapshot
+
+ def __init__ (self, lvm, base, name, size=LVM_SNAPSHOT_SIZE) :
+ LVMVolume.__init__(self, lvm, name)
+
+ self.base = base
+ self.size = size
+
+ def open (self) :
+ """
+ Create snapshot volume.
+ """
+
+ # create
+ self.lvm.command('lvcreate', self.base.lvm_path, snapshot=True, name=self.name, size=self.size)
+
+ def close (self) :
+ """
+ Remove snapshot volume.
+ """
+
+ # XXX: can't deactivate snapshot volume
+ #self.lvm.command('lvchange', name, available='n')
+
+ # XXX: risky!
+ self.lvm.command('lvremove', '-f', self.lvm_path)
+
+ def __repr__ (self) :
+ return "LVMSnapshot(lvm={lvm}, base={base}, name={name})".format(
+ lvm = str(self.lvm),
+ base = str(self.base),
+ name = repr(self.name),
+ )
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/backup/mount.py Tue Feb 14 18:57:21 2012 +0200
@@ -0,0 +1,106 @@
+"""
+ Mount filesystems.
+"""
+
+from pvl.backup.invoke import command
+
+import contextlib
+import os.path
+import logging
+
+log = logging.getLogger('pvl.backup.mount')
+
+
+class MountError (Exception) :
+ pass
+
+class Mount (object) :
+ """
+ Trivial filesystem mounting
+ """
+
+ MOUNT = '/bin/mount'
+ UMOUNT = '/bin/umount'
+
+
+ def __init__ (self, dev, mnt, readonly=False) :
+ """
+ dev - device path
+ mnt - mount path
+ readonly - mount readonly
+ """
+
+ self.dev = dev
+ self.mnt = mnt
+ self.readonly = readonly
+
+ @property
+ def path (self) :
+ return self.mnt
+
+ def options (self) :
+ """
+ Mount options as a comma-separated string.
+ """
+
+ options = [
+ ('ro' if self.readonly else None),
+ ]
+
+ return ','.join(option for option in options if option)
+
+ def open (self) :
+ """
+ Mount
+ """
+
+ # check
+ if not os.path.isdir(self.mnt) :
+ raise MountError("Mountpoint is not a directory: {mnt}".format(mnt=self.mnt))
+
+ if os.path.ismount(self.mnt) :
+ raise MountError("Mountpoint is already mounted: {mnt}".format(mnt=self.mnt))
+
+ if not os.path.exists(self.dev) :
+ raise MountError("Device does not exist: {dev}".format(dev=self.dev))
+
+ # mount
+ command(self.MOUNT, self.dev, self.mnt, options=self.options())
+
+ def close (self) :
+ """
+ Un-mount
+ """
+
+ # check
+ if not os.path.ismount(self.mnt):
+ raise MountError("Mountpoint is not mounted: {mnt}".format(mnt=self.mnt))
+
+ # umount
+ command(self.UMOUNT, self.mnt)
+
+@contextlib.contextmanager
+def mount (dev, mnt, **kwargs) :
+ """
+ Use a temporary mount:
+
+ with mount('/dev/...', '/mnt', readonly=True) as mount:
+ ...
+ """
+
+ mount = Mount(dev, mnt, **kwargs)
+
+ # open
+ log.debug("open: %s", mount)
+ mount.open()
+
+ try :
+ log.debug("got: %s", mount)
+ yield mount
+
+ finally:
+ # cleanup
+ log.debug("cleanup: %s", mount)
+ mount.close()
+
+
--- /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")
+
+
--- a/rsync-lvm-server.py Tue Feb 14 18:19:55 2012 +0200
+++ b/rsync-lvm-server.py Tue Feb 14 18:57:21 2012 +0200
@@ -1,530 +1,15 @@
#!/usr/bin/python
-import optparse, shlex
+from pvl.backup.rsync import RSyncCommandFormatError
+from pvl.backup.invoke import InvokeError
+from pvl.backup import rsync
-import subprocess
-import os, os.path
-
-import contextlib
+import optparse
+import os
import logging
log = logging.getLogger()
-class InvokeError (Exception) :
- def __init__ (self, cmd, exit) :
- self.cmd = cmd
- self.exit = exit
-
- def __str__ (self) :
- return "{cmd} failed: {exit}".format(cmd=self.cmd, exit=self.exit)
-
-def invoke (cmd, args, data=None) :
- """
- Invoke a command directly.
-
- data: data to pass in on stdin, returning stdout.
- if given as False, passes through our process stdin/out
-
- Doesn't give any data on stdin, and keeps process stderr.
- Returns stdout.
- """
-
- log.debug("cmd={cmd}, args={args}".format(cmd=cmd, args=args))
-
- if data is False :
- # keep process stdin/out
- io = None
- else :
- io = subprocess.PIPE
-
- p = subprocess.Popen([cmd] + args, stdin=io, stdout=io)
-
- # get output
- stdout, stderr = p.communicate(input=data)
-
- if p.returncode :
- # failed
- raise InvokeError(cmd, p.returncode)
-
- return stdout
-
-def optargs (*args, **kwargs) :
- """
- Convert args/options into command-line format
- """
-
- # process
- opts = [('--{opt}'.format(opt=opt), value if value != True else None) for opt, value in kwargs.iteritems() if value]
-
- # flatten
- opts = [str(opt_part) for opt_parts in opts for opt_part in opt_parts if opt_part]
-
- args = [str(arg) for arg in args if arg]
-
- return opts + args
-
-def command (cmd, *args, **opts) :
- """
- Invoke a command with options/arguments, given via Python arguments/keyword arguments.
-
- Return stdout.
- """
-
- log.debug("{cmd} {opts} {args}".format(cmd=cmd, args=args, opts=opts))
-
- # invoke
- return invoke(cmd, optargs(*args, **opts))
-
-class LVM (object) :
- """
- LVM VolumeGroup
- """
-
- # path to lvm2 binary
- LVM = '/sbin/lvm'
-
-
- # VG name
- name = None
-
- def __init__ (self, name) :
- self.name = name
-
- def lv_name (self, lv) :
- """
- vg/lv name.
- """
-
- return '{vg}/{lv}'.format(vg=self.name, lv=lv)
-
- def lv_path (self, lv) :
- """
- /dev/vg/lv path.
- """
-
- return '/dev/{vg}/{lv}'.format(vg=self.name, lv=lv)
-
- def command (self, cmd, *args, **opts) :
- """
- Invoke a command with options/arguments, given via Python arguments/keyword arguments
- """
-
- log.debug("{cmd} {opts} {args}".format(cmd=cmd, args=args, opts=opts))
-
- # invoke
- invoke(self.LVM, [cmd] + optargs(*args, **opts))
-
- def volume (self, name) :
- """
- Return an LVMVolume for given named LV.
- """
-
- return LVMVolume(self, name)
-
- @contextlib.contextmanager
- def snapshot (self, base, **kwargs) :
- """
- A Context Manager for handling an LVMSnapshot.
-
- See LVMSnapshot.create()
-
- with lvm.snapshot(lv) as snapshot : ...
- """
-
- log.debug("creating snapshot from {base}: {opts}".format(base=base, opts=kwargs))
- snapshot = LVMSnapshot.create(self, base, **kwargs)
-
- try :
- log.debug("got: {0}".format(snapshot))
- yield snapshot
-
- finally:
- # cleanup
- # XXX: do we need to wait for it to get closed after mount?
- log.debug("cleanup: {0}".format(snapshot))
- snapshot.close()
-
- def __repr__ (self) :
- return "LVM(name={name})".format(name=repr(self.name))
-
-class LVMVolume (object) :
- """
- LVM Logical Volume.
- """
-
- # VG
- lvm = None
-
- # name
- name = None
-
- def __init__ (self, lvm, name) :
- self.lvm = lvm
- self.name = name
-
- @property
- def lvm_path (self) :
- return self.lvm.lv_name(self.name)
-
- @property
- def dev_path (self) :
- return self.lvm.lv_path(self.name)
-
- def __repr__ (self) :
- return "LVMVolume(lvm={lvm}, name={name})".format(
- lvm = repr(self.lvm),
- name = repr(self.name),
- )
-
-class LVMSnapshot (LVMVolume) :
- """
- LVM snapshot
- """
-
- # default snapshot size
- LVM_SNAPSHOT_SIZE = '5G'
-
- # base lv
- base = None
-
- @classmethod
- def create (cls, lvm, base, tag, size=LVM_SNAPSHOT_SIZE) :
- """
- Create a new LVM snapshot of the given LV.
-
- Returns a (snapshot_name, dev_path) tuple.
- """
-
- # snapshot name
- name = '{name}-{tag}'.format(name=base.name, tag=tag)
-
- # snapshot
- snapshot = cls(lvm, base, name)
-
- # verify LV exists
- lvm.command('lvs', base.lvm_path)
-
- if not os.path.exists(base.dev_path) :
- raise Exception("lvm_snapshot: source LV does not exist: {path}".format(path=base.dev_path))
-
- if os.path.exists(snapshot.dev_path) :
- raise Exception("lvm_snapshot: target LV snapshot already exists: {path}".format(path=snapshot.dev_path))
-
- # create
- snapshot.open()
-
- # verify
- if not os.path.exists(snapshot.dev_path) :
- raise Exception("lvm_snapshot: target LV snapshot did not appear: {path}".format(path=snapshot.dev_path))
-
- # yay
- return snapshot
-
- def __init__ (self, lvm, base, name, size=LVM_SNAPSHOT_SIZE) :
- LVMVolume.__init__(self, lvm, name)
-
- self.base = base
- self.size = size
-
- def open (self) :
- """
- Create snapshot volume.
- """
-
- # create
- self.lvm.command('lvcreate', self.base.lvm_path, snapshot=True, name=self.name, size=self.size)
-
- def close (self) :
- """
- Remove snapshot volume.
- """
-
- # XXX: can't deactivate snapshot volume
- #self.lvm.command('lvchange', name, available='n')
-
- # XXX: risky!
- self.lvm.command('lvremove', '-f', self.lvm_path)
-
- def __repr__ (self) :
- return "LVMSnapshot(lvm={lvm}, base={base}, name={name})".format(
- lvm = str(self.lvm),
- base = str(self.base),
- name = repr(self.name),
- )
-
-
-class MountError (Exception) :
- pass
-
-class Mount (object) :
- """
- Trivial filesystem mounting
- """
-
- MOUNT = '/bin/mount'
- UMOUNT = '/bin/umount'
-
-
- def __init__ (self, dev, mnt, readonly=False) :
- """
- dev - device path
- mnt - mount path
- readonly - mount readonly
- """
-
- self.dev = dev
- self.mnt = mnt
- self.readonly = readonly
-
- @property
- def path (self) :
- return self.mnt
-
- def options (self) :
- """
- Mount options as a comma-separated string.
- """
-
- options = [
- ('ro' if self.readonly else None),
- ]
-
- return ','.join(option for option in options if option)
-
- def open (self) :
- """
- Mount
- """
-
- # check
- if not os.path.isdir(self.mnt) :
- raise MountError("Mountpoint is not a directory: {mnt}".format(mnt=self.mnt))
-
- if os.path.ismount(self.mnt) :
- raise MountError("Mountpoint is already mounted: {mnt}".format(mnt=self.mnt))
-
- if not os.path.exists(self.dev) :
- raise MountError("Device does not exist: {dev}".format(dev=self.dev))
-
- # mount
- command(self.MOUNT, self.dev, self.mnt, options=self.options())
-
- def close (self) :
- """
- Un-mount
- """
-
- # check
- if not os.path.ismount(self.mnt):
- raise MountError("Mountpoint is not mounted: {mnt}".format(mnt=self.mnt))
-
- # umount
- command(self.UMOUNT, self.mnt)
-
-@contextlib.contextmanager
-def mount (dev, mnt, **kwargs) :
- """
- Use a temporary mount:
-
- with mount('/dev/...', '/mnt', readonly=True) as mount:
- ...
- """
-
- mount = Mount(dev, mnt, **kwargs)
-
- # open
- log.debug("open: %s", mount)
- mount.open()
-
- try :
- log.debug("got: %s", mount)
- yield mount
-
- finally:
- # cleanup
- log.debug("cleanup: %s", mount)
- mount.close()
-
-class RSyncCommandFormatError (Exception) :
- """
- Improper rsync command
- """
-
- pass
-
-def parse_rsync (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
-
-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 rsync_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")
-
# command-line options
options = None
@@ -567,7 +52,6 @@
return options, args
-
def rsync_wrapper (command, restrict='lvm:') :
"""
Wrap given rsync command.
@@ -577,7 +61,7 @@
try :
# parse
- rsync_cmd, rsync_options, source_path, dest_path = parse_rsync(command,
+ rsync_cmd, rsync_options, source_path, dest_path = rsync.parse_command(command,
restrict_readonly = options.readonly,
)
@@ -590,7 +74,7 @@
try :
# parse source
- source = rsync_source(path,
+ source = rsync.parse_source(path,
restrict_path = options.restrict_path,
)