pvl/backup/lvm.py
author Tero Marttila <terom@paivola.fi>
Fri, 02 Mar 2012 15:40:21 +0200
changeset 29 5abd153d78eb
parent 10 724a90bd77b5
child 42 43e27a3e9efe
permissions -rw-r--r--
lvm: try and workaround an umount -> lvremove udev timing bug with a time.sleep(1)
"""
    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:
            # 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?
            #       https://bugzilla.redhat.com/show_bug.cgi?id=577798
            #       some udev event bug, possibly fixed in lvm2 2.02.86?
            # try to just patiently wait for it to settle down... if this isn't enough, we need some dmremove magic
            log.debug("cleanup: waiting for snapshot volume to settle...")
            import time
            time.sleep(1)

            # cleanup
            log.debug("cleanup: {0}".format(snapshot))
            snapshot.close()

    def __str__ (self) :
        return self.name

    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 __str__ (self) :
        return self.lvm_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),
        )