From: Arvin Schnell Date: Tue, 24 Jul 2012 12:38:24 +0000 (+0200) Subject: - added experimental support for LVM thin-provisioned snapshots X-Git-Tag: v0.1.3~134^2~14 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=3bda118a1870ad5cf99ec644c2f163d6d5a5202e;p=thirdparty%2Fsnapper.git - added experimental support for LVM thin-provisioned snapshots --- diff --git a/doc/snapper.8.in b/doc/snapper.8.in index 2dea5dd8..30df1386 100644 --- a/doc/snapper.8.in +++ b/doc/snapper.8.in @@ -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 -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 of template for the new configuration file. @@ -251,4 +256,4 @@ Arvin Schnell .SH "SEE ALSO" .LP -btrfs(8) +btrfs(8), lvcreate(2) diff --git a/package/snapper.changes b/package/snapper.changes index 92b9a4cf..ac65c41c 100644 --- a/package/snapper.changes +++ b/package/snapper.changes @@ -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 diff --git a/snapper/AppUtil.cc b/snapper/AppUtil.cc index a1d7b630..64dc2656 100644 --- a/snapper/AppUtil.cc +++ b/snapper/AppUtil.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) [2004-2011] Novell, Inc. + * Copyright (c) [2004-2012] Novell, Inc. * * All Rights Reserved. * @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -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 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, ...) { diff --git a/snapper/AppUtil.h b/snapper/AppUtil.h index 34472263..0446d9e0 100644 --- a/snapper/AppUtil.h +++ b/snapper/AppUtil.h @@ -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 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 void classic(StreamType& stream) { diff --git a/snapper/Filesystem.cc b/snapper/Filesystem.cc index abd10a3a..9ab83f28 100644 --- a/snapper/Filesystem.cc +++ b/snapper/Filesystem.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 Novell, Inc. + * Copyright (c) [2011-2012] Novell, Inc. * * All Rights Reserved. * @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include "snapper/Log.h" #include "snapper/Filesystem.h" @@ -33,24 +33,65 @@ #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& 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 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 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), "-", "--"); + } + } diff --git a/snapper/Filesystem.h b/snapper/Filesystem.h index 0c44d66a..2520a8f1 100644 --- a/snapper/Filesystem.h +++ b/snapper/Filesystem.h @@ -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; + + }; + } diff --git a/snapper/Snapper.cc b/snapper/Snapper.cc index d8ab0100..a9328a7e 100644 --- a/snapper/Snapper.cc +++ b/snapper/Snapper.cc @@ -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) { diff --git a/snapper/SnapperDefines.h b/snapper/SnapperDefines.h index 89ce74ed..a3a3f625 100644 --- a/snapper/SnapperDefines.h +++ b/snapper/SnapperDefines.h @@ -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