]> git.ipfire.org Git - thirdparty/snapper.git/commitdiff
- added experimental support for LVM thin-provisioned snapshots
authorArvin Schnell <aschnell@suse.de>
Tue, 24 Jul 2012 12:38:24 +0000 (14:38 +0200)
committerArvin Schnell <aschnell@suse.de>
Tue, 24 Jul 2012 12:38:24 +0000 (14:38 +0200)
doc/snapper.8.in
package/snapper.changes
snapper/AppUtil.cc
snapper/AppUtil.h
snapper/Filesystem.cc
snapper/Filesystem.h
snapper/Snapper.cc
snapper/SnapperDefines.h

index 2dea5dd8d7aa0ba58f7a7dbff7583211ac86cc66..30df1386d2f46256e4d73a9b3abe4de53f2490ac 100644 (file)
@@ -1,4 +1,4 @@
-.TH "SNAPPER" "8" "2012-03-20" "@VERSION@" "Filesystem Snapshot Management"
+.TH "SNAPPER" "8" "2012-07-24" "@VERSION@" "Filesystem Snapshot Management"
 .SH "NAME"
 .LP
 snapper - Command\-line program for filesystem snapshot management
@@ -16,7 +16,8 @@ Snapper is a command\-line program for filesystem snapshot management. It can
 create, delete and compare snapshots and undo changes done between snapshots.
 .LP
 Snapper never modifies the content of snapshots. Thus snapper creates
-read-only snapshots. Supported filesystems are btrfs and ext4.
+read-only snapshots. Supported filesystems are btrfs and ext4 as well as
+snapshots of LVM logical volumes with thin-provisioning.
 
 .SH CONCEPTS
 
@@ -115,8 +116,12 @@ will likely need the global option \fI--config\fR, see \fBGLOBAL OPTIONS\fR and
 \fBCONCEPTS\fR.
 .TP
 \fI\-f, \-\-fstype\fR <fstype>
-Manually set filesystem type. Supported values are btrfs and ext4. Without
-this option snapper detect the filesystem.
+Manually set filesystem type. Supported values are btrfs, ext4 and lvm. For
+lvm snapper used LVM thin-provisioned snapshots. The filesystem type on top of
+LVM can be provided in parentheses, e.g. lvm(xfs). This is required for XFS.
+
+Without this option snapper tries to detect the filesystem.
+
 .TP
 \fI\-t, \-\-template\fR <name>
 Name of template for the new configuration file.
@@ -251,4 +256,4 @@ Arvin Schnell <aschnell@suse.de>
 
 .SH "SEE ALSO"
 .LP
-btrfs(8)
+btrfs(8), lvcreate(2)
index 92b9a4cf87b69f4daa51ab662ef805a66ab87075..ac65c41c25c8d806fc0795903d586b5651646801 100644 (file)
@@ -1,3 +1,8 @@
+-------------------------------------------------------------------
+Tue Jul 24 14:35:44 CEST 2012 - aschnell@suse.de
+
+- added experimental support for LVM thin-provisioned snapshots
+
 -------------------------------------------------------------------
 Tue Jun 12 10:07:08 CEST 2012 - aschnell@suse.de
 
index a1d7b630810011d42d28b95fd987298ddddd96ea..64dc2656ea1feaeb055a05549a6bcbcd4c46d3bc 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) [2004-2011] Novell, Inc.
+ * Copyright (c) [2004-2012] Novell, Inc.
  *
  * All Rights Reserved.
  *
@@ -31,6 +31,7 @@
 #include <sys/utsname.h>
 #include <sys/ioctl.h>
 #include <dirent.h>
+#include <mntent.h>
 #include <string>
 #include <libxml/tree.h>
 #include <boost/algorithm/string.hpp>
@@ -73,6 +74,14 @@ namespace snapper
     }
 
 
+    bool
+    checkAnything(const string& Path_Cv)
+    {
+       struct stat Stat_ri;
+       return stat(Path_Cv.c_str(), &Stat_ri) >= 0;
+    }
+
+
     list<string>
     glob(const string& path, int flags)
     {
@@ -199,6 +208,40 @@ namespace snapper
     }
 
 
