From cdac4d1af338af29949bb29045728c8be67a3f08 Mon Sep 17 00:00:00 2001 From: Johan Fleury Date: Fri, 6 Mar 2020 17:24:06 -0500 Subject: [PATCH] feat: add initial (and experimental) support for ZFS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On config creation, a new dataset is created (`.snapshots`) that will store snapshots information and a symlink to the snapshot in the `.zfs` directory. This dataset is destroyed on config deletion. Due to a ZFS “limitations”, snapshots can only be created read-only and cannot be derived from a parent. This is based on the initial idea and work of Daniel Sullivan (mumblepins). Implements #145. --- configure.ac | 13 ++- snapper/Filesystem.cc | 6 ++ snapper/Makefile.am | 6 ++ snapper/Snapper.cc | 6 ++ snapper/Zfs.cc | 198 ++++++++++++++++++++++++++++++++++++++++++ snapper/Zfs.h | 74 ++++++++++++++++ snapper/ZfsUtils.cc | 126 +++++++++++++++++++++++++++ snapper/ZfsUtils.h | 61 +++++++++++++ 8 files changed, 489 insertions(+), 1 deletion(-) create mode 100755 snapper/Zfs.cc create mode 100755 snapper/Zfs.h create mode 100644 snapper/ZfsUtils.cc create mode 100644 snapper/ZfsUtils.h diff --git a/configure.ac b/configure.ac index 0c3b58f4e..6ee84eeeb 100644 --- a/configure.ac +++ b/configure.ac @@ -37,6 +37,7 @@ AC_PATH_PROG([LVSBIN], [lvs], [/sbin/lvs]) AC_PATH_PROG([LVCHANGEBIN], [lvchange], [/sbin/lvchange]) AC_PATH_PROG([LVMBIN], [lvm], [/sbin/lvm]) AC_PATH_PROG([LVRENAMEBIN], [lvrename], [/sbin/lvrename]) +AC_PATH_PROG([ZFSBIN], [zfs], [/sbin/zfs]) AC_DEFINE_UNQUOTED([CHSNAPBIN], ["$CHSNAPBIN"], [Path of chsnap program.]) AC_DEFINE_UNQUOTED([CPBIN], ["$CPBIN"], [Path of cp program.]) @@ -50,6 +51,7 @@ AC_DEFINE_UNQUOTED([LVSBIN], ["$LVSBIN"], [Path of lvs program.]) AC_DEFINE_UNQUOTED([LVCHANGEBIN], ["$LVCHANGEBIN"], [Path of lvchange program.]) AC_DEFINE_UNQUOTED([LVMBIN], ["$LVMBIN"], [Path of lvm program.]) AC_DEFINE_UNQUOTED([LVRENAMEBIN], ["$LVRENAMEBIN"], [Path of lvrename program.]) +AC_DEFINE_UNQUOTED([ZFSBIN], ["$ZFSBIN"], [Path of zfs program.]) dnl Automake 1.11 enables silent compilation dnl Disable it by "configure --disable-silent-rules" or "make V=1" @@ -115,7 +117,16 @@ if test "x$with_lvm" = "xyes"; then AC_DEFINE(ENABLE_LVM, 1, [Enable LVM thin-provisioned snapshots support]) fi -if test "x$with_btrfs" != "xyes" -a "x$with_lvm" != "xyes" -a "x$with_ext4" != "xyes"; then +AC_ARG_ENABLE([zfs], AC_HELP_STRING([--disable-zfs],[Disable Zfs internal snapshots support]), + [with_zfs=$enableval],[with_zfs=yes]) + +AM_CONDITIONAL(ENABLE_ZFS, [test "x$with_zfs" = "xyes"]) + +if test "x$with_zfs" = "xyes"; then + AC_DEFINE(ENABLE_ZFS, 1, [Enable Zfs internal snapshots support]) +fi + +if test "x$with_btrfs" != "xyes" -a "x$with_lvm" != "xyes" -a "x$with_ext4" != "xyes" -a "x$with_zfs" != "xyes"; then AC_MSG_ERROR([You have to enable at least one snapshot type (remove some --disable-xxx parameter)]) fi diff --git a/snapper/Filesystem.cc b/snapper/Filesystem.cc index c01961ab5..fcdde901b 100644 --- a/snapper/Filesystem.cc +++ b/snapper/Filesystem.cc @@ -45,6 +45,9 @@ #ifdef ENABLE_LVM #include "snapper/Lvm.h" #endif +#ifdef ENABLE_ZFS +#include "snapper/Zfs.h" +#endif #include "snapper/Snapper.h" #include "snapper/SnapperTmpl.h" #include "snapper/SnapperDefines.h" @@ -106,6 +109,9 @@ namespace snapper #endif #ifdef ENABLE_LVM &Lvm::create, +#endif +#ifdef ENABLE_ZFS + &Zfs::create, #endif NULL }; diff --git a/snapper/Makefile.am b/snapper/Makefile.am index 3e8b0c071..1e2320d8c 100644 --- a/snapper/Makefile.am +++ b/snapper/Makefile.am @@ -51,6 +51,12 @@ libsnapper_la_SOURCES += \ LvmCache.cc LvmCache.h endif +if ENABLE_ZFS +libsnapper_la_SOURCES += \ + Zfs.cc Zfs.h \ + ZfsUtils.cc ZfsUtils.h +endif + if ENABLE_ROLLBACK libsnapper_la_SOURCES += \ MntTable.cc MntTable.h diff --git a/snapper/Snapper.cc b/snapper/Snapper.cc index 23086b1a8..966f3893a 100644 --- a/snapper/Snapper.cc +++ b/snapper/Snapper.cc @@ -48,6 +48,7 @@ #include "snapper/Hooks.h" #include "snapper/Btrfs.h" #include "snapper/BtrfsUtils.h" +#include "snapper/ZfsUtils.h" #ifdef ENABLE_SELINUX #include "snapper/Selinux.h" #include "snapper/Regex.h" @@ -1012,6 +1013,11 @@ namespace snapper #endif "ext4," +#ifndef ENABLE_ZFS + "no-" +#endif + "zfs," + #ifndef ENABLE_XATTRS "no-" #endif diff --git a/snapper/Zfs.cc b/snapper/Zfs.cc new file mode 100755 index 000000000..67e5f3dd0 --- /dev/null +++ b/snapper/Zfs.cc @@ -0,0 +1,198 @@ +/* + * Copyright (c) [2019] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you may + * find current contact information at www.novell.com. + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "snapper/Log.h" +#include "snapper/Filesystem.h" +#include "snapper/Zfs.h" +#include "snapper/Snapper.h" +#include "snapper/SnapperTmpl.h" +#include "snapper/SystemCmd.h" +#include "snapper/SnapperDefines.h" + + +namespace snapper +{ + Filesystem* + Zfs::create(const string& fstype, const string& subvolume, const string& root_prefix) + { + if (fstype == "zfs") + return new Zfs(subvolume, root_prefix); + + return NULL; + } + + Zfs::Zfs(const string& subvolume, const string& root_prefix) : Filesystem(subvolume, root_prefix) + { + if (access(ZFSBIN, X_OK) != 0) + SN_THROW(ProgramNotInstalledException(ZFSBIN " is not installed")); + + bool found = false; + MtabData mtab_data; + + if (!getMtabData(subvolume, found, mtab_data)) + { + y2err("can't get mtab data for " + subvolume); + SN_THROW(InvalidConfigException()); + } + + if (!found) + { + y2err("filesystem not mounted"); + SN_THROW(InvalidConfigException()); + } + + zfs_volume_name = mtab_data.device; + zfs_snapshot_volume_name = zfs_volume_name + "/.snapshots"; + } + + string + Zfs::snapshotDir(unsigned int num) const + { + return (subvolume == "/" ? "" : subvolume) + "/.snapshots/" + decString(num) + "/snapshot"; + } + + void + Zfs::createConfig() const + { + create_volume(zfs_snapshot_volume_name, true); + } + + void + Zfs::deleteConfig() const + { + delete_volume(zfs_snapshot_volume_name); + } + + SDir + Zfs::openInfosDir() const + { + SDir subvolume_dir = openSubvolumeDir(); + + if (subvolume_dir.mkdir(".snapshots", 0700) != 0 && errno != EEXIST) + { + y2err("mkdir failed errno:" << errno << " (" << strerror(errno) << ")"); + SN_THROW(CreateConfigFailedException("mkdir failed")); + } + + SDir infos_dir(subvolume_dir, ".snapshots"); + + struct stat stat; + + if (infos_dir.stat(&stat) != 0) + { + SN_THROW(IOErrorException("failed to stat " + infos_dir.fullname())); + } + + if (stat.st_uid != 0) + { + SN_THROW(IOErrorException(".snapshots must have owner root")); + } + + if (stat.st_gid != 0 && stat.st_mode & S_IWGRP) + { + SN_THROW(IOErrorException(".snapshots must have group root or must not be group-writable")); + } + + if (stat.st_mode & S_IWOTH) + { + SN_THROW(IOErrorException(".snapshots must not be world-writable")); + } + + return infos_dir; + } + + SDir + Zfs::openSnapshotDir(unsigned int num) const + { + SDir info_dir = openInfoDir(num); + SDir snapshot_dir(info_dir, "snapshot"); + + return snapshot_dir; + } + + void + Zfs::mountSnapshot(unsigned int num) const + { + } + + void + Zfs::umountSnapshot(unsigned int num) const + { + } + + /* ZFS snapshots are always mounted int the `.zfs` directory, they are also + * automatically symlinked by snapper in the datasets' `.snapshot` + * directory. + */ + bool + Zfs::isSnapshotMounted(unsigned int num) const + { + return true; + } + + void + Zfs::createSnapshot(unsigned int num, unsigned int num_parent, bool read_only, bool quota, bool empty) const + { + if (num_parent != 0 || !read_only || quota || empty) + SN_THROW(UnsupportedException()); + + create_snapshot(zfs_volume_name, decString(num)); + symlink("../../.zfs/snapshot/" + decString(num), snapshotDir(num)); + } + + void + Zfs::deleteSnapshot(unsigned int num) const + { + unlink(snapshotDir(num).c_str()); + delete_snapshot(zfs_volume_name, decString(num)); + } + + bool + Zfs::checkSnapshot(unsigned int num) const + { + return snapshot_exists(zfs_volume_name, decString(num)); + } + + /* ZFS does not support mutable snapshots. + */ + bool + Zfs::isSnapshotReadOnly(unsigned int num) const + { + return true; + } + + void + Zfs::sync() const + { + } +} diff --git a/snapper/Zfs.h b/snapper/Zfs.h new file mode 100755 index 000000000..381188cca --- /dev/null +++ b/snapper/Zfs.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) [2019] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you may + * find current contact information at www.novell.com. + */ + + +#ifndef SNAPPER_ZFS_H +#define SNAPPER_ZFS_H + + +#include "snapper/Filesystem.h" +#include "snapper/ZfsUtils.h" + + +namespace snapper +{ + using namespace ZfsUtils; + + class Zfs : public Filesystem + { + public: + static Filesystem* create(const string& fstype, const string& subvolume, const string& root_prefix); + + Zfs(const string& subvolume, const string& root_prefix); + + virtual string fstype() const + { + return "zfs"; + } + + virtual string snapshotDir(unsigned int num) const; + + virtual void createConfig() const; + virtual void deleteConfig() const; + + virtual SDir openInfosDir() const; + + virtual SDir openSnapshotDir(unsigned int num) const; + + virtual void mountSnapshot(unsigned int num) const; + virtual void umountSnapshot(unsigned int num) const; + virtual bool isSnapshotMounted(unsigned int num) const; + + virtual void createSnapshot(unsigned int num, unsigned int num_parent, bool read_only, bool quota, bool empty) const; + virtual void deleteSnapshot(unsigned int num) const; + virtual bool checkSnapshot(unsigned int num) const; + + virtual bool isSnapshotReadOnly(unsigned int num) const; + + virtual void sync() const; + + private: + string zfs_volume_name; + string zfs_snapshot_volume_name; + }; +} + +#endif diff --git a/snapper/ZfsUtils.cc b/snapper/ZfsUtils.cc new file mode 100644 index 000000000..b58b27472 --- /dev/null +++ b/snapper/ZfsUtils.cc @@ -0,0 +1,126 @@ +/* + * Copyright (c) [2019] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you may + * find current contact information at www.novell.com. + */ + + +#include "config.h" + +#include +#include + +#include "snapper/Regex.h" +#include "snapper/Snapper.h" +#include "snapper/SystemCmd.h" +#include "snapper/ZfsUtils.h" + + +namespace snapper +{ + namespace ZfsUtils + { + void + create_volume(const string& name, const bool exist_ok) + { + SystemCmd cmd(ZFSBIN " create " + string(exist_ok ? "-p " : "") + " " + quote(name)); + + if (cmd.retcode() != 0) + SN_THROW(ZfsCallException("Creating ZFS subvol failed")); + } + + void + delete_volume(const string& name) + { + if (!volume_exists(name)) + return; + + SystemCmd cmd(ZFSBIN " destroy " + quote(name)); + + if (cmd.retcode() != 0) + SN_THROW(ZfsCallException("Deleting ZFS vol failed")); + } + + bool + volume_exists(const string& name) + { + SystemCmd cmd(ZFSBIN " get -H -d 1 -o name name " + name); + + if (cmd.retcode() != 0) + return false; + + return true; + } + + /* + * Create a ZFS snapshot. + * + * @throws ZfsCallException if creation fails. + */ + void + create_snapshot(const string& fs, const string& name) + { + SystemCmd cmd(ZFSBIN " snapshot " + quote(fs + "@" + name)); + + if (cmd.retcode() != 0) + SN_THROW(ZfsCallException("Creating ZFS snapshot failed")); + } + + /* + * Delete a ZFS snapshot. + * + * @throws ZfsCallException if deleting fails. + */ + void + delete_snapshot(const string& fs, const string& name) + { + delete_volume(fs + "@" + name); + } + + bool + snapshot_exists(const string& fs, const string& name) + { + return volume_exists(quote(fs + "@" + name)); + } + + /* Extract component names from volume name. + * + * The volume name must be of the form pool[/dataset][@snapshot]. + * + * @returns a std::tuple with the pool name, dataset name and snapshot name + */ + tuple + extract_components(const string& name) + { + string pool, dataset, snapshot; + + string::size_type first_slash = name.find_first_of('/'); + string::size_type first_at = name.find_first_of('@'); + + pool = string(name, 0, first_slash); + + if (first_slash != string::npos) + dataset = string(name, first_slash + 1, first_at - first_slash - 1); + + if (first_at != string::npos) + snapshot = string(name, first_at + 1); + + return std::make_tuple(pool, dataset, snapshot); + } + } +} diff --git a/snapper/ZfsUtils.h b/snapper/ZfsUtils.h new file mode 100644 index 000000000..c0b2f8e48 --- /dev/null +++ b/snapper/ZfsUtils.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) [2019] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you may + * find current contact information at www.novell.com. + */ + +#ifndef ZFSUTILS_H +#define ZFSUTILS_H + +#include +#include "snapper/Exception.h" +#include "snapper/FileUtils.h" + +namespace snapper +{ + using std::string; + using std::vector; + using std::tuple; + + namespace ZfsUtils + { + struct ZfsCallException : public Exception + { + explicit ZfsCallException(const string& msg) : Exception(msg) {} + }; + + struct ZfsVolumeInfo + { + string pool; + string dataset; + string snapshot; + }; + + void create_volume(const string& name, const bool exist_ok = false); + void delete_volume(const string& name); + bool volume_exists(const string& name); + + void create_snapshot(const string& fs, const string& name); + void delete_snapshot(const string& fs, const string& name); + bool snapshot_exists(const string& fs, const string& name); + + tuple extract_components(const string& name); + } +} + +#endif /* ZFSUTILS_H */