--- 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))
# ...