+    bool
+    getMtabData(const string& mount_point, bool& found, MtabData& mtab_data)
+    {
+       FILE* f = setmntent("/etc/mtab", "r");
+       if (!f)
+       {
+           y2err("setmntent failed");
+           return false;
+       }
+
+       found = false;
+
+       struct mntent* m;
+       while ((m = getmntent(f)))
+       {
+           if (strcmp(m->mnt_type, "rootfs") == 0)
+               continue;
+
+           if (m->mnt_dir == mount_point)
+           {
+               found = true;
+               mtab_data.device = m->mnt_fsname;
+               mtab_data.mount_point = m->mnt_dir;
+               mtab_data.type = m->mnt_type;
+               break;
+           }
+       }
+
+       endmntent(f);
+
+       return true;
+    }
+
+
     string
     sformat(const string& format, ...)
     {
index 344722632cf1f3ae525b274dfc8c3b0417e55245..0446d9e02ade3ec49f12137ba7371ff260f0cbc3 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) [2004-2011] Novell, Inc.
+ * Copyright (c) [2004-2012] Novell, Inc.
  *
  * All Rights Reserved.
  *
@@ -43,6 +43,7 @@ namespace snapper
     void createPath(const string& Path_Cv);
     bool checkNormalFile(const string& Path_Cv);
     bool checkDir(const string& Path_Cv);
+    bool checkAnything(const string& Path_Cv);
 
     list<string> glob(const string& path, int flags);
 
@@ -56,6 +57,16 @@ namespace snapper
 
     string realpath(const string& path);
 
+    struct MtabData
+    {
+       string device;
+       string mount_point;
+       string type;
+    };
+
+    bool getMtabData(const string& mount_point, bool& found, MtabData& mtab_data);
+
+
     template<class StreamType>
     void classic(StreamType& stream)
     {
index abd10a3a55c90f9ce7535d081d90484480b06f1d..9ab83f28fd2cc43ac9552c298b591a7f2638c9eb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 Novell, Inc.
+ * Copyright (c) [2011-2012] Novell, Inc.
  *
  * All Rights Reserved.
  *
@@ -25,7 +25,7 @@
 #include <sys/types.h>
 #include <errno.h>
 #include <unistd.h>
-#include <mntent.h>
+#include <boost/algorithm/string.hpp>
 
 #include "snapper/Log.h"
 #include "snapper/Filesystem.h"
 #include "snapper/SnapperTmpl.h"
 #include "snapper/SystemCmd.h"
 #include "snapper/SnapperDefines.h"
+#include "snapper/Regex.h"
 
 
 namespace snapper
 {
 
+    bool
+    mount(const string& device, const string& mount_point, const string& mount_type,
+         const vector<string>& options)
+    {
+       string cmd_line = MOUNTBIN " -t " + mount_type + " --read-only";
+
+       if (!options.empty())
+           cmd_line += " -o " + boost::join(options, ",");
+
+       cmd_line += " " + quote(device) + " " + quote(mount_point);
+
+       SystemCmd cmd(cmd_line);
+       return cmd.retcode() == 0;
+    }
+
+
+    bool
+    umount(const string& mount_point)
+    {
+       SystemCmd cmd(UMOUNTBIN " " + quote(mount_point));
+       return cmd.retcode() == 0;
+    }
+
+
     Filesystem*
     Filesystem::create(const string& fstype, const string& subvolume)
     {
-       if (fstype == "btrfs")
-           return new Btrfs(subvolume);
+       typedef Filesystem* (*func_t)(const string& fstype, const string& subvolume);
 
-       if (fstype == "ext4")
-           return new Ext4(subvolume);
+       static const func_t funcs[] = { &Btrfs::create, &Ext4::create, &Lvm::create, NULL };
 
+       for (const func_t* func = funcs; func != NULL; ++func)
+       {
+           Filesystem* fs = (*func)(fstype, subvolume);
+           if (fs)
+               return fs;
+       }
+
+       y2err("do not know about fstype '" << fstype << "'");
        throw InvalidConfigException();
     }
 
 
+    Filesystem*
+    Btrfs::create(const string& fstype, const string& subvolume)
+    {
+       if (fstype == "btrfs")
+           return new Btrfs(subvolume);
+
+       return NULL;
+    }
+
+
     Btrfs::Btrfs(const string& subvolume)
        : Filesystem(subvolume)
     {
@@ -89,8 +130,8 @@ namespace snapper
     string
     Btrfs::snapshotDir(unsigned int num) const
     {
-       return (subvolume == "/" ? "" : subvolume) + "/.snapshots/" +
-           decString(num) + "/snapshot";
+       return (subvolume == "/" ? "" : subvolume) + "/.snapshots/" + decString(num) +
+           "/snapshot";
     }
 
 
@@ -139,6 +180,16 @@ namespace snapper
     }
 
 
+    Filesystem*
+    Ext4::create(const string& fstype, const string& subvolume)
+    {
+       if (fstype == "ext4")
+           return new Ext4(subvolume);
+
+       return NULL;
+    }
+
+
     Ext4::Ext4(const string& subvolume)
        : Filesystem(subvolume)
     {
@@ -157,7 +208,7 @@ namespace snapper
     void
     Ext4::createConfig() const
     {
-       int r1 = mkdir((subvolume + "/.snapshots").c_str(), 700);
+       int r1 = mkdir((subvolume + "/.snapshots").c_str(), 0700);
        if (r1 == 0)
        {
            SystemCmd cmd1(CHATTRBIN " +x " + quote(subvolume + "/.snapshots"));
@@ -170,7 +221,7 @@ namespace snapper
            throw CreateConfigFailedException("mkdir failed");
        }
 
-       int r2 = mkdir((subvolume + "/.snapshots/.info").c_str(), 700);
+       int r2 = mkdir((subvolume + "/.snapshots/.info").c_str(), 0700);
        if (r2 == 0)
        {
            SystemCmd cmd2(CHATTRBIN " -x " + quote(subvolume + "/.snapshots/.info"));
@@ -250,29 +301,11 @@ namespace snapper
     bool
     Ext4::isSnapshotMounted(unsigned int num) const
     {
-       FILE* f = setmntent("/etc/mtab", "r");
-       if (!f)
-       {
-           y2err("setmntent failed");
-           throw IsSnapshotMountedFailedException();
-       }
-
        bool mounted = false;
+       MtabData mtab_data;
 
-       struct mntent* m;
-       while ((m = getmntent(f)))
-       {
-           if (strcmp(m->mnt_type, "rootfs") == 0)
-               continue;
-
-           if (m->mnt_dir == snapshotDir(num))
-           {
-               mounted = true;
-               break;
-           }
-       }
-
-       endmntent(f);
+       if (!getMtabData(snapshotDir(num), mounted, mtab_data))
+           throw IsSnapshotMountedFailedException();
 
        return mounted;
     }
@@ -295,9 +328,12 @@ namespace snapper
            throw MountSnapshotFailedException();
        }
 
-       SystemCmd cmd2(MOUNTBIN " -t ext4 -r -o loop,noload " + quote(snapshotFile(num)) +
-                      " " + quote(snapshotDir(num)));
-       if (cmd2.retcode() != 0)
+       vector<string> options;
+       options.push_back("noatime");
+       options.push_back("loop");
+       options.push_back("noload");
+
+       if (!mount(snapshotFile(num), snapshotDir(num), "ext4", options))
            throw MountSnapshotFailedException();
     }
 
@@ -308,12 +344,11 @@ namespace snapper
        if (!isSnapshotMounted(num))
            return;
 
-       SystemCmd cmd1(UMOUNTBIN " " + quote(snapshotDir(num)));
-       if (cmd1.retcode() != 0)
+       if (!umount(snapshotDir(num)))
            throw UmountSnapshotFailedException();
 
-       SystemCmd cmd2(CHSNAPBIN " -n " + quote(snapshotFile(num)));
-       if (cmd2.retcode() != 0)
+       SystemCmd cmd1(CHSNAPBIN " -n " + quote(snapshotFile(num)));
+       if (cmd1.retcode() != 0)
            throw UmountSnapshotFailedException();
 
        rmdir(snapshotDir(num).c_str());
@@ -326,4 +361,192 @@ namespace snapper
        return checkNormalFile(snapshotFile(num));
     }
 
+
+    Filesystem*
+    Lvm::create(const string& fstype, const string& subvolume)
+    {
+       if (fstype == "lvm")
+           return new Lvm(subvolume, "auto");
+
+       Regex rx("^lvm\\(([_a-z0-9]+)\\)$");
+       if (rx.match(fstype))
+           return new Lvm(subvolume, rx.cap(1));
+
+       return NULL;
+    }
+
+
+    Lvm::Lvm(const string& subvolume, const string& mount_type)
+       : Filesystem(subvolume), mount_type(mount_type)
+    {
+       if (access(LVCREATE, X_OK) != 0)
+       {
+           throw ProgramNotInstalledException(LVCREATE " not installed");
+       }
+
+       if (!detectLvmNames())
+       {
+           throw InvalidConfigException();
+       }
+    }
+
+
+    void
+    Lvm::createConfig() const
+    {
+       int r1 = mkdir((subvolume + "/.snapshots").c_str(), 0700);
+       if (r1 != 0 && errno != EEXIST)
+       {
+           y2err("mkdir failed errno:" << errno << " (" << strerror(errno) << ")");
+           throw CreateConfigFailedException("mkdir failed");
+       }
+    }
+
+
+    void
+    Lvm::deleteConfig() const
+    {
+       int r1 = rmdir((subvolume + "/.snapshots").c_str());
+       if (r1 != 0)
+       {
+           y2err("rmdir failed errno:" << errno << " (" << strerror(errno) << ")");
+           throw DeleteConfigFailedException("rmdir failed");
+       }
+    }
+
+
+    string
+    Lvm::infosDir() const
+    {
+       return (subvolume == "/" ? "" : subvolume) + "/.snapshots";
+    }
+
+
+    string
+    Lvm::snapshotDir(unsigned int num) const
+    {
+       return (subvolume == "/" ? "" : subvolume) + "/.snapshots/" + decString(num) +
+           "/snapshot";
+    }
+
+
+    string
+    Lvm::snapshotLvName(unsigned int num) const
+    {
+       return lv_name + "-snapshot" + decString(num);
+    }
+
+
+    void
+    Lvm::createSnapshot(unsigned int num) const
+    {
+       sync();                 // TODO looks like a bug that this is needed (with ext4)
+
+       SystemCmd cmd(LVCREATE " --snapshot --name " + quote(snapshotLvName(num)) + " " +
+                     quote(vg_name + "/" + lv_name));
+       if (cmd.retcode() != 0)
+           throw CreateSnapshotFailedException();
+
+       int r1 = mkdir(snapshotDir(num).c_str(), 0700);
+       if (r1 != 0 && errno != EEXIST)
+       {
+           y2err("mkdir failed errno:" << errno << " (" << strerror(errno) << ")");
+           throw CreateSnapshotFailedException();
+       }
+    }
+
+
+    void
+    Lvm::deleteSnapshot(unsigned int num) const
+    {
+       SystemCmd cmd(LVREMOVE " --force " + quote(vg_name + "/" + snapshotLvName(num)));
+       if (cmd.retcode() != 0)
+           throw DeleteSnapshotFailedException();
+
+       rmdir(snapshotDir(num).c_str());
+    }
+
+
+    bool
+    Lvm::isSnapshotMounted(unsigned int num) const
+    {
+       bool mounted = false;
+       MtabData mtab_data;
+
+       if (!getMtabData(snapshotDir(num), mounted, mtab_data))
+           throw IsSnapshotMountedFailedException();
+
+       return mounted;
+    }
+
+
+    void
+    Lvm::mountSnapshot(unsigned int num) const
+    {
+       if (isSnapshotMounted(num))
+           return;
+
+       vector<string> options;
+       options.push_back("noatime");
+       if (mount_type == "xfs")
+           options.push_back("nouuid");
+
+       if (!mount(getDevice(num), snapshotDir(num), mount_type, options))
+           throw MountSnapshotFailedException();
+    }
+
+
+    void
+    Lvm::umountSnapshot(unsigned int num) const
+    {
+       if (!isSnapshotMounted(num))
+           return;
+
+       if (!umount(snapshotDir(num)))
+           throw UmountSnapshotFailedException();
+    }
+
+
+    bool
+    Lvm::checkSnapshot(unsigned int num) const
+    {
+       return checkAnything(getDevice(num));
+    }
+
+
+    bool
+    Lvm::detectLvmNames()
+    {
+       bool found = false;
+       MtabData mtab_data;
+
+       if (!getMtabData(subvolume, found, mtab_data))
+           return false;
+
+       if (!found)
+       {
+           y2err("logical volume not mounted");
+           return false;
+       }
+
+       Regex rx("^/dev/mapper/([^-]+)-([^-]+)$");
+       if (rx.match(mtab_data.device))
+       {
+           vg_name = boost::replace_all_copy(rx.cap(1), "--", "-");
+           lv_name = boost::replace_all_copy(rx.cap(2), "--", "-");
+           return true;
+       }
+
+       y2err("could not detect lvm names from '" << mtab_data.device << "'");
+       return false;
+    }
+
+
+    string
+    Lvm::getDevice(unsigned int num) const
+    {
+       return "/dev/mapper/" + boost::replace_all_copy(vg_name, "-", "--") + "-" +
+           boost::replace_all_copy(snapshotLvName(num), "-", "--");
+    }
+
 }
index 0c44d66a9b984a02e8906e063254618d78151508..2520a8f1e25e0dd89f657b6ac55d7b391de84d58 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 Novell, Inc.
+ * Copyright (c) [2011-2012] Novell, Inc.
  *
  * All Rights Reserved.
  *
@@ -44,7 +44,7 @@ namespace snapper
 
        static Filesystem* create(const string& fstype, const string& subvolume);
 
-       virtual string name() const = 0;
+       virtual string fstype() const = 0;
 
        virtual void createConfig() const = 0;
        virtual void deleteConfig() const = 0;
@@ -72,9 +72,11 @@ namespace snapper
     {
     public:
 
+       static Filesystem* create(const string& fstype, const string& subvolume);
+
        Btrfs(const string& subvolume);
 
-       virtual string name() const { return "btrfs"; }
+       virtual string fstype() const { return "btrfs"; }
 
        virtual void createConfig() const;
        virtual void deleteConfig() const;
@@ -98,9 +100,11 @@ namespace snapper
     {
     public:
 
+       static Filesystem* create(const string& fstype, const string& subvolume);
+
        Ext4(const string& subvolume);
 
-       virtual string name() const { return "ext4"; }
+       virtual string fstype() const { return "ext4"; }
 
        virtual void createConfig() const;
        virtual void deleteConfig() const;
@@ -120,6 +124,46 @@ namespace snapper
 
     };
 
+
+    class Lvm : public Filesystem
+    {
+    public:
+
+       static Filesystem* create(const string& fstype, const string& subvolume);
+
+       Lvm(const string& subvolume, const string& mount_type);
+
+       virtual string fstype() const { return "lvm(" + mount_type + ")"; }
+
+       virtual void createConfig() const;
+       virtual void deleteConfig() const;
+
+       virtual string infosDir() const;
+       virtual string snapshotDir(unsigned int num) const;
+       virtual string snapshotLvName(unsigned int num) const;
+
+       virtual void createSnapshot(unsigned int num) const;
+       virtual void deleteSnapshot(unsigned int num) const;
+
+       virtual bool isSnapshotMounted(unsigned int num) const;
+       virtual void mountSnapshot(unsigned int num) const;
+       virtual void umountSnapshot(unsigned int num) const;
+
+       virtual bool checkSnapshot(unsigned int num) const;
+
+    private:
+
+       const string mount_type;
+
+       bool detectLvmNames();
+
+       string getDevice(unsigned int num) const;
+
+       string vg_name;
+       string lv_name;
+
+    };
+
 }
 
 
index d8ab010015f9497fd6a073dafbe903810ec429b5..a9328a7e0409da6eed384ed36b64d65b6a9c254d 100644 (file)
@@ -71,7 +71,7 @@ namespace snapper
        config->getValue("FSTYPE", fstype);
        filesystem = Filesystem::create(fstype, subvolume);
 
-       y2mil("subvolume:" << subvolume << " filesystem:" << filesystem->name());
+       y2mil("subvolume:" << subvolume << " filesystem:" << filesystem->fstype());
 
        if (!disable_filters)
            loadIgnorePatterns();
@@ -628,7 +628,7 @@ namespace snapper
        {
            SysconfigFile config(CONFIGSDIR "/" + config_name);
            config.setValue("SUBVOLUME", subvolume);
-           config.setValue("FSTYPE", fstype);
+           config.setValue("FSTYPE", filesystem->fstype());
        }
        catch (const FileNotFoundException& e)
        {
index 89ce74ed8e83e55099b41dbc9daeced10a827086..a3a3f625a06a4fb7cb9ce279836e53c63ea0b211 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) [2004-2011] Novell, Inc.
+ * Copyright (c) [2004-2012] Novell, Inc.
  *
  * All Rights Reserved.
  *
@@ -49,5 +49,8 @@
 #define MOUNTBIN "/bin/mount"
 #define UMOUNTBIN "/bin/umount"
 
+#define LVCREATE "/sbin/lvcreate"
+#define LVREMOVE "/sbin/lvremove"
+
 
 #endif