diff -r d5fd342015d3 -r 9211cb493130 rsync-lvm-server.py --- a/rsync-lvm-server.py Tue Feb 14 14:00:49 2012 +0200 +++ b/rsync-lvm-server.py Tue Feb 14 14:23:16 2012 +0200 @@ -12,135 +12,206 @@ ) log = logging.getLogger() -LVM = '/sbin/lvm' LVM_VG = 'asdf' -LVM_SNAPSHOT_SIZE = '5G' - -def lvm_name (vg, lv) : - """ - LVM vg/lv name. - """ - return '{vg}/{lv}'.format(vg=vg, lv=lv) - -def lvm_path (vg, lv) : - """ - Map LVM VG+LV to /dev path. - """ - - return '/dev/{vg}/{lv}'.format(vg=vg, lv=lv) - -def lvm_invoke (cmd, args) : +class LVM (object) : """ - Invoke LVM command directly. - - Doesn't give any data on stdin, and keeps process stderr. - Returns stdout. - """ - - log.debug("cmd={cmd}, args={args}".format(cmd=cmd, args=args)) - - p = subprocess.Popen([LVM, cmd] + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - - # get output - stdout, stderr = p.communicate(input=None) - - if p.returncode : - raise Exception("LVM ({cmd}) failed: {returncode}".format(cmd=cmd, returncode=p.returncode)) - - return stdout - -def lvm (cmd, *args, **opts) : - """ - Invoke simple LVM command with options/arguments, and no output. + LVM VolumeGroup """ - log.debug("cmd={cmd}, opts={opts}, args={args}".format(cmd=cmd, args=args, opts=opts)) - - # process - opts = [('--{opt}'.format(opt=opt), value if value != True else None) for opt, value in opts.iteritems() if value] - - # flatten - opts = [str(opt_part) for opt_parts in opts for opt_part in opt_parts if opt_part] + # path to lvm2 binary + LVM = '/sbin/lvm' - args = [str(arg) for arg in args if arg] - - # invoke - lvm_invoke(cmd, opts + args) + + # VG name + name = None -def lvm_snapshot_create (vg, lv, size=LVM_SNAPSHOT_SIZE) : + 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 invoke (self, cmd, args) : + """ + Invoke LVM command directly. + + Doesn't give any data on stdin, and keeps process stderr. + Returns stdout. + """ + + log.debug("cmd={cmd}, args={args}".format(cmd=cmd, args=args)) + + p = subprocess.Popen([self.LVM, cmd] + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + + # get output + stdout, stderr = p.communicate(input=None) + + if p.returncode : + raise Exception("LVM ({cmd}) failed: {returncode}".format(cmd=cmd, returncode=p.returncode)) + + return stdout + + def command (self, cmd, *args, **opts) : + """ + Invoke simple LVM command with options/arguments, and no output. + """ + + log.debug("cmd={cmd}, opts={opts}, args={args}".format(cmd=cmd, args=args, opts=opts)) + + # process + opts = [('--{opt}'.format(opt=opt), value if value != True else None) for opt, value in opts.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] + + # invoke + self.invoke(cmd, opts + args) + + 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) + + log.debug("got snapshot={0}".format(snapshot)) + yield snapshot + + log.debug("cleanup: {0}".format(snapshot)) + snapshot.close() + +class LVMVolume (object) : """ - Create a new LVM snapshot of the given LV. - - Returns a (snapshot_name, dev_path) tuple. + LVM Logical Volume. """ - # path to device - lv_name = lvm_name(vg, lv) - lv_path = lvm_path(vg, lv) - - # snapshot name - snap_lv = '{lv}-backup'.format(lv=lv) - snap_name = lvm_name(vg, snap_lv) - snap_path = lvm_path(vg, snap_lv) - - # verify LV exists - lvm('lvs', lv_name) - - if not os.path.exists(lv_path) : - raise Exception("lvm_snapshot: source LV does not exist: {path}".format(path=lv_path)) - - if os.path.exists(snap_path) : - raise Exception("lvm_snapshot: target LV snapshot already exists: {path}".format(path=snap_path)) - - # create - lvm('lvcreate', lv_name, snapshot=True, name=snap_lv, size=size) - - # verify - if not os.path.exists(snap_path) : - raise Exception("lvm_snapshot: target LV snapshot did not appear: {path}".format(path=snap_path)) + # VG + lvm = None - # yay - return snap_name, snap_path - -def lvm_snapshot_remove (name) : - """ - Remove given snapshot volume. - """ - - # XXX: can't deactivate snapshot volume - #lvm('lvchange', name, available='n') - - # XXX: risky! - lvm('lvremove', '-f', name) + # name + name = None -@contextlib.contextmanager -def lvm_snapshot (*args, **kwargs) : - """ - A Context Manager for handling an LVM snapshot. - - with lvm_snapshot(vg, lv) as (snapshot_name, snapshot_path) : ... - """ + def __init__ (self, lvm, name) : + self.lvm = lvm + self.name = name - log.debug("creating snapshot: {0}".format(args)) - name, path = lvm_snapshot_create(*args, **kwargs) + @property + def lvm_path (self) : + return self.lvm.lv_name(self.name) - log.debug("got name={0}, path={1}".format(name, path)) - yield name, path + @property + def dev_path (self) : + return self.lvm.lv_path(self.name) - log.debug("cleanup: {0}".format(name)) - lvm_snapshot_remove(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 main (argv) : + # LVM VolumeGroup to manipulate + lvm = LVM('asdf') + # XXX: get LV from rsync command - lvm_vg='asdf' - backup_lv='test' + backup_lv = lvm.volume('test') # snapshot log.info("Open snapshot...") - with lvm_snapshot(lvm_vg, backup_lv) as (snapshot_name, snapshot_path): - log.info("Snapshot opened: {name}".format(name=snapshot_name)) + with lvm.snapshot(backup_lv, tag='backup') as snapshot: + log.info("Snapshot opened: {name}".format(name=snapshot.lvm_path)) # ...