terom@5: """ terom@5: Simple /sbin/lvm wrapper for handling snapshots. terom@5: """ terom@5: terom@5: from pvl.backup.invoke import invoke, optargs, InvokeError terom@5: terom@5: import contextlib terom@5: import os.path terom@5: import logging terom@42: import time terom@5: terom@5: log = logging.getLogger('pvl.backup.lvm') terom@5: terom@42: # default snapshot size terom@42: LVM_SNAPSHOT_SIZE = '5G' terom@42: terom@42: # number of seconds to wait for lvm snapshot to settle after unmount.. terom@42: LVM_SNAPSHOT_WAIT = 5 terom@42: terom@61: # number of times to retry removal, due to lvm/udev bug.. terom@61: LVM_SNAPSHOT_RETRY = 5 terom@61: terom@5: class LVMError (Exception) : terom@5: pass terom@5: terom@5: class LVM (object) : terom@5: """ terom@5: LVM VolumeGroup terom@5: """ terom@5: terom@5: # path to lvm2 binary terom@5: LVM = '/sbin/lvm' terom@5: terom@5: terom@5: # VG name terom@5: name = None terom@5: terom@69: def __init__ (self, name, sudo=None) : terom@69: """ terom@69: name - VG name terom@69: sudo - invoke sudo terom@69: """ terom@69: terom@5: self.name = name terom@69: self.sudo = sudo terom@5: terom@5: def lv_name (self, lv) : terom@5: """ terom@5: vg/lv name. terom@5: """ terom@5: terom@5: return '{vg}/{lv}'.format(vg=self.name, lv=lv) terom@5: terom@5: def lv_path (self, lv) : terom@5: """ terom@5: /dev/vg/lv path. terom@5: """ terom@5: terom@5: return '/dev/{vg}/{lv}'.format(vg=self.name, lv=lv) terom@5: terom@5: def command (self, cmd, *args, **opts) : terom@5: """ terom@5: Invoke a command with options/arguments, given via Python arguments/keyword arguments terom@5: """ terom@5: terom@5: log.debug("{cmd} {opts} {args}".format(cmd=cmd, args=args, opts=opts)) terom@5: terom@5: # invoke terom@69: invoke(self.LVM, [cmd] + optargs(*args, **opts), sudo=self.sudo) terom@5: terom@5: def volume (self, name) : terom@5: """ terom@5: Return an LVMVolume for given named LV. terom@5: """ terom@5: terom@5: return LVMVolume(self, name) terom@5: terom@5: @contextlib.contextmanager terom@61: def snapshot (self, base, wait=LVM_SNAPSHOT_WAIT, retry=LVM_SNAPSHOT_RETRY, **opts) : terom@5: """ terom@5: A Context Manager for handling an LVMSnapshot. terom@5: terom@5: See LVMSnapshot.create() terom@5: terom@5: with lvm.snapshot(lv) as snapshot : ... terom@42: terom@42: wait - wait given interval for the snapshot device to settle before unmounting it terom@61: retry - retry removal given number of times terom@42: **opts - LVMSnapshot.create() options (e.g. size) terom@5: """ terom@5: terom@61: log.debug("creating snapshot from {base}: wait={wait}, retry={retry} {opts}".format(base=base, wait=wait, retry=retry, opts=opts)) terom@42: snapshot = LVMSnapshot.create(self, base, **opts) terom@5: terom@5: try : terom@5: log.debug("got: {0}".format(snapshot)) terom@5: yield snapshot terom@5: terom@5: finally: terom@61: # XXX: there's some common udev bug with removing lvm snapshots terom@61: # https://bugzilla.redhat.com/show_bug.cgi?id=577798 terom@61: # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=618016 terom@61: # possibly fixed in lvm2 2.02.86? terom@61: # try to just patiently wait for it to settle down... then retry... if this isn't enough, we need some dmremove magic? terom@61: while True : terom@61: # wait.. terom@61: if wait : terom@61: log.debug("%s: cleanup: waiting %.2f seconds for snapshot volume to settle...", snapshot, wait) terom@61: time.sleep(wait) terom@29: terom@61: # lvremove terom@61: try : terom@61: log.debug("%s: cleanup", snapshot) terom@61: snapshot.close() terom@61: terom@61: except InvokeError as ex : terom@61: if ex.exit != 5 : terom@61: # lvremove sez "Can't remove open logical volume ..." -> exit(5); terom@61: raise terom@61: terom@61: # retry counter? terom@61: if retry : terom@61: log.warn("%s: cleanup: lvremove failed, retrying...", snapshot) terom@61: retry -= 1 terom@61: terom@61: # retry terom@61: continue terom@61: terom@61: else : terom@61: # failed terom@61: log.error("%s: cleanup: lvremove failed, aborting...", snapshot) terom@61: raise terom@61: terom@61: else : terom@61: # done terom@61: break terom@5: terom@10: def __str__ (self) : terom@10: return self.name terom@10: terom@5: def __repr__ (self) : terom@5: return "LVM(name={name})".format(name=repr(self.name)) terom@5: terom@5: class LVMVolume (object) : terom@5: """ terom@5: LVM Logical Volume. terom@5: """ terom@5: terom@5: # VG terom@5: lvm = None terom@5: terom@5: # name terom@5: name = None terom@5: terom@5: def __init__ (self, lvm, name) : terom@5: self.lvm = lvm terom@5: self.name = name terom@5: terom@5: @property terom@5: def lvm_path (self) : terom@5: return self.lvm.lv_name(self.name) terom@5: terom@5: @property terom@5: def dev_path (self) : terom@5: return self.lvm.lv_path(self.name) terom@5: terom@5: def verify_exists (self) : terom@5: """ terom@5: Verify that the LV exists. terom@5: terom@5: Raises an LVMError otherwise. terom@5: """ terom@5: terom@5: # lvdisplay terom@5: try : terom@5: self.lvm.command('lvs', self.lvm_path) terom@5: terom@5: except InvokeError : terom@5: raise LVMError("Unable to lvdisplay LV: {path}".format(path=self.lvm_path)) terom@5: terom@5: # dev terom@5: if not self.test_dev() : terom@5: raise LVMError("LV dev does not exist: {path}".format(path=self.dev_path)) terom@5: terom@5: def verify_missing (self) : terom@5: """ terom@5: Verify that the LV does NOT exist. terom@5: terom@5: Raises an LVMError otherwise. terom@5: """ terom@5: terom@5: if self.test_dev() : terom@5: raise Exception("LV already exists: {path}".format(path=self.dev_path)) terom@5: terom@5: def test_dev (self) : terom@5: """ terom@5: Tests for existance of device file, returning True/False. terom@5: """ terom@5: terom@5: return os.path.exists(self.dev_path) terom@5: terom@10: def __str__ (self) : terom@10: return self.lvm_path terom@10: terom@5: def __repr__ (self) : terom@5: return "LVMVolume(lvm={lvm}, name={name})".format( terom@5: lvm = repr(self.lvm), terom@5: name = repr(self.name), terom@5: ) terom@5: terom@5: class LVMSnapshot (LVMVolume) : terom@5: """ terom@5: LVM snapshot terom@5: """ terom@5: terom@5: # base lv terom@5: base = None terom@5: terom@5: @classmethod terom@5: def create (cls, lvm, base, tag, size=LVM_SNAPSHOT_SIZE) : terom@5: """ terom@5: Create a new LVM snapshot of the given LV. terom@5: terom@5: Returns a (snapshot_name, dev_path) tuple. terom@5: """ terom@5: terom@5: # snapshot name terom@5: name = '{name}-{tag}'.format(name=base.name, tag=tag) terom@5: terom@5: # snapshot instance terom@42: snapshot = cls(lvm, base, name, size=size) terom@5: terom@5: ## verify terom@5: # base should exist terom@5: base.verify_exists() terom@5: terom@5: # snapshot should not terom@5: snapshot.verify_missing() terom@5: terom@5: ## create terom@5: snapshot.open() terom@5: terom@5: # verify terom@5: if not snapshot.test_dev() : terom@5: raise LVMError("Failed to find new snapshot LV device: {path}".format(path=snapshot.dev_path)) terom@5: terom@5: # yay terom@5: return snapshot terom@5: terom@5: def __init__ (self, lvm, base, name, size=LVM_SNAPSHOT_SIZE) : terom@5: LVMVolume.__init__(self, lvm, name) terom@5: terom@5: self.base = base terom@5: self.size = size terom@5: terom@5: def open (self) : terom@5: """ terom@5: Create snapshot volume. terom@5: """ terom@5: terom@5: # create terom@5: self.lvm.command('lvcreate', self.base.lvm_path, snapshot=True, name=self.name, size=self.size) terom@5: terom@5: def close (self) : terom@5: """ terom@5: Remove snapshot volume. terom@5: """ terom@5: terom@61: # don't typo me! terom@5: self.lvm.command('lvremove', '-f', self.lvm_path) terom@5: terom@5: def __repr__ (self) : terom@5: return "LVMSnapshot(lvm={lvm}, base={base}, name={name})".format( terom@5: lvm = str(self.lvm), terom@5: base = str(self.base), terom@5: name = repr(self.name), terom@5: ) terom@5: terom@5: