[yum-commits] docs/yum-fs-snapshot.1 docs/yum-fs-snapshot.conf.5 plugins/fs-snapshot

skvidal at osuosl.org skvidal at osuosl.org
Wed Feb 3 21:06:15 UTC 2010


 docs/yum-fs-snapshot.1               |   10 +
 docs/yum-fs-snapshot.conf.5          |   15 +-
 plugins/fs-snapshot/fs-snapshot.conf |    5 
 plugins/fs-snapshot/fs-snapshot.py   |  243 +++++++++++++++++++++++++++++------
 4 files changed, 231 insertions(+), 42 deletions(-)

New commits:
commit 52b1d860058f3daf8ad507b84dc78beb844864ea
Author: Mike Snitzer <msnitzer at fedoraproject.org>
Date:   Wed Feb 3 16:06:12 2010 -0500

    add lvm support for fs-snapshot plugin

diff --git a/docs/yum-fs-snapshot.1 b/docs/yum-fs-snapshot.1
index 0fc1397..1c3c834 100644
--- a/docs/yum-fs-snapshot.1
+++ b/docs/yum-fs-snapshot.1
@@ -1,5 +1,5 @@
 .\" yum-fs-snapshot
-.TH YUM-FS-SNAPSHOT 1 "14 December 2009" "" "User Manuals"
+.TH YUM-FS-SNAPSHOT 1 "3 February 2010" "" "User Manuals"
 .SH NAME
 .B yum-fs-snapshot
 .SH SYNOPSIS
@@ -10,7 +10,11 @@ package
 .BR yum-fs-snapshot(1)
 is a Yum plugin for taking snapshots of your filesystems before running a yum
 transaction.  By default it will take a snapshot of any filesystem that can be
-snapshotted, which currently is limited to BTRFS filesystems. 
+snapshotted, which currently is limited to BTRFS filesystems.  However,
+all filesystems built on LVM logical volumes may be snapshotted at the
+block level using LVM snapshots.  LVM snapshot support is provided for
+the purpose of system rollback.  As such LVM snapshots will only be
+created if the kernel supports the "snapshot-merge" DM target.
 .SH FILES
 .B yum-fs-snapshot
 uses a configuration file for its specific actions: 
@@ -24,6 +28,8 @@ uses a configuration file for its specific actions:
 .SH AUTHORS
 .nf
 Josef Bacik <josef at toxicpanda.com>
+.br
+Mike Snitzer <msnitzer at fedoraproject.org>
 .fi
 .SH "SEE ALSO"
 .BR yum (1)
diff --git a/docs/yum-fs-snapshot.conf.5 b/docs/yum-fs-snapshot.conf.5
index 9cf49e1..a4b4f02 100644
--- a/docs/yum-fs-snapshot.conf.5
+++ b/docs/yum-fs-snapshot.conf.5
@@ -1,5 +1,5 @@
 .\" yum-fs-snapshot.conf.5
-.TH YUM-FS-SNAPSHOT.CONF 5 "14 December 2009" "" "File Formats"
+.TH YUM-FS-SNAPSHOT.CONF 5 "3 February 2010" "" "File Formats"
 .SH NAME
 .B yum-fs-snapshot.conf(5)
 
@@ -9,7 +9,7 @@ is the configuration file for
 .B yum-fs-snapshot(1)
 Yum plugin for snapshotting your filesystems before running a yum transaction.
 By default, this plugin will snapshot all filesystems that it is capable of
-snapshotting.
+snapshotting.  This includes block-level snapshots using LVM snapshots.
 .SH FILES
 .I /etc/yum/pluginconf.d/fs-snapshot.conf
 .SH FILE FORMAT
@@ -20,9 +20,20 @@ utilizes configuration options in the form of
 .IP exclude
 This is a space delimited list of the mount points you do not wish to have
 snapshotted by this plugin.
+.SH OPTION - [lvm] section
+.IP enabled
+This is a boolean value used to control whether LVM snapshots will be
+created for filesystems built on LVM logical volumes.
+.SH OPTION - [lvm] section
+.IP lvcreate_size_args
+This is the space delimited lvcreate argument list that is used to
+specify the size of the snapshot LV.  Valid lvcreate size options are -l
+or -L.  If not specified then LVM snapshots will not be created.
 .SH AUTHOR
 .RS
 Josef Bacik <josef at toxicpanda.com>
+.br
+Mike Snitzer <msnitzer at fedoraproject.org>
 .RS
 .SH SEE ALSO
 .BR yum-fs-snapshot(1)
diff --git a/plugins/fs-snapshot/fs-snapshot.conf b/plugins/fs-snapshot/fs-snapshot.conf
index e7002aa..3a67c69 100644
--- a/plugins/fs-snapshot/fs-snapshot.conf
+++ b/plugins/fs-snapshot/fs-snapshot.conf
@@ -1,2 +1,7 @@
 [main]
 enabled = 1
+
+[lvm]
+enabled = 0
+# 'lvcreate_size_args' option must specify the snapshot LV size using -L or -l
+#lvcreate_size_args = -l 15%ORIGIN
diff --git a/plugins/fs-snapshot/fs-snapshot.py b/plugins/fs-snapshot/fs-snapshot.py
index b7ecf2f..0a36af4 100644
--- a/plugins/fs-snapshot/fs-snapshot.py
+++ b/plugins/fs-snapshot/fs-snapshot.py
@@ -12,8 +12,9 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 
-# Copyright 2009 Red Hat, Inc
+# Copyright 2009-2010 Red Hat, Inc
 # written by Josef Bacik <josef at toxicpanda.com>
+#            Mike Snitzer <msnitzer at fedoraproject.org>
 
 """
 This plugin creates a snapshot before any yum update or yum remove operation on
@@ -35,83 +36,249 @@ from subprocess import Popen,PIPE
 requires_api_version = '2.4'
 plugin_type = (TYPE_CORE,)
 
-def pretrans_hook(conduit):
+# Globals
+lvm_key = "create_lvm_snapshot"
+# avoid multiple snapshot-merge checks via inspect_volume_lvm()
+dm_snapshot_merge_checked = 0
+dm_snapshot_merge_support = 0
+
+def kernel_supports_dm_snapshot_merge():
+    # verify the kernel provides the 'snapshot-merge' DM target
+    # - modprobe dm-snapshot; dmsetup targets | grep -q snapshot-merge
+    global dm_snapshot_merge_checked, dm_snapshot_merge_support
+    if dm_snapshot_merge_checked:
+        return dm_snapshot_merge_support
+    os.system("modprobe dm-snapshot")
+    p = Popen(["dmsetup", "targets"], stdout=PIPE, stderr=PIPE)
+    err = p.wait()
+    if not err:
+        output = p.communicate()[0]
+        if not output.find("snapshot-merge") == -1:
+            dm_snapshot_merge_support = 1
+        dm_snapshot_merge_checked = 1
+    return dm_snapshot_merge_support
+
+def inspect_volume_lvm(conduit, volume):
     """
-    This runs before the transaction starts.  Try to snapshot anything and
-    everything that is snapshottable, since we do not know what an RPM will
-    modify (thank you scriptlets).
+    If volume is an LVM logical volume:
+    - translate /dev/mapper name for LVM command use
+    - conditionally establish lvm_key in volume
     """
-    if not os.path.exists("/etc/mtab"):
-        conduit.info(1, "fs-snapshot: could not open /etc/mtab")
-        return
+    lvm_support = conduit.confBool('lvm', 'enabled', default=0)
+    if not lvm_support:
+        return 1
+    device = volume["device"]
+    # Inspect DM and LVM devices
+    if device.startswith("/dev/dm-"):
+        conduit.info(2, "fs-snapshot: unable to snapshot DM device: " + device)
+        return 0
+    if device.startswith("/dev/mapper/"):
+        # convert /dev/mapper name to /dev/vg/lv for use with LVM2 tools
+        # - 'dmsetup splitname' will collapse any escaped characters
+        p = Popen(["dmsetup", "splitname", "--separator", "/", "--noheadings",
+                   "-o", "vg_name,lv_name", device], stdout=PIPE, stderr=PIPE)
+        err = p.wait()
+        if err:
+            return 0
+        output = p.communicate()[0]
+        device = output.strip().replace("/dev/mapper/", "/dev/")
+        volume["device"] = device
+
+    # Check if device is managed by lvm
+    # - FIXME filter out snapshot (and other) LVs; for now just rely
+    #   on 'lvcreate' to prevent snapshots of unsupported LV types
+    p = Popen(["lvs", device], stdout=PIPE, stderr=PIPE)
+    err = p.wait()
+    if not err:
+        # FIXME allow creating snapshot LVs even if kernel doesn't
+        # support snapshot-merge based system rollback? make configurable?
+        if not kernel_supports_dm_snapshot_merge():
+            conduit.error(1, "fs-snapshot: skipping volume: %s, "
+                          "kernel doesn't support snapshot-merge" % device)
+            return 0
+        volume[lvm_key] = 1
+    return 1
 
-    excludeList = conduit.confString('main', 'exclude', default="").split()
+def inspect_volume(conduit, volume):
+    """
+    Hook to check/filter volume for special characteristics.
+    Returns 0 if volume failed inspection, otherwise 1.
+    All inspect_volume_* methods act as filters; if they
+    return 0 that means this volume failed inspection.
+    """
+    if not inspect_volume_lvm(conduit, volume):
+        return 0
+    # Additional inspect_volume_* methods may prove unnecessary but the
+    # filtering nature of these methods would make them unavoidable; e.g.
+    # just because a volume is LVM doesn't mean other filters should
+    # be short-circuited
+    return 1
 
+def get_volumes(conduit):
+    """
+    Return all volumes that may be snapshotted.
+    Each volume is a dictionary that contains descriptive key=value
+    pairs.  All volumes will have 'device', 'mntpnt', and 'fstype'
+    keys.  Extra keys may be established as a side-effect of
+    inspect_volume().
+    """
+    # FIXME may look to return dictionary of volume dictionaries to
+    # allow a volume to be looked up using its path (as the key).
+    # - when a kernel package is being installed: could prove useful to check
+    #   if "/" is an LVM volume and "/boot" is not able to be snapshotted; if
+    #   so warn user that "/boot" changes (e.g. grub's menu.lst) will need to
+    #   be manually rolled back.
+    volumes = []
+
+    excluded_mntpnts = conduit.confString('main', 'exclude', default="").split()
     try:
         mtabfile = open('/etc/mtab', 'r')
         for line in mtabfile.readlines():
-            device, mntpnt, type, rest  = line.split(' ', 3)
+            device, mntpnt, fstype, rest = line.split(' ', 3)
+            volume = { "device" : device,
+                       "mntpnt" : mntpnt,
+                       "fstype" : fstype }
+
+            if mntpnt in excluded_mntpnts:
+                continue
 
             # skip bind mounts
             if not rest.find("bind") == -1:
                 continue
 
-            skip = False
-            for pnt in excludeList:
-                if pnt == mntpnt:
-                    skip = True
-                    break
+            # skip any mounts whose device doesn't have a leading /
+            # - avoids proc, sysfs, devpts, sunrpc, none, etc.
+            if not device.find("/") == 0:
+                continue
 
-            if skip:
+            # skip volume if it doesn't pass inspection
+            # - inspect_volume may create additional keys in this volume
+            if not inspect_volume(conduit, volume):
                 continue
 
-            rc = _create_snapshot(device, mntpnt, type, conduit)
-            if rc == 1:
-                conduit.info(1, "fs-snapshot: error snapshotting " + mntpnt)
+            volumes.append(volume)
+
         mtabfile.close()
-    except Exception as (errno, strerror):
-        conduit.info(1, "fs-snapshot: error reading /etc/mtab")
 
-def _create_snapshot(device, mntpnt, type, conduit):
+    except Exception, e:
+        msg = "fs-snapshot: error processing mounted volumes: %s" % e
+        conduit.error(1, msg)
+
+    return volumes
+
+
+def _create_snapshot(conduit, snapshot_tag, volume):
     """
     Determines if the device is capable of being snapshotted and then calls the
     appropriate snapshotting function.  The idea is you could add something for
     lvm snapshots, nilfs2 or whatever else here.
     """
+    if volume["fstype"] == "btrfs":
+        return _create_btrfs_snapshot(conduit, snapshot_tag, volume)
+    elif volume.has_key(lvm_key):
+        return _create_lvm_snapshot(conduit, snapshot_tag, volume)
 
-    # at some point it would be nice to add filtering here, so users can specify
-    # which filesystems they don't want snapshotted and we'd automatically match
-    # them here and just return.
-
-    if type == "btrfs":
-        return _create_btrfs_snapshot(mntpnt, conduit)
-    
     return 0
 
-def _create_btrfs_snapshot(dir, conduit):
+def _create_btrfs_snapshot(conduit, snapshot_tag, volume):
     """
     Runs the commands necessary for a snapshot.  Basically its just
 
     btrfsctl -c /dir/to/snapshot    #this syncs the fs
-    btrfsctl -s /dir/to/snapshot/yum-month-date-year-hour:minute
+    btrfsctl -s /dir/to/snapshot/${snapshot_tag}
                 /dir/to/snapshot
 
     and then we're done.
     """
-
+    mntpnt = volume["mntpnt"]
     #/etc/mtab doesn't have /'s at the end of the mount point, unless of course
     #the mountpoint is /
-    if not dir.endswith("/"):
-        dir = dir + "/"
+    if not mntpnt.endswith("/"):
+        mntpnt = mntpnt + "/"
 
-    snapname = dir + "yum-" + time.strftime("%m-%d-%y-%H:%M")
-    conduit.info(1, "fs-snapshot: snapshotting " + dir + ": " + snapname)
-    p = Popen(["btrfsctl", "-c", dir], stdout=PIPE, stderr=PIPE)
+    snapname = mntpnt + snapshot_tag
+    conduit.info(1, "fs-snapshot: snapshotting " + mntpnt + ": " + snapname)
+    p = Popen(["btrfsctl", "-c", mntpnt], stdout=PIPE, stderr=PIPE)
     err = p.wait()
     if err:
         return 1
-    p = Popen(["btrfsctl", "-s", snapname, dir], stdout=PIPE, stderr=PIPE)
+    p = Popen(["btrfsctl", "-s", snapname, mntpnt], stdout=PIPE, stderr=PIPE)
     err = p.wait()
     if err:
         return 1
     return 0
+
+def _create_lvm_snapshot(conduit, snapshot_tag, volume):
+    """
+    Create LVM snapshot LV and tag it with $snapshot_tag.
+    - This assumes that the volume is an origin LV whose VG
+      has enough free space to accommodate a snapshot LV.
+    - Also assumes user has configured 'lvcreate_size_args'.
+    """
+    lvcreate_size_args = conduit.confString('lvm', 'lvcreate_size_args',
+                                            default=None)
+    if not lvcreate_size_args:
+        conduit.error(1, "fs-snapshot: 'lvcreate_size_args' was not provided "
+                      "in the '[lvm]' section of the config file")
+        return 1
+
+    if not lvcreate_size_args.startswith("-L") and not lvcreate_size_args.startswith("-l"):
+        conduit.error(1, "fs-snapshot: 'lvcreate_size_args' did not use -L or -l")
+        return 1
+
+    device = volume["device"]
+    if device.count('/') != 3:
+        return 1
+
+    mntpnt = volume["mntpnt"]
+    if mntpnt == "/":
+        # FIXME only print a variant of this warning if a kernel
+        # will be installed by the current yum transaction
+        conduit.info(1, "fs-snapshot: WARNING: creating LVM snapshot of root LV.  If a kernel is\n"
+                        "                      being installed /boot may need to be manually restored\n"
+                        "                      in the event that a system rollback proves necessary.")
+
+    snap_device = device + "_" + snapshot_tag
+    snap_lvname = snap_device.split('/')[3]
+    conduit.info(1, "fs-snapshot: snapshotting %s (%s): %s" %
+                 (mntpnt, device, snap_lvname))
+    # Create snapshot LV
+    lvcreate_cmd = ["lvcreate", "-s", "-n", snap_lvname]
+    lvcreate_cmd.extend(lvcreate_size_args.split())
+    lvcreate_cmd.append(device)
+    p = Popen(lvcreate_cmd, stdout=PIPE, stderr=PIPE)
+    err = p.wait()
+    if err:
+        conduit.error(1, "fs-snapshot: failed command: %s\n%s" %
+                      (" ".join(lvcreate_cmd), p.communicate()[1]))
+        return 1
+    # Add tag ($snapshot_tag) to snapshot LV
+    # - should help facilitate merge of all snapshot LVs created
+    #   by a yum transaction, e.g.: lvconvert --merge @snapshot_tag
+    p = Popen(["lvchange", "--addtag", snapshot_tag, snap_device],
+              stdout=PIPE, stderr=PIPE)
+    err = p.wait()
+    if err:
+        conduit.error(1, "fs-snapshot: couldn't add tag to snapshot: %s" %
+                      snap_device)
+    return 0
+
+def init_hook(conduit):
+    if hasattr(conduit, 'registerPackageName'):
+        conduit.registerPackageName("yum-plugin-fs-snapshot")
+    conduit.info(3, "Loading File System Snapshot support.")
+
+def pretrans_hook(conduit):
+    """
+    This runs before the transaction starts.  Try to snapshot anything and
+    everything that is snapshottable, since we do not know what an RPM will
+    modify (thank you scriptlets).
+    """
+    # common snapshot tag format: yum_${year}${month}${day}${hour}${minute}${sec}
+    snapshot_tag = "yum_" + time.strftime("%Y%m%d%H%M%S")
+
+    volumes = get_volumes(conduit)
+    for volume in volumes:
+        rc = _create_snapshot(conduit, snapshot_tag, volume)
+        if rc == 1:
+            conduit.error(1, "fs-snapshot: error snapshotting " + volume["mntpnt"])


More information about the Yum-commits mailing list