# Makefile.am for snapper/client
#
-SUBDIRS = utils proxy snapper mksubvolume installation-helper systemd-helper
+SUBDIRS = utils proxy snapper mksubvolume installation-helper systemd-helper snbk
--- /dev/null
+*.o
+*.lo
+*.la
+snbk
--- /dev/null
+/*
+ * Copyright (c) 2024 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 <cstring>
+
+#include <snapper/SnapperDefines.h>
+#include <snapper/AppUtil.h>
+
+#include "BackupConfig.h"
+#include "JsonFile.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ const vector<string> EnumInfo<BackupConfig::TargetMode>::names({ "local", "ssh-push" });
+
+
+ BackupConfig::BackupConfig(const string& name)
+ : name(name)
+ {
+ JsonFile json_file(BACKUP_CONFIGS_DIR "/" + name + ".json");
+
+ if (!get_child_value(json_file.get_root(), "config", config))
+ SN_THROW(Exception("config entry not found in file"));
+
+ string tmp1;
+ if (!get_child_value(json_file.get_root(), "target-mode", tmp1))
+ SN_THROW(Exception("target-mode entry not found in file"));
+ if (!toValue(tmp1, target_mode, false))
+ SN_THROW(Exception("unknown target-mode"));
+
+ if (!get_child_value(json_file.get_root(), "source-path", source_path))
+ SN_THROW(Exception("source-path entry not found in file"));
+
+ if (!get_child_value(json_file.get_root(), "target-path", target_path))
+ SN_THROW(Exception("target-path entry not found in file"));
+
+ get_child_value(json_file.get_root(), "automatic", automatic);
+
+ if (target_mode == TargetMode::SSH_PUSH)
+ {
+ if (!get_child_value(json_file.get_root(), "ssh-host", ssh_host))
+ SN_THROW(Exception("ssh-host entry not found in file"));
+
+ get_child_value(json_file.get_root(), "ssh-port", ssh_port);
+ get_child_value(json_file.get_root(), "ssh-user", ssh_user);
+ get_child_value(json_file.get_root(), "ssh-identity", ssh_identity);
+ }
+ }
+
+
+ Shell
+ BackupConfig::get_source_shell() const
+ {
+ Shell source_shell;
+
+ return source_shell;
+ }
+
+
+ Shell
+ BackupConfig::get_target_shell() const
+ {
+ Shell target_shell;
+
+ if (target_mode == TargetMode::SSH_PUSH)
+ {
+ target_shell.mode = Shell::Mode::SSH;
+ target_shell.ssh_options = ssh_options();
+ }
+
+ return target_shell;
+ }
+
+
+ vector<string>
+ BackupConfig::ssh_options() const
+ {
+ vector<string> options = { ssh_host };
+
+ if (ssh_port != 0)
+ options.insert(options.end(), { "-p", to_string(ssh_port) });
+
+ if (!ssh_user.empty())
+ options.insert(options.end(), { "-l", ssh_user });
+
+ if (!ssh_identity.empty())
+ options.insert(options.end(), { "-i", ssh_identity });
+
+ return options;
+ }
+
+
+ vector<string>
+ read_backup_config_names()
+ {
+ const vector<string> filenames = glob(BACKUP_CONFIGS_DIR "/" "*.json", 0);
+ const size_t l1 = strlen(BACKUP_CONFIGS_DIR "/");
+ const size_t l2 = strlen(".json");
+
+ vector<string> names;
+
+ for (const string& filename : filenames)
+ names.push_back(filename.substr(l1, filename.size() - l1 - l2));
+
+ return names;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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_BACKUP_CONFIG_H
+#define SNAPPER_BACKUP_CONFIG_H
+
+
+#include <string>
+#include <vector>
+
+#include <snapper/Enum.h>
+
+#include "Shell.h"
+
+
+namespace snapper
+{
+
+ using std::string;
+ using std::vector;
+
+
+ class BackupConfig
+ {
+ public:
+
+ enum class TargetMode
+ {
+ LOCAL, SSH_PUSH
+ };
+
+ BackupConfig(const string& name);
+
+ const string name;
+
+ string config;
+
+ TargetMode target_mode = TargetMode::LOCAL;
+
+ string source_path;
+ string target_path;
+
+ bool automatic = false;
+
+ string ssh_host;
+ unsigned int ssh_port = 0;
+ string ssh_user;
+ string ssh_identity;
+
+ Shell get_source_shell() const;
+ Shell get_target_shell() const;
+
+ private:
+
+ vector<string> ssh_options() const;
+
+ };
+
+
+ using BackupConfigs = vector<BackupConfig>;
+
+
+ template <> struct EnumInfo<BackupConfig::TargetMode> { static const vector<string> names; };
+
+
+ vector<string>
+ read_backup_config_names();
+
+}
+
+
+#endif
--- /dev/null
+/*
+ * Copyright (c) [2004-2015] Novell, Inc.
+ * Copyright (c) [2017-2024] 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 <cstring>
+#include <regex>
+#include <boost/algorithm/string.hpp>
+
+#include "snapper/SnapperTmpl.h"
+#include "snapper/SystemCmd.h"
+#include "snapper/SnapperDefines.h"
+#include "snapper/Exception.h"
+#include "snapper/Log.h"
+#include "CmdBtrfs.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ CmdBtrfsSubvolumeList::CmdBtrfsSubvolumeList(const Shell& shell, const string& mount_point)
+ {
+ SystemCmd::Args cmd_args = { BTRFS_BIN, "subvolume", "list", "-a", "-puqR", mount_point };
+ SystemCmd cmd(shellify(shell, cmd_args));
+
+ if (cmd.retcode() != 0)
+ {
+ y2err("command '" << cmd.cmd() << "' failed: " << cmd.retcode());
+ for (const string& tmp : cmd.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception("'btrfs subvolume list' failed"));
+ }
+
+ parse(cmd.get_stdout());
+ }
+
+
+ void
+ CmdBtrfsSubvolumeList::parse(const vector<string>& lines)
+ {
+ for (const string& line : lines)
+ {
+ Entry entry;
+
+ string::size_type pos1 = line.find("ID ");
+ if (pos1 == string::npos)
+ SN_THROW(Exception("could not find 'id' in 'btrfs subvolume list' output"));
+ line.substr(pos1 + strlen("ID ")) >> entry.id;
+
+ string::size_type pos2 = line.find(" parent ");
+ if (pos2 == string::npos)
+ SN_THROW(Exception("could not find 'parent' in 'btrfs subvolume list' output"));
+ line.substr(pos2 + strlen(" parent ")) >> entry.parent_id;
+
+ // Subvolume can already be deleted, in which case parent is "0"
+ // (and path "DELETED"). That is a temporary state.
+ if (entry.parent_id == 0)
+ continue;
+
+ string::size_type pos3 = line.find(" path ");
+ if (pos3 == string::npos)
+ SN_THROW(Exception("could not find 'path' in 'btrfs subvolume list' output"));
+ entry.path = line.substr(pos3 + strlen(" path "));
+ if (boost::starts_with(entry.path, "<FS_TREE>/"))
+ entry.path.erase(0, strlen("<FS_TREE>/"));
+
+ string::size_type pos4 = line.find(" uuid ");
+ if (pos4 == string::npos)
+ SN_THROW(Exception("could not find 'uuid' in 'btrfs subvolume list' output"));
+ line.substr(pos4 + strlen(" uuid ")) >> entry.uuid;
+
+ string::size_type pos5 = line.find(" parent_uuid ");
+ if (pos5 == string::npos)
+ SN_THROW(Exception("could not find 'parent_uuid' in 'btrfs subvolume list' output"));
+ line.substr(pos5 + strlen(" parent_uuid ")) >> entry.parent_uuid;
+ if (entry.parent_uuid == "-")
+ entry.parent_uuid = "";
+
+ string::size_type pos6 = line.find(" received_uuid ");
+ if (pos6 == string::npos)
+ SN_THROW(Exception("could not find 'received_uuid' in 'btrfs subvolume list' output"));
+ line.substr(pos6 + strlen(" received_uuid ")) >> entry.parent_uuid;
+ if (entry.received_uuid == "-")
+ entry.received_uuid = "";
+
+ data.push_back(entry);
+ }
+
+ // No way to get read-only flag. Only showing read-only snapshots does not help.
+
+ y2mil(*this);
+ }
+
+
+ CmdBtrfsSubvolumeList::const_iterator
+ CmdBtrfsSubvolumeList::find_entry_by_path(const string& path) const
+ {
+ for (const_iterator it = data.begin(); it != data.end(); ++it)
+ {
+ if (it->path == path)
+ return it;
+ }
+
+ return data.end();
+ }
+
+
+ std::ostream&
+ operator<<(std::ostream& s, const CmdBtrfsSubvolumeList& cmd_btrfs_subvolume_list)
+ {
+ for (const CmdBtrfsSubvolumeList::Entry& entry : cmd_btrfs_subvolume_list)
+ s << entry;
+
+ return s;
+ }
+
+
+ std::ostream&
+ operator<<(std::ostream& s, const CmdBtrfsSubvolumeList::Entry& entry)
+ {
+ s << "id:" << entry.id << " parent-id:" << entry.parent_id
+ << " path:" << entry.path << " uuid:" << entry.uuid;
+
+ if (!entry.parent_uuid.empty())
+ s << " parent-uuid:" << entry.parent_uuid;
+
+ if (!entry.received_uuid.empty())
+ s << " received-uuid:" << entry.received_uuid;
+
+ s << '\n';
+
+ return s;
+ }
+
+
+ CmdBtrfsSubvolumeShow::CmdBtrfsSubvolumeShow(const Shell& shell, const string& mount_point)
+ {
+ SystemCmd::Args cmd_args = { BTRFS_BIN, "subvolume", "show", mount_point };
+ SystemCmd cmd(shellify(shell, cmd_args));
+
+ if (cmd.retcode() != 0)
+ {
+ y2err("command '" << cmd.cmd() << "' failed: " << cmd.retcode());
+ for (const string& tmp : cmd.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception("'btrfs subvolume show' failed"));
+ }
+
+ parse(cmd.get_stdout());
+ }
+
+
+ void
+ CmdBtrfsSubvolumeShow::parse(const vector<string>& lines)
+ {
+ static const regex uuid_regex("[ \t]*UUID:[ \t]*(" UUID_REGEX "|-)[ \t]*", regex::extended);
+ static const regex parent_uuid_regex("[ \t]*Parent UUID:[ \t]*(" UUID_REGEX "|-)[ \t]*", regex::extended);
+ static const regex received_uuid_regex("[ \t]*Received UUID:[ \t]*(" UUID_REGEX "|-)[ \t]*", regex::extended);
+ static const regex creation_time_regex("[ \t]*Creation time:[ \t]*([-+0-9: ]+)[ \t]*", regex::extended);
+ static const regex flags_regex("[ \t]*Flags:[ \t]*(" "readonly" "|-)[ \t]*", regex::extended);
+
+ smatch match;
+
+ for (const string& line : lines)
+ {
+ if (regex_match(line, match, uuid_regex))
+ uuid = match[1];
+
+ if (regex_match(line, match, parent_uuid_regex))
+ {
+ if (match[1] != "-")
+ parent_uuid = match[1];
+ }
+
+ if (regex_match(line, match, received_uuid_regex))
+ {
+ if (match[1] != "-")
+ received_uuid = match[1];
+ }
+
+ if (regex_match(line, match, creation_time_regex))
+ creation_time = match[1];
+
+ // so far readonly is the only flag so unclear whether several flags will be
+ // separated by space or comma
+
+ if (regex_match(line, match, flags_regex))
+ read_only = match[1] == "readonly";
+ }
+
+ if (uuid.empty())
+ SN_THROW(Exception("could not find 'uuid' in 'btrfs subvolume show' output"));
+
+ if (uuid == "-")
+ {
+ // If the btrfs was created with older kernels (whatever that means) (tested
+ // with SLES 11 SP3), the top-level subvolume does not have a UUID. Other
+ // subvolumes do have a UUID. In that case also all subvolumes are listed
+ // wrongly as snapshots of the top-level subvolume (by 'btrfs subvolume
+ // show'). The relationship between other subvolumes/snapshots seems to be
+ // fine.
+
+ y2mil("could not find 'uuid' in 'btrfs subvolume show' output - happens if "
+ "btrfs was created with an old kernel");
+
+ uuid = "";
+ }
+
+ y2mil(*this);
+ }
+
+
+ std::ostream&
+ operator<<(std::ostream& s, const CmdBtrfsSubvolumeShow& cmd_btrfs_subvolume_show)
+ {
+ s << "uuid:" << cmd_btrfs_subvolume_show.uuid;
+
+ if (!cmd_btrfs_subvolume_show.parent_uuid.empty())
+ s << " parent-uuid:" << cmd_btrfs_subvolume_show.parent_uuid;
+
+ if (!cmd_btrfs_subvolume_show.received_uuid.empty())
+ s << " received-uuid:" << cmd_btrfs_subvolume_show.received_uuid;
+
+ if (!cmd_btrfs_subvolume_show.creation_time.empty())
+ s << " creation-time:" << cmd_btrfs_subvolume_show.creation_time;
+
+ if (cmd_btrfs_subvolume_show.read_only)
+ s << " read-only";
+
+ s << '\n';
+
+ return s;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) [2004-2015] Novell, Inc.
+ * Copyright (c) [2017-2024] 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_CMD_BTRFS_H
+#define SNAPPER_CMD_BTRFS_H
+
+
+#include "Shell.h"
+
+
+namespace snapper
+{
+
+ using std::string;
+ using std::vector;
+
+
+ /**
+ * Class to probe for btrfs subvolumes: Call "btrfs subvolume list
+ * <mount-point>".
+ */
+ class CmdBtrfsSubvolumeList
+ {
+ public:
+
+ static const long top_level_id = 5;
+ static const long unknown_id = -1;
+
+ CmdBtrfsSubvolumeList(const Shell& shell, const string& mount_point);
+
+ /**
+ * Entry for every subvolume (unfortunately except the top-level).
+ *
+ * Caution: parent_id and parent_uuid are something completely
+ * different - not just different ways to specify the
+ * "parent".
+ */
+ struct Entry
+ {
+ long id = unknown_id;
+ long parent_id = unknown_id;
+ string path;
+ string uuid;
+ string parent_uuid;
+ string received_uuid;
+ };
+
+ typedef vector<Entry>::value_type value_type;
+ typedef vector<Entry>::const_iterator const_iterator;
+
+ const_iterator begin() const { return data.begin(); }
+ const_iterator end() const { return data.end(); }
+
+ const_iterator find_entry_by_path(const string& path) const;
+
+ friend std::ostream& operator<<(std::ostream& s, const CmdBtrfsSubvolumeList& cmd_btrfs_subvolume_list);
+ friend std::ostream& operator<<(std::ostream& s, const Entry& entry);
+
+ private:
+
+ void parse(const vector<string>& lines);
+
+ vector<Entry> data;
+
+ };
+
+
+ /**
+ * Class to probe for btrfs subvolume information: Call "btrfs subvolume
+ * show <mount-point>".
+ */
+ class CmdBtrfsSubvolumeShow
+ {
+ public:
+
+ CmdBtrfsSubvolumeShow(const Shell& shell, const string& mount_point);
+
+ const string& get_uuid() const { return uuid; }
+ const string& get_parent_uuid() const { return parent_uuid; }
+ const string& get_received_uuid() const { return received_uuid; }
+ const string& get_creation_time() const { return creation_time; }
+ bool is_read_only() const { return read_only; }
+
+ friend std::ostream& operator<<(std::ostream& s, const CmdBtrfsSubvolumeShow&
+ cmd_btrfs_subvolume_show);
+
+ private:
+
+ void parse(const vector<string>& lines);
+
+ string uuid;
+ string parent_uuid;
+ string received_uuid;
+ string creation_time; // TODO should be time_t
+ bool read_only = false;
+
+ };
+
+}
+
+#endif
--- /dev/null
+/*
+ * Copyright (c) 2024 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 "CmdFindmnt.h"
+#include "JsonFile.h"
+
+#include "snapper/SystemCmd.h"
+#include "snapper/SnapperDefines.h"
+#include "snapper/Exception.h"
+#include "snapper/Log.h"
+
+
+namespace snapper
+{
+
+ CmdFindmnt::CmdFindmnt(const Shell& shell, const string& path)
+ : path(path)
+ {
+ SystemCmd::Args cmd_args = { FINDMNT_BIN, "--json", "--target", path };
+ SystemCmd cmd(shellify(shell, cmd_args));
+
+ if (cmd.retcode() != 0)
+ {
+ y2err("command '" << cmd.cmd() << "' failed: " << cmd.retcode());
+ for (const string& tmp : cmd.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception("'findmnt' failed"));
+ }
+
+ parse_json(cmd.get_stdout());
+ }
+
+
+ void
+ CmdFindmnt::parse_json(const vector<string>& lines)
+ {
+ JsonFile json_file(lines);
+
+ vector<json_object*> tmp1;
+
+ if (!get_child_nodes(json_file.get_root(), "filesystems", tmp1))
+ SN_THROW(Exception("\"filesystems\" not found in json output of 'findmnt'"));
+
+ for (json_object* tmp2 : tmp1)
+ {
+ if (!get_child_value(tmp2, "source", source))
+ SN_THROW(Exception("\"source\" not found or invalid"));
+
+ if (!get_child_value(tmp2, "target", target))
+ SN_THROW(Exception("\"target\" not found or invalid"));
+ }
+
+ y2mil(*this);
+ }
+
+
+ std::ostream&
+ operator<<(std::ostream& s, const CmdFindmnt& cmd_findmnt)
+ {
+ s << "path:" << cmd_findmnt.path << " source:" << cmd_findmnt.source
+ << " target:" << cmd_findmnt.target;
+
+ return s;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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_CMD_FINDMNT_H
+#define SNAPPER_CMD_FINDMNT_H
+
+
+#include "Shell.h"
+
+
+namespace snapper
+{
+
+ using std::string;
+ using std::vector;
+
+
+ /**
+ * Class to probe for mount points: Call "findmnt --target <path>".
+ */
+ class CmdFindmnt
+ {
+ public:
+
+ CmdFindmnt(const Shell& shell, const string& path);
+
+ const string& get_source() const { return source; }
+ const string& get_target() const { return target; }
+
+ friend std::ostream& operator<<(std::ostream& s, const CmdFindmnt& cmd_findmnt);
+
+ private:
+
+ void parse_json(const vector<string>& lines);
+
+ const string path;
+
+ string source;
+ string target;
+
+ };
+
+}
+
+#endif
--- /dev/null
+/*
+ * Copyright (c) 2024 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 "CmdRealpath.h"
+
+#include "snapper/SystemCmd.h"
+#include "snapper/SnapperDefines.h"
+#include "snapper/Exception.h"
+#include "snapper/Log.h"
+
+
+namespace snapper
+{
+
+ CmdRealpath::CmdRealpath(const Shell& shell, const string& path)
+ : path(path)
+ {
+ SystemCmd::Args cmd_args = { REALPATH_BIN, path };
+ SystemCmd cmd(shellify(shell, cmd_args));
+
+ if (cmd.retcode() != 0)
+ {
+ y2err("command '" << cmd.cmd() << "' failed: " << cmd.retcode());
+ for (const string& tmp : cmd.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception("'realpath' failed"));
+ }
+
+ parse(cmd.get_stdout());
+ }
+
+
+ void
+ CmdRealpath::parse(const vector<string>& lines)
+ {
+ if (lines.size() != 1)
+ SN_THROW(Exception("failed to parse output of 'realpath'"));
+
+ realpath = lines[0];
+
+ y2mil(*this);
+ }
+
+
+ std::ostream&
+ operator<<(std::ostream& s, const CmdRealpath& cmd_realpath)
+ {
+ s << "path:" << cmd_realpath.path << " realpath:" << cmd_realpath.realpath;
+
+ return s;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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_CMD_REALPATH_H
+#define SNAPPER_CMD_REALPATH_H
+
+
+#include "Shell.h"
+
+
+namespace snapper
+{
+
+ using std::string;
+ using std::vector;
+
+
+ /**
+ * Class to probe realpath: Call "realpath <path>".
+ */
+ class CmdRealpath
+ {
+ public:
+
+ CmdRealpath(const Shell& shell, const string& path);
+
+ const string& get_realpath() const { return realpath; }
+
+ friend std::ostream& operator<<(std::ostream& s, const CmdRealpath& cmd_realpath);
+
+ private:
+
+ void parse(const vector<string>& lines);
+
+ const string path;
+
+ string realpath;
+
+ };
+
+}
+
+#endif
--- /dev/null
+/*
+ * Copyright (c) [2019-2024] 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 "../utils/help.h"
+#include "../misc.h"
+#include "client/utils/text.h"
+#include "client/utils/TableFormatter.h"
+#include "client/utils/CsvFormatter.h"
+
+#include "GlobalOptions.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ void
+ GlobalOptions::help_global_options()
+ {
+ cout << " " << _("Global options:") << '\n';
+
+ print_options({
+ { _("--quiet, -q"), _("Suppress normal output.") },
+ { _("--verbose, -v"), _("Increase verbosity.") },
+ { _("--debug"), _("Turn on debugging.") },
+ { _("--target-mode <target-mode>"), _("Only use backup-config with specified target-mode.") },
+ { _("--automatic"), _("Only use backup-config with automatic set.") },
+ { _("--utc"), _("Display dates and times in UTC.") },
+ { _("--iso"), _("Display dates and times in ISO format.") },
+ { _("--table-style, -t <style>"), _("Table style (integer).") },
+ { _("--machine-readable <format>"), _("Set a machine-readable output format (csv, json).") },
+ { _("--csvout"), _("Set CSV output format.") },
+ { _("--jsonout"), _("Set JSON output format.") },
+ { _("--separator <separator>"), _("Character separator for CSV output format.") },
+ { _("--no-headers"), _("No headers for CSV output format.") },
+ { _("--backup-config, -b <name>"), _("Set name of backup-config to use.") },
+ { _("--no-dbus"), _("Operate without DBus.") },
+ { _("--version"), _("Print version and exit.") }
+ });
+ }
+
+
+ GlobalOptions::GlobalOptions(GetOpts& parser)
+ {
+ const vector<Option> options = {
+ Option("quiet", no_argument, 'q'),
+ Option("verbose", no_argument, 'v'),
+ Option("debug", no_argument),
+ Option("target-mode", required_argument),
+ Option("automatic", no_argument),
+ Option("utc", no_argument),
+ Option("iso", no_argument),
+ Option("table-style", required_argument, 't'),
+ Option("machine-readable", required_argument),
+ Option("csvout", no_argument),
+ Option("jsonout", no_argument),
+ Option("separator", required_argument),
+ Option("no-headers", no_argument),
+ Option("backup-config", required_argument, 'b'),
+ Option("no-dbus", no_argument),
+ Option("version", no_argument),
+ Option("help", no_argument, 'h')
+ };
+
+ ParsedOpts opts = parser.parse(options);
+
+ check_options(opts);
+
+ _quiet = opts.has_option("quiet");
+ _verbose = opts.has_option("verbose");
+ _debug = opts.has_option("debug");
+ _target_mode = only_target_mode_value(opts);
+ _automatic = opts.has_option("automatic");
+ _utc = opts.has_option("utc");
+ _iso = opts.has_option("iso");
+ _no_dbus = opts.has_option("no-dbus");
+ _version = opts.has_option("version");
+ _help = opts.has_option("help");
+ _table_style = table_style_value(opts);
+ _output_format = output_format_value(opts);
+ _headers = !opts.has_option("no-headers");
+ _separator = separator_value(opts);
+ _backup_config = backup_config_value(opts);
+ }
+
+
+ void
+ GlobalOptions::check_options(const ParsedOpts& opts) const
+ {
+ }
+
+
+ boost::optional<BackupConfig::TargetMode>
+ GlobalOptions::only_target_mode_value(const ParsedOpts& opts) const
+ {
+ ParsedOpts::const_iterator it = opts.find("target-mode");
+ if (it == opts.end())
+ return boost::optional<BackupConfig::TargetMode>();
+
+ BackupConfig::TargetMode target_mode;
+
+ if (!toValue(it->second, target_mode, false))
+ {
+ string error = sformat(_("Invalid target mode '%s'."), it->second.c_str()) + '\n';
+ SN_THROW(OptionsException(error));
+ }
+
+ return target_mode;
+ }
+
+
+ Style
+ GlobalOptions::table_style_value(const ParsedOpts& opts) const
+ {
+ ParsedOpts::const_iterator it = opts.find("table-style");
+ if (it == opts.end())
+ return TableFormatter::auto_style();
+
+ try
+ {
+ unsigned long value = stoul(it->second);
+
+ if (value >= Table::num_styles)
+ throw exception();
+
+ return (Style)(value);
+ }
+ catch (const exception&)
+ {
+ string error = sformat(_("Invalid table style '%s'."), it->second.c_str()) + '\n' +
+ sformat(_("Use an integer number from %d to %d."), 0, Table::num_styles - 1);
+
+ SN_THROW(OptionsException(error));
+ }
+
+ return TableFormatter::auto_style();
+ }
+
+
+ GlobalOptions::OutputFormat
+ GlobalOptions::output_format_value(const ParsedOpts& opts) const
+ {
+ if (opts.has_option("csvout"))
+ return OutputFormat::CSV;
+
+ if (opts.has_option("jsonout"))
+ return OutputFormat::JSON;
+
+ ParsedOpts::const_iterator it = opts.find("machine-readable");
+ if (it == opts.end())
+ return OutputFormat::TABLE;
+
+ OutputFormat output_format;
+ if (!toValue(it->second, output_format, false))
+ {
+ string error = sformat(_("Invalid machine readable format '%s'."), it->second.c_str()) + '\n' +
+ possible_enum_values<OutputFormat>();
+
+ SN_THROW(OptionsException(error));
+ }
+
+ return output_format;
+ }
+
+
+ string
+ GlobalOptions::separator_value(const ParsedOpts& opts) const
+ {
+ ParsedOpts::const_iterator it = opts.find("separator");
+ if (it == opts.end())
+ return CsvFormatter::default_separator;
+
+ return it->second;
+ }
+
+
+ boost::optional<string>
+ GlobalOptions::backup_config_value(const ParsedOpts& opts) const
+ {
+ ParsedOpts::const_iterator it = opts.find("backup-config");
+ if (it == opts.end())
+ return boost::optional<string>();
+
+ return it->second;
+ }
+
+
+ const vector<string> EnumInfo<GlobalOptions::OutputFormat>::names({ "table", "csv", "json" });
+
+}
--- /dev/null
+/*
+ * Copyright (c) [2019-2024] 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_GLOBAL_OPTIONS_H
+#define SNAPPER_GLOBAL_OPTIONS_H
+
+
+#include <string>
+#include <boost/optional.hpp>
+
+#include <snapper/Enum.h>
+
+#include "client/utils/GetOpts.h"
+#include "client/utils/Table.h"
+#include "BackupConfig.h"
+
+
+namespace snapper
+{
+
+ class GlobalOptions
+ {
+
+ public:
+
+ enum class OutputFormat { TABLE, CSV, JSON };
+
+ static void help_global_options();
+
+ GlobalOptions(GetOpts& get_opts);
+
+ bool quiet() const { return _quiet; }
+ bool verbose() const { return _verbose; }
+ bool debug() const { return _debug; }
+ boost::optional<BackupConfig::TargetMode> target_mode() { return _target_mode; }
+ bool automatic() const { return _automatic; }
+ bool utc() const { return _utc; }
+ bool iso() const { return _iso; }
+ bool no_dbus() const { return _no_dbus; }
+ bool version() const { return _version; }
+ bool help() const { return _help; }
+ Style table_style() const { return _table_style; }
+ OutputFormat output_format() const { return _output_format; }
+ string separator() const { return _separator; }
+ bool headers() const { return _headers; }
+ boost::optional<string> backup_config() const { return _backup_config; }
+
+ private:
+
+ void check_options(const ParsedOpts& parsed_opts) const;
+
+ boost::optional<BackupConfig::TargetMode> only_target_mode_value(const ParsedOpts& parsed_opts) const;
+ Style table_style_value(const ParsedOpts& parsed_opts) const;
+ OutputFormat output_format_value(const ParsedOpts& parsed_opts) const;
+ string separator_value(const ParsedOpts& parsed_opts) const;
+ boost::optional<string> backup_config_value(const ParsedOpts& parsed_opts) const;
+
+ bool _quiet;
+ bool _verbose;
+ bool _debug;
+ boost::optional<BackupConfig::TargetMode> _target_mode;
+ bool _automatic;
+ bool _utc;
+ bool _iso;
+ bool _no_dbus;
+ bool _version;
+ bool _help;
+ Style _table_style;
+ OutputFormat _output_format;
+ string _separator;
+ bool _headers;
+ boost::optional<string> _backup_config;
+
+ };
+
+
+ template <> struct EnumInfo<GlobalOptions::OutputFormat> { static const vector<string> names; };
+
+}
+
+#endif
--- /dev/null
+/*
+ * Copyright (c) [2017-2024] 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 <stdio.h>
+#include <sys/stat.h>
+#include <functional>
+#include <memory>
+
+#include "snapper/Exception.h"
+#include "snapper/AppUtil.h"
+
+#include "JsonFile.h"
+
+
+namespace snapper
+{
+
+ class JsonTokener
+ {
+ public:
+
+ JsonTokener()
+ : p(json_tokener_new())
+ {
+ if (!p)
+ SN_THROW(Exception("out of memory"));
+ }
+
+ ~JsonTokener()
+ {
+ json_tokener_free(p);
+ }
+
+ json_tokener* get() { return p; }
+
+ private:
+
+ json_tokener* p;
+
+ };
+
+
+ JsonFile::JsonFile(const vector<string>& lines)
+ {
+ JsonTokener tokener;
+
+ for (const string& line : lines)
+ {
+ root = json_tokener_parse_ex(tokener.get(), line.c_str(), line.size());
+
+ switch (json_tokener_get_error(tokener.get()))
+ {
+ case json_tokener_continue:
+ continue;
+
+ case json_tokener_success:
+ return;
+
+ default:
+ break;
+ }
+ }
+
+ SN_THROW(Exception("json parser failed"));
+ }
+
+
+ JsonFile::JsonFile(const string& filename)
+ {
+ FILE* fp = fopen(filename.c_str(), "r");
+ if (!fp)
+ SN_THROW(Exception(sformat("open for json file '%s' failed", filename.c_str())));
+
+ struct stat st;
+ if (fstat(fileno(fp), &st) != 0)
+ {
+ fclose(fp);
+ SN_THROW(Exception(sformat("stat for json file '%s' failed", filename.c_str())));
+ }
+
+ vector<char> data(st.st_size);
+ if (fread(data.data(), 1, st.st_size, fp) != (size_t)(st.st_size))
+ {
+ fclose(fp);
+ SN_THROW(Exception(sformat("read for json file '%s' failed", filename.c_str())));
+ }
+
+ if (fclose(fp) != 0)
+ SN_THROW(Exception(sformat("close for json file '%s' failed", filename.c_str())));
+
+ JsonTokener tokener;
+
+ root = json_tokener_parse_ex(tokener.get(), data.data(), st.st_size);
+
+ if (json_tokener_get_error(tokener.get()) != json_tokener_success)
+ {
+ json_object_put(root);
+ SN_THROW(Exception(sformat("parsing json file '%s' failed", filename.c_str())));
+ }
+
+ if (tokener.get()->char_offset != st.st_size)
+ {
+ json_object_put(root);
+ SN_THROW(Exception(sformat("excessive content in json file '%s'", filename.c_str())));
+ }
+ }
+
+
+ JsonFile::~JsonFile()
+ {
+ json_object_put(root);
+ }
+
+
+ template<>
+ bool
+ get_child_value(json_object* parent, const char* name, string& value)
+ {
+ json_object* child;
+
+ if (!json_object_object_get_ex(parent, name, &child))
+ return false;
+
+ if (!json_object_is_type(child, json_type_string))
+ return false;
+
+ value = json_object_get_string(child);
+
+ return true;
+ }
+
+
+ template<>
+ bool
+ get_child_value(json_object* parent, const char* name, bool& value)
+ {
+ json_object* child;
+
+ if (!json_object_object_get_ex(parent, name, &child))
+ return false;
+
+ if (!json_object_is_type(child, json_type_boolean))
+ return false;
+
+ value = json_object_get_boolean(child);
+
+ return true;
+ }
+
+
+ template<>
+ bool
+ get_child_value(json_object* parent, const char* name, unsigned int& value)
+ {
+ static_assert(sizeof(unsigned int) <= 4, "unsigned int wider than 32 bit");
+
+ json_object* child;
+
+ if (!json_object_object_get_ex(parent, name, &child))
+ return false;
+
+ if (!json_object_is_type(child, json_type_int) && !json_object_is_type(child, json_type_string))
+ return false;
+
+ value = json_object_get_int(child);
+
+ return true;
+ }
+
+
+ bool
+ get_child_nodes(json_object* parent, const char* name, vector<json_object*>& children)
+ {
+ json_object* child;
+
+ if (!json_object_object_get_ex(parent, name, &child))
+ return false;
+
+ if (!json_object_is_type(child, json_type_array))
+ return false;
+
+ children.clear();
+
+ size_t s = json_object_array_length(child);
+ for (size_t i = 0; i < s; ++i)
+ children.push_back(json_object_array_get_idx(child, i));
+
+ return true;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) [2017-2024] 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_JSON_FILE_H
+#define SNAPPER_JSON_FILE_H
+
+
+#include <json-c/json.h>
+#include <string>
+#include <vector>
+#include <boost/noncopyable.hpp>
+
+
+namespace snapper
+{
+
+ using std::string;
+ using std::vector;
+
+
+ /*
+ * The user might expect more use of const here but in the end it does not work out
+ * since json_object_get_string does not take a const (likely due to reference
+ * counting).
+ */
+
+
+ class JsonFile : private boost::noncopyable
+ {
+
+ public:
+
+ JsonFile(const vector<string>& lines);
+
+ JsonFile(const string& filename);
+
+ ~JsonFile();
+
+ json_object* get_root() { return root; }
+
+ private:
+
+ json_object* root = nullptr;
+
+ };
+
+
+ template<typename Type> bool
+ get_child_value(json_object* parent, const char* name, Type& value);
+
+
+ bool
+ get_child_nodes(json_object* parent, const char* name, vector<json_object*>& children);
+
+}
+
+
+#endif
--- /dev/null
+#
+# Makefile.am for snapper/client/snbk
+#
+
+AM_CPPFLAGS = -I$(top_srcdir) $(DBUS_CFLAGS) $(JSON_C_CFLAGS)
+
+sbin_PROGRAMS = snbk
+
+snbk_SOURCES = \
+ snbk.cc \
+ cmd.h \
+ cmd-list-configs.cc \
+ cmd-list.cc \
+ cmd-transfer.cc \
+ cmd-delete.cc \
+ cmd-transfer-and-delete.cc \
+ BackupConfig.cc BackupConfig.h \
+ TheBigThing.cc TheBigThing.h \
+ GlobalOptions.cc GlobalOptions.h \
+ Shell.cc Shell.h \
+ CmdBtrfs.cc CmdBtrfs.h \
+ CmdFindmnt.cc CmdFindmnt.h \
+ CmdRealpath.cc CmdRealpath.h \
+ JsonFile.cc JsonFile.h
+
+snbk_LDADD = \
+ ../../snapper/libsnapper.la \
+ ../../dbus/libdbus.la \
+ ../proxy/libproxy.la \
+ ../proxy/libclient.la \
+ $(JSON_C_LIBS)
--- /dev/null
+/*
+ * Copyright (c) 2024 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 "snapper/SnapperDefines.h"
+#include "snapper/Exception.h"
+
+#include "Shell.h"
+
+
+namespace snapper
+{
+
+ namespace
+ {
+
+ // TODO move to libsnapper?
+
+ string
+ quote(const vector<string>& strs)
+ {
+ string ret;
+
+ for (vector<string>::const_iterator it = strs.begin(); it != strs.end(); ++it)
+ {
+ if (it != strs.begin())
+ ret.append(" ");
+ ret.append(SystemCmd::quote(*it));
+ }
+
+ return ret;
+ }
+
+
+ string
+ quote(const SystemCmd::Args& args)
+ {
+ return quote(args.get_values());
+ }
+
+ }
+
+
+ SystemCmd::Args
+ shellify(const Shell& shell, const SystemCmd::Args& args1)
+ {
+ switch (shell.mode)
+ {
+ case Shell::Mode::DIRECT:
+ {
+ return args1;
+ }
+
+ case Shell::Mode::SSH:
+ {
+ SystemCmd::Args args2 = { SSH_BIN };
+ args2 << shell.ssh_options << quote(args1);
+ return args2;
+ }
+ }
+
+ SN_THROW(Exception("invalid shell mode"));
+ __builtin_unreachable();
+ }
+
+
+ SystemCmd::Args
+ shellify_pipe(const SystemCmd::Args& args1, const Shell& shell2, const SystemCmd::Args& args2)
+ {
+ string tmp1 = quote(args1);
+ string tmp2 = quote(args2);
+
+ switch (shell2.mode)
+ {
+ case Shell::Mode::DIRECT:
+ {
+ return SystemCmd::Args { SH_BIN, "-c", tmp1 + " | " + tmp2 };
+ }
+
+ case Shell::Mode::SSH:
+ {
+ return SystemCmd::Args { SH_BIN, "-c", tmp1 + " | " + SSH_BIN " " +
+ quote(shell2.ssh_options) + " " + quote(tmp2) };
+ }
+ }
+
+ SN_THROW(Exception("invalid shell mode"));
+ __builtin_unreachable();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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_SHELL_H
+#define SNAPPER_SHELL_H
+
+
+#include <string>
+#include <vector>
+
+#include "snapper/SystemCmd.h"
+
+
+namespace snapper
+{
+
+ using std::string;
+ using std::vector;
+
+
+ struct Shell
+ {
+ enum class Mode
+ {
+ DIRECT, SSH
+ };
+
+ Mode mode = Mode::DIRECT;
+ vector<string> ssh_options;
+ };
+
+
+ SystemCmd::Args
+ shellify(const Shell& shell, const SystemCmd::Args& args);
+
+
+ SystemCmd::Args
+ shellify_pipe(const SystemCmd::Args& args1, const Shell& shell2, const SystemCmd::Args& args2);
+
+}
+
+#endif
--- /dev/null
+/*
+ * Copyright (c) 2024 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 <iostream>
+#include <regex>
+#include <boost/algorithm/string/predicate.hpp>
+
+#include "snapper/SystemCmd.h"
+#include "snapper/SnapperDefines.h"
+#include "snapper/SnapperTmpl.h"
+
+#include "../proxy/proxy.h"
+#include "../utils/text.h"
+
+#include "CmdBtrfs.h"
+#include "CmdFindmnt.h"
+#include "CmdRealpath.h"
+#include "BackupConfig.h"
+#include "TheBigThing.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ const vector<string> EnumInfo<TheBigThing::SourceState>::names({
+ "missing", "read-only", "read-write"
+ });
+
+
+ const vector<string> EnumInfo<TheBigThing::TargetState>::names({
+ "missing", "valid", "invalid"
+ });
+
+
+ void
+ TheBigThing::transfer(const BackupConfig& backup_config, const TheBigThings& the_big_things,
+ bool quiet)
+ {
+ if (!quiet)
+ cout << sformat(_("Transferring snapshot %d."), num) << '\n';
+
+ if (source_state == SourceState::MISSING)
+ SN_THROW(Exception(_("Snapshot not on source.")));
+
+ if (target_state != TargetState::MISSING)
+ SN_THROW(Exception(_("Snapshot already on target.")));
+
+ // Create directory on target.
+
+ SystemCmd::Args cmd1_args = { MKDIR_BIN, backup_config.target_path + "/" + to_string(num) };
+ SystemCmd cmd1(shellify(backup_config.get_target_shell(), cmd1_args));
+ if (cmd1.retcode() != 0)
+ {
+ y2err("command '" << cmd1.cmd() << "' failed: " << cmd1.retcode());
+ for (const string& tmp : cmd1.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd1.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception(_("'mkdir' failed.")));
+ }
+
+ // Copy info.xml to target.
+
+ switch (backup_config.target_mode)
+ {
+ case BackupConfig::TargetMode::LOCAL:
+ {
+ SystemCmd::Args cmd2_args = { CP_BIN, backup_config.source_path + "/" SNAPSHOTS_NAME "/" +
+ to_string(num) + "/info.xml", backup_config.target_path + "/" + to_string(num) + "/" };
+ SystemCmd cmd2(shellify(backup_config.get_target_shell(), cmd2_args));
+ if (cmd2.retcode() != 0)
+ {
+ y2err("command '" << cmd2.cmd() << "' failed: " << cmd2.retcode());
+ for (const string& tmp : cmd2.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd2.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception(_("'cp info.xml' failed.")));
+ }
+ }
+ break;
+
+ case BackupConfig::TargetMode::SSH_PUSH:
+ {
+ SystemCmd::Args cmd2_args = { SCP_BIN };
+ if (backup_config.ssh_port != 0)
+ cmd2_args << "-P" << to_string(backup_config.ssh_port);
+ if (!backup_config.ssh_identity.empty())
+ cmd2_args << "-i" << backup_config.ssh_identity;
+ cmd2_args << backup_config.source_path + "/" SNAPSHOTS_NAME "/" + to_string(num) + "/info.xml"
+ << (backup_config.ssh_user.empty() ? "" : "a") + backup_config.ssh_host + ":" +
+ backup_config.target_path + "/" + to_string(num) + "/";
+ SystemCmd cmd2(cmd2_args);
+ if (cmd2.retcode() != 0)
+ {
+ y2err("command '" << cmd2.cmd() << "' failed: " << cmd2.retcode());
+ for (const string& tmp : cmd2.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd2.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception(_("'scp info.xml' failed.")));
+ }
+ }
+ break;
+ }
+
+ // Copy snapshot to target.
+
+ TheBigThings::const_iterator it1 = the_big_things.find_send_parent(*this);
+
+ SystemCmd::Args cmd3a_args = { BTRFS_BIN, "send" };
+ if (it1 != the_big_things.end())
+ cmd3a_args << "-p" << backup_config.source_path + "/" SNAPSHOTS_NAME "/" +
+ to_string(it1->num) + "/" SNAPSHOT_NAME;
+ cmd3a_args << backup_config.source_path + "/" SNAPSHOTS_NAME "/" + to_string(num) + "/" SNAPSHOT_NAME;
+
+ SystemCmd::Args cmd3b_args = { BTRFS_BIN, "receive", backup_config.target_path + "/" + to_string(num) };
+
+ y2deb("source: " << cmd3a_args.get_values());
+ y2deb("target: " << cmd3b_args.get_values());
+
+ SystemCmd cmd3(shellify_pipe(cmd3a_args, backup_config.get_target_shell(), cmd3b_args));
+ if (cmd3.retcode() != 0)
+ {
+ y2err("command '" << cmd3.cmd() << "' failed: " << cmd3.retcode());
+ for (const string& tmp : cmd3.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd3.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception(_("'btrfs send | btrfs receive' failed.")));
+ }
+
+ target_state = TargetState::VALID;
+ }
+
+
+ void
+ TheBigThing::remove(const BackupConfig& backup_config, bool quiet)
+ {
+ if (!quiet)
+ cout << sformat(_("Deleting snapshot %d."), num) << '\n';
+
+ if (target_state == TargetState::MISSING)
+ SN_THROW(Exception(_("Snapshot not on target.")));
+
+ // Delete snapshot on target.
+
+ SystemCmd::Args cmd1_args = { BTRFS_BIN, "subvolume", "delete", backup_config.target_path + "/" +
+ to_string(num) + "/" SNAPSHOT_NAME };
+ SystemCmd cmd1(shellify(backup_config.get_target_shell(), cmd1_args));
+ if (cmd1.retcode() != 0)
+ {
+ y2err("command '" << cmd1.cmd() << "' failed: " << cmd1.retcode());
+ for (const string& tmp : cmd1.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd1.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception(_("'btrfs subvolume delete' failed.")));
+ }
+
+ // Remove info.xml on target.
+
+ SystemCmd::Args cmd2_args = { RM_BIN, backup_config.target_path + "/" + to_string(num) + "/info.xml" };
+ SystemCmd cmd2(shellify(backup_config.get_target_shell(), cmd2_args));
+ if (cmd2.retcode() != 0)
+ {
+ y2err("command '" << cmd2.cmd() << "' failed: " << cmd2.retcode());
+ for (const string& tmp : cmd2.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd2.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception(_("'rm info.xml' failed.")));
+ }
+
+ // Remove directory on target.
+
+ SystemCmd::Args cmd3_args = { RMDIR_BIN, backup_config.target_path + "/" + to_string(num) };
+ SystemCmd cmd3(shellify(backup_config.get_target_shell(), cmd3_args));
+ if (cmd3.retcode() != 0)
+ {
+ y2err("command '" << cmd3.cmd() << "' failed: " << cmd3.retcode());
+ for (const string& tmp : cmd3.get_stdout())
+ y2err(tmp);
+ for (const string& tmp : cmd3.get_stderr())
+ y2err(tmp);
+
+ SN_THROW(Exception(_("'rmdir' failed.")));
+ }
+
+ target_state = TargetState::MISSING;
+ }
+
+
+ bool
+ operator<(const TheBigThing& lhs, const TheBigThing& rhs)
+ {
+ return lhs.num < rhs.num;
+ }
+
+
+ TheBigThings::TheBigThings(const BackupConfig& backup_config, ProxySnappers* snappers, bool verbose)
+ : snapper(snappers->getSnapper(backup_config.config)), locker(snapper)
+ {
+ if (backup_config.source_path != snapper->getConfig().getSubvolume())
+ SN_THROW(Exception(_("Path mismatch between backup-config and config.")));
+
+ probe_source(backup_config, verbose);
+ probe_target(backup_config, verbose);
+
+ sort(the_big_things.begin(), the_big_things.end());
+ }
+
+
+ void
+ TheBigThings::probe_source(const BackupConfig& backup_config, bool verbose)
+ {
+ const Shell shell_source = backup_config.get_source_shell();
+
+ // Query snapshots on source from snapperd.
+
+ if (verbose)
+ cout << _("Probing source snapshots.") << endl;
+
+ if (backup_config.source_path != snapper->getConfig().getSubvolume())
+ SN_THROW(Exception(_("Path mismatch.")));
+
+ const ProxySnapshots& source_snapshots = snapper->getSnapshots();
+
+ if (verbose)
+ cout << _("Probing extra information for source snapshots.") << endl;
+
+ for (const ProxySnapshot& source_snapshot : source_snapshots)
+ {
+ unsigned int num = source_snapshot.getNum();
+ if (num == 0)
+ continue;
+
+ // Query additional information (uuids, read-only) from btrfs.
+
+ CmdBtrfsSubvolumeShow extra(shell_source, backup_config.source_path + "/" SNAPSHOTS_NAME "/" +
+ to_string(num) + "/" SNAPSHOT_NAME);
+
+ TheBigThing the_big_thing(num);
+ the_big_thing.date = source_snapshot.getDate();
+ the_big_thing.source_state = extra.is_read_only() ? TheBigThing::SourceState::READ_ONLY :
+ TheBigThing::SourceState::READ_WRITE;
+ the_big_thing.source_uuid = extra.get_uuid();
+ the_big_thing.source_parent_uuid = extra.get_parent_uuid();
+ the_big_thing.source_received_uuid = extra.get_received_uuid();
+ the_big_thing.source_creation_time = extra.get_creation_time();
+
+ the_big_things.push_back(the_big_thing);
+ }
+ }
+
+
+ void
+ TheBigThings::probe_target(const BackupConfig& backup_config, bool verbose)
+ {
+ const Shell shell_target = backup_config.get_target_shell();
+
+ // Query snapshots on target from btrfs.
+
+ if (verbose)
+ cout << _("Probing target snapshots.") << endl;
+
+ // In case the target-path is a symbolic link (or includes things like "/../") we
+ // need a lookup for the realpath first.
+
+ CmdRealpath cmd_realpath(shell_target, backup_config.target_path);
+ const string target_path = cmd_realpath.get_realpath();
+
+ CmdFindmnt cmd_findmnt(shell_target, target_path);
+ const string mount_point = cmd_findmnt.get_target();
+
+ if (target_path.size() < mount_point.size())
+ SN_THROW(Exception("unsupported target-path setup"));
+
+ if (!boost::starts_with(target_path, mount_point))
+ SN_THROW(Exception("unsupported target-path setup"));
+
+ CmdBtrfsSubvolumeList target_snapshots(shell_target, mount_point);
+
+ string start;
+ if (target_path != mount_point)
+ start = target_path.substr(mount_point.size() + 1) + "/";
+
+ if (verbose)
+ cout << _("Probing extra information for target snapshots.") << endl;
+
+ static const regex num_regex("([0-9]+)/snapshot", regex::extended);
+
+ for (const CmdBtrfsSubvolumeList::Entry& target_snapshot : target_snapshots)
+ {
+ if (!boost::starts_with(target_snapshot.path, start))
+ continue;
+
+ string path = target_snapshot.path.substr(start.size());
+
+ smatch match;
+
+ if (!regex_match(path, match, num_regex))
+ {
+ string error = sformat(_("Invalid subvolume path '%s' on target."), path.c_str());
+ SN_THROW(Exception(error));
+ }
+
+ unsigned int num = stoi(match[1]);
+
+ // Query additional information (receive-uuid, read-only) from btrfs.
+
+ CmdBtrfsSubvolumeShow y(shell_target, target_path + "/" + path);
+
+ bool is_read_only = y.is_read_only();
+ if (!is_read_only)
+ {
+ y2deb(num << " not read-only, maybe interrupted transfer");
+ }
+
+ vector<TheBigThing>::iterator it = find(num);
+ if (it != end())
+ {
+ // Wrong receive-uuid can happen when a snapshots is transferred, then removed
+ // and a new one with the same number is generated.
+
+ // When a snapshot is restored using btrfs send and receive the received
+ // uuid of the source is identical to the received uuid of the target -
+ // not the uuid of the target. In that case the target is also valid.
+
+ bool correct_uuid = false;
+
+ if (!y.get_received_uuid().empty())
+ {
+ if (it->source_uuid == y.get_received_uuid())
+ correct_uuid = true;
+ else if (it->source_received_uuid == y.get_received_uuid())
+ correct_uuid = true;
+
+ if (!correct_uuid)
+ {
+ y2deb(num << " wrong uuid, maybe snapshot number reuse");
+ }
+ }
+
+ if (correct_uuid && is_read_only)
+ it->target_state = TheBigThing::TargetState::VALID;
+ else
+ it->target_state = TheBigThing::TargetState::INVALID;
+ }
+ else
+ {
+ TheBigThing the_big_thing(num);
+
+ // Cannot check received-uuid so assume valid.
+
+ if (is_read_only)
+ the_big_thing.target_state = TheBigThing::TargetState::VALID;
+ else
+ the_big_thing.target_state = TheBigThing::TargetState::INVALID;
+
+ it = the_big_things.insert(the_big_things.end(), the_big_thing);
+ }
+
+ it->target_uuid = y.get_uuid();
+ it->target_parent_uuid = y.get_parent_uuid();
+ it->target_received_uuid = y.get_received_uuid();
+ it->target_creation_time = y.get_creation_time();
+ }
+ }
+
+
+ void
+ TheBigThings::transfer(const BackupConfig& backup_config, bool quiet, bool verbose)
+ {
+ for (TheBigThing& the_big_thing : the_big_things)
+ {
+ if (the_big_thing.source_state == TheBigThing::SourceState::READ_ONLY)
+ {
+ if (the_big_thing.target_state == TheBigThing::TargetState::INVALID)
+ {
+ the_big_thing.remove(backup_config, quiet);
+ }
+
+ if (the_big_thing.target_state == TheBigThing::TargetState::MISSING)
+ {
+ the_big_thing.transfer(backup_config, *this, quiet);
+ }
+ }
+ }
+ }
+
+
+ void
+ TheBigThings::remove(const BackupConfig& backup_config, bool quiet, bool verbose)
+ {
+ for (TheBigThing& the_big_thing : the_big_things)
+ {
+ if (the_big_thing.target_state == TheBigThing::TargetState::INVALID)
+ {
+ the_big_thing.remove(backup_config, quiet);
+ }
+
+ if (the_big_thing.source_state == TheBigThing::SourceState::MISSING &&
+ the_big_thing.target_state != TheBigThing::TargetState::MISSING)
+ {
+ the_big_thing.remove(backup_config, quiet);
+ }
+ }
+ }
+
+
+ vector<TheBigThing>::iterator
+ TheBigThings::find(unsigned int num)
+ {
+ return find_if(begin(), end(), [num](const TheBigThing& the_big_thing) {
+ return the_big_thing.num == num;
+ });
+ }
+
+
+ TheBigThings::const_iterator
+ TheBigThings::find_send_parent(const TheBigThing& the_big_thing) const
+ {
+ typedef vector<TheBigThing>::const_reverse_iterator const_reverse_iterator;
+
+ // Find the direct parent or a previous snapshots with the same parent UUID (more
+ // a sibling).
+
+ for (const_reverse_iterator it1 = the_big_things.rbegin(); it1 != the_big_things.rend(); ++it1)
+ {
+ if (it1->num >= the_big_thing.num)
+ continue;
+
+ if (it1->source_state != TheBigThing::SourceState::READ_ONLY ||
+ it1->target_state != TheBigThing::TargetState::VALID)
+ continue;
+
+ if (it1->source_uuid == the_big_thing.source_parent_uuid)
+ // base() is a bit surprising, compensate that
+ return (it1 + 1).base();
+
+ if (it1->source_parent_uuid == the_big_thing.source_parent_uuid)
+ return (it1 + 1).base();
+ }
+
+ // Find the direct parent of the direct parent. This case can happen after a
+ // rollback.
+
+ for (const_reverse_iterator it1 = the_big_things.rbegin(); it1 != the_big_things.rend(); ++it1)
+ {
+ if (it1->num >= the_big_thing.num)
+ continue;
+
+ // Here the direct parent itself might be read-write and thus not available on
+ // the target at all.
+
+ if (it1->source_uuid == the_big_thing.source_parent_uuid)
+ {
+ for (const_reverse_iterator it2 = the_big_things.rbegin(); it2 != the_big_things.rend(); ++it2)
+ {
+ if (it2->num >= it1->num)
+ continue;
+
+ if (it2->source_state != TheBigThing::SourceState::READ_ONLY ||
+ it2->target_state != TheBigThing::TargetState::VALID)
+ continue;
+
+ if (it2->source_uuid == it1->source_parent_uuid)
+ return (it2 + 1).base();
+ }
+ }
+ }
+
+ return end();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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_THE_BIG_THING_H
+#define SNAPPER_THE_BIG_THING_H
+
+
+#include <string>
+#include <vector>
+
+#include "../proxy/proxy.h"
+#include "../proxy/locker.h"
+
+
+namespace snapper
+{
+
+ using std::string;
+ using std::vector;
+
+
+ class BackupConfig;
+ class TheBigThings;
+
+
+ class TheBigThing
+ {
+ public:
+
+ // snapshots on target are always read-only if valid
+
+ enum class SourceState { MISSING, READ_ONLY, READ_WRITE };
+ enum class TargetState { MISSING, VALID, INVALID };
+
+ TheBigThing(unsigned int num) : num(num) {}
+
+ void transfer(const BackupConfig& backup_config, const TheBigThings& the_big_things, bool quiet);
+
+ void remove(const BackupConfig& backup_config, bool quiet);
+
+ unsigned int num;
+ time_t date = 0; // as reported by snapper
+
+ SourceState source_state = SourceState::MISSING;
+ TargetState target_state = TargetState::MISSING;
+
+ string source_uuid;
+ string source_parent_uuid;
+ string source_received_uuid;
+ string source_creation_time;
+
+ string target_uuid;
+ string target_parent_uuid;
+ string target_received_uuid;
+ string target_creation_time;
+
+ };
+
+
+ template <> struct EnumInfo<TheBigThing::SourceState> { static const vector<string> names; };
+
+ template <> struct EnumInfo<TheBigThing::TargetState> { static const vector<string> names; };
+
+
+ class TheBigThings
+ {
+ public:
+
+ /**
+ * Queries the snapshots on the source and target. Also gets a ProxySnapper and
+ * locks it.
+ */
+ TheBigThings(const BackupConfig& backup_config, ProxySnappers* snappers, bool verbose);
+
+ void transfer(const BackupConfig& backup_config, bool quiet, bool verbose);
+
+ void remove(const BackupConfig& backup_config, bool quiet, bool verbose);
+
+ typedef vector<TheBigThing>::iterator iterator;
+ typedef vector<TheBigThing>::const_iterator const_iterator;
+
+ iterator begin() { return the_big_things.begin(); }
+ const_iterator begin() const { return the_big_things.begin(); }
+
+ iterator end() { return the_big_things.end(); }
+ const_iterator end() const { return the_big_things.end(); }
+
+ iterator find(unsigned int num);
+
+ /**
+ * Detect a suitable parent for btrfs send. Return end() iff none is found.
+ */
+ const_iterator find_send_parent(const TheBigThing& the_big_thing) const;
+
+ private:
+
+ const ProxySnapper* snapper;
+ const Locker locker;
+
+ vector<TheBigThing> the_big_things;
+
+ void probe_source(const BackupConfig& backup_config, bool verbose);
+ void probe_target(const BackupConfig& backup_config, bool verbose);
+
+ };
+
+}
+
+#endif
--- /dev/null
+/*
+ * Copyright (c) 2024 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 <iostream>
+#include <regex>
+
+#include <snapper/AppUtil.h>
+
+#include "../proxy/errors.h"
+#include "../utils/text.h"
+
+#include "BackupConfig.h"
+#include "GlobalOptions.h"
+#include "TheBigThing.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ void
+ help_delete()
+ {
+ cout << " " << _("Delete:") << '\n'
+ << "\t" << _("snbk delete [numbers]") << '\n'
+ << '\n';
+ }
+
+
+ void
+ command_delete(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers)
+ {
+ static const regex num_regex("[0-9]+", regex::extended);
+
+ ParsedOpts opts = get_opts.parse("delete", GetOpts::no_options);
+
+ vector<unsigned int> nums;
+
+ while (get_opts.has_args())
+ {
+ string arg = get_opts.pop_arg();
+
+ if (!regex_match(arg, num_regex))
+ SN_THROW(Exception(_("Failed to parse number.")));
+
+ nums.push_back(stoi(arg));
+ }
+
+ unsigned int errors = 0;
+
+ for (const BackupConfig& backup_config : backup_configs)
+ {
+ if (!global_options.quiet())
+ cout << sformat(_("Running delete for backup config '%s'."),
+ backup_config.name.c_str()) << endl;
+
+ try
+ {
+ TheBigThings the_big_things(backup_config, snappers, global_options.verbose());
+
+ if (nums.empty())
+ {
+ the_big_things.remove(backup_config, global_options.quiet(), global_options.quiet());
+ }
+ else
+ {
+ for (unsigned int num : nums)
+ {
+ TheBigThings::iterator it = the_big_things.find(num);
+ if (it == the_big_things.end())
+ {
+ string error = sformat(_("Snapshot number %d not found."), num);
+ SN_THROW(Exception(error));
+ }
+
+ it->remove(backup_config, global_options.quiet());
+ }
+ }
+ }
+ catch (const DBus::ErrorException& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << error_description(e) << endl;
+
+ ++errors;
+ }
+ catch (const Exception& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << e.what() << '\n';
+
+ cerr << sformat(_("Running delete for backup config '%s' failed."),
+ backup_config.name.c_str()) << endl;
+
+ ++errors;
+ }
+ }
+
+ if (errors != 0)
+ {
+ string error = sformat(_("Running delete failed for %d of %ld backup config.",
+ "Running delete failed for %d of %ld backup configs.",
+ backup_configs.size()), errors, backup_configs.size());
+
+ SN_THROW(Exception(error));
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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 <iostream>
+
+#include "../utils/text.h"
+#include "../utils/TableFormatter.h"
+#include "../utils/CsvFormatter.h"
+#include "../utils/JsonFormatter.h"
+#include "../utils/OutputOptions.h"
+#include "../proxy/proxy.h"
+
+#include "BackupConfig.h"
+#include "GlobalOptions.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ void
+ help_list_configs()
+ {
+ cout << " " << _("List configs:") << '\n'
+ << "\t" << _("snbk list-configs") << '\n'
+ << '\n';
+ }
+
+
+ namespace
+ {
+
+ enum class Column
+ {
+ NAME, CONFIG, TARGET_MODE, AUTOMATIC, SOURCE_PATH, TARGET_PATH, SSH_HOST,
+ SSH_USER, SSH_PORT, SSH_IDENTITY
+ };
+
+
+ Cell
+ header_for(Column column)
+ {
+ switch (column)
+ {
+ case Column::NAME:
+ return Cell(_("Name"));
+
+ case Column::CONFIG:
+ return Cell(_("Config"));
+
+ case Column::TARGET_MODE:
+ return Cell(_("Target Mode"));
+
+ case Column::AUTOMATIC:
+ return Cell(_("Automatic"));
+
+ case Column::SOURCE_PATH:
+ return Cell(_("Source Path"));
+
+ case Column::TARGET_PATH:
+ return Cell(_("Target Path"));
+
+ case Column::SSH_HOST:
+ return Cell(_("SSH Host"), Id::SSH_HOST);
+
+ case Column::SSH_USER:
+ return Cell(_("SSH User"), Id::SSH_USER);
+
+ case Column::SSH_PORT:
+ return Cell(_("SSH Port"), Id::SSH_PORT);
+
+ case Column::SSH_IDENTITY:
+ return Cell(_("SSH Identity"), Id::SSH_IDENTITY);
+ }
+
+ SN_THROW(Exception("invalid column value"));
+ __builtin_unreachable();
+ }
+
+
+ boost::any
+ value_for_as_any(Column column, const BackupConfig& backup_config)
+ {
+ switch (column)
+ {
+ case Column::NAME:
+ return backup_config.name;
+
+ case Column::CONFIG:
+ return backup_config.config;
+
+ case Column::TARGET_MODE:
+ return toString(backup_config.target_mode);
+
+ case Column::AUTOMATIC:
+ return backup_config.automatic;
+
+ case Column::SOURCE_PATH:
+ return backup_config.source_path;
+
+ case Column::TARGET_PATH:
+ return backup_config.target_path;
+
+ case Column::SSH_HOST:
+ if (backup_config.ssh_host.empty())
+ return nullptr;
+ return backup_config.ssh_host;
+
+ case Column::SSH_USER:
+ if (backup_config.ssh_user.empty())
+ return nullptr;
+ return backup_config.ssh_user;
+
+ case Column::SSH_PORT:
+ if (backup_config.ssh_port == 0)
+ return nullptr;
+ return to_string(backup_config.ssh_port);
+
+ case Column::SSH_IDENTITY:
+ if (backup_config.ssh_identity.empty())
+ return nullptr;
+ return backup_config.ssh_identity;
+ }
+
+ SN_THROW(Exception("invalid column value"));
+ __builtin_unreachable();
+ }
+
+
+ string
+ value_for_as_string(const OutputOptions& output_options, Column column, const BackupConfig& backup_config)
+ {
+ return any_to_string(output_options, value_for_as_any(column, backup_config));
+ }
+
+
+ json_object*
+ value_for_as_json(const OutputOptions& output_options, Column column, const BackupConfig& backup_config)
+ {
+ return any_to_json(output_options, value_for_as_any(column, backup_config));
+ }
+
+
+ void
+ output_table(const GlobalOptions& global_options, const vector<Column>& columns,
+ const BackupConfigs& backup_configs)
+ {
+ OutputOptions output_options(global_options.utc(), global_options.iso(), true);
+
+ TableFormatter formatter(global_options.table_style());
+
+ for (Column column : columns)
+ formatter.header().push_back(header_for(column));
+
+ formatter.auto_visibility().push_back(Id::SSH_HOST);
+ formatter.auto_visibility().push_back(Id::SSH_USER);
+ formatter.auto_visibility().push_back(Id::SSH_PORT);
+ formatter.auto_visibility().push_back(Id::SSH_IDENTITY);
+
+ for (const BackupConfig& backup_config : backup_configs)
+ {
+ vector<string> row;
+
+ for (Column column : columns)
+ row.push_back(value_for_as_string(output_options, column, backup_config));
+
+ formatter.rows().push_back(row);
+ }
+
+ cout << formatter;
+ }
+
+
+ void
+ output_csv(const GlobalOptions& global_options, const vector<Column>& columns,
+ const BackupConfigs& backup_configs)
+ {
+ OutputOptions output_options(global_options.utc(), global_options.iso(), false);
+
+ CsvFormatter formatter(global_options.separator(), global_options.headers());
+
+ for (Column column : columns)
+ formatter.header().push_back(toString(column));
+
+ for (const BackupConfig& backup_config : backup_configs)
+ {
+ vector<string> row;
+
+ for (Column column : columns)
+ row.push_back(value_for_as_string(output_options, column, backup_config));
+
+ formatter.rows().push_back(row);
+ }
+
+ cout << formatter;
+ }
+
+
+ void
+ output_json(const GlobalOptions& global_options, const vector<Column>& columns,
+ const BackupConfigs& backup_configs)
+ {
+ OutputOptions output_options(global_options.utc(), global_options.iso(), false);
+
+ JsonFormatter formatter;
+
+ json_object* json_backup_configs = json_object_new_array();
+ json_object_object_add(formatter.root(), "backup-configs", json_backup_configs);
+
+ for (const BackupConfig& backup_config : backup_configs)
+ {
+ json_object* json_backup_config = json_object_new_object();
+ json_object_array_add(json_backup_configs, json_backup_config);
+
+ for (const Column& column : columns)
+ {
+ json_object* tmp = value_for_as_json(output_options, column, backup_config);
+ if (tmp)
+ json_object_object_add(json_backup_config, toString(column).c_str(), tmp);
+ }
+ }
+
+ cout << formatter;
+ }
+
+ }
+
+
+ void
+ command_list_configs(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers)
+ {
+ ParsedOpts opts = get_opts.parse("list-configs", GetOpts::no_options);
+
+ vector<Column> columns = { Column::NAME, Column::CONFIG, Column::TARGET_MODE,
+ Column::AUTOMATIC, Column::SOURCE_PATH, Column::TARGET_PATH, Column::SSH_HOST,
+ Column::SSH_USER, Column::SSH_PORT, Column::SSH_IDENTITY };
+
+ if (get_opts.has_args())
+ {
+ SN_THROW(OptionsException(_("Command 'list-configs' does not take arguments.")));
+ }
+
+ switch (global_options.output_format())
+ {
+ case GlobalOptions::OutputFormat::TABLE:
+ output_table(global_options, columns, backup_configs);
+ break;
+
+ case GlobalOptions::OutputFormat::CSV:
+ output_csv(global_options, columns, backup_configs);
+ break;
+
+ case GlobalOptions::OutputFormat::JSON:
+ output_json(global_options, columns, backup_configs);
+ break;
+ }
+ }
+
+
+ template <> struct EnumInfo<Column> { static const vector<string> names; };
+
+ const vector<string> EnumInfo<Column>::names({
+ "name", "config", "target-mode", "automatic", "source-path", "target-path", "ssh-host",
+ "ssh-user", "ssh-port", "ssh-identity"
+ });
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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 <iostream>
+
+#include "snapper/AppUtil.h"
+#include "snapper/SnapperDefines.h"
+
+#include "../utils/text.h"
+#include "../utils/TableFormatter.h"
+#include "../utils/CsvFormatter.h"
+#include "../utils/JsonFormatter.h"
+#include "../utils/OutputOptions.h"
+
+// a collision with client/proxy/errors.h
+#ifdef error_description
+#undef error_description
+#endif
+
+#include "../proxy/proxy.h"
+#include "../proxy/errors.h"
+
+#include "BackupConfig.h"
+#include "GlobalOptions.h"
+#include "TheBigThing.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ void
+ help_list()
+ {
+ cout << " " << _("List:") << '\n'
+ << "\t" << _("snbk list") << '\n'
+ << '\n';
+ }
+
+
+ namespace
+ {
+
+ enum class Column
+ {
+ NAME, NUMBER, DATE, SOURCE_STATE, SOURCE_UUID, SOURCE_PARENT_UUID, SOURCE_RECEIVED_UUID,
+ SOURCE_CREATION_TIME, TARGET_STATE, TARGET_UUID, TARGET_PARENT_UUID, TARGET_RECEIVED_UUID,
+ TARGET_CREATION_TIME
+ };
+
+
+ Cell
+ header_for(Column column)
+ {
+ switch (column)
+ {
+ case Column::NAME:
+ return Cell(_("Name"));
+
+ case Column::NUMBER:
+ return Cell(_("#"), Id::NUMBER, Align::RIGHT);
+
+ case Column::DATE:
+ return Cell(_("Date"));
+
+ case Column::SOURCE_STATE:
+ return Cell(_("Source State"));
+
+ case Column::SOURCE_UUID:
+ return Cell(_("(Source) UUID"));
+
+ case Column::SOURCE_PARENT_UUID:
+ return Cell(_("(Source) Parent UUID"));
+
+ case Column::SOURCE_RECEIVED_UUID:
+ return Cell(_("(Source) Received UUID"));
+
+ case Column::SOURCE_CREATION_TIME:
+ return Cell(_("(Source) Creation Time"));
+
+ case Column::TARGET_STATE:
+ return Cell(_("Target State"));
+
+ case Column::TARGET_UUID:
+ return Cell(_("(Target) UUID"));
+
+ case Column::TARGET_PARENT_UUID:
+ return Cell(_("(Target) Parent UUID"));
+
+ case Column::TARGET_RECEIVED_UUID:
+ return Cell(_("(Target) Received UUID"));
+
+ case Column::TARGET_CREATION_TIME:
+ return Cell(_("(Target) Creation Time"));
+ }
+
+ SN_THROW(Exception("invalid column value"));
+ __builtin_unreachable();
+ }
+
+
+ boost::any
+ value_for_as_any(const OutputOptions& output_options, Column column, const BackupConfig& backup_config,
+ const TheBigThing& the_big_thing)
+ {
+ const string::size_type uuid_cutoff = 14;
+
+ switch (column)
+ {
+ case Column::NAME:
+ return backup_config.name;
+
+ case Column::NUMBER:
+ return the_big_thing.num;
+
+ case Column::DATE:
+ if (the_big_thing.date == 0)
+ return nullptr;
+ return datetime(the_big_thing.date, output_options.utc, output_options.iso);
+
+ case Column::SOURCE_STATE:
+ if (the_big_thing.source_state == TheBigThing::SourceState::MISSING)
+ return nullptr;
+ return toString(the_big_thing.source_state);
+
+ case Column::SOURCE_UUID:
+ if (the_big_thing.source_uuid.empty())
+ return nullptr;
+ return the_big_thing.source_uuid.substr(0, uuid_cutoff);
+
+ case Column::SOURCE_PARENT_UUID:
+ if (the_big_thing.source_parent_uuid.empty())
+ return nullptr;
+ return the_big_thing.source_parent_uuid.substr(0, uuid_cutoff);
+
+ case Column::SOURCE_RECEIVED_UUID:
+ if (the_big_thing.source_received_uuid.empty())
+ return nullptr;
+ return the_big_thing.source_received_uuid.substr(0, uuid_cutoff);
+
+ case Column::SOURCE_CREATION_TIME:
+ return the_big_thing.source_creation_time;
+
+ case Column::TARGET_STATE:
+ if (the_big_thing.target_state == TheBigThing::TargetState::MISSING)
+ return nullptr;
+ return toString(the_big_thing.target_state);
+
+ case Column::TARGET_UUID:
+ if (the_big_thing.target_uuid.empty())
+ return nullptr;
+ return the_big_thing.target_uuid.substr(0, uuid_cutoff);
+
+ case Column::TARGET_PARENT_UUID:
+ if (the_big_thing.target_parent_uuid.empty())
+ return nullptr;
+ return the_big_thing.target_parent_uuid.substr(0, uuid_cutoff);
+
+ case Column::TARGET_RECEIVED_UUID:
+ if (the_big_thing.target_received_uuid.empty())
+ return nullptr;
+ return the_big_thing.target_received_uuid.substr(0, uuid_cutoff);
+
+ case Column::TARGET_CREATION_TIME:
+ return the_big_thing.target_creation_time;
+ }
+
+ SN_THROW(Exception("invalid column value"));
+ __builtin_unreachable();
+ }
+
+
+ string
+ value_for_as_string(const OutputOptions& output_options, Column column, const BackupConfig& backup_config,
+ const TheBigThing& the_big_thing)
+ {
+ return any_to_string(output_options, value_for_as_any(output_options, column, backup_config,
+ the_big_thing));
+ }
+
+
+ json_object*
+ value_for_as_json(const OutputOptions& output_options, Column column, const BackupConfig& backup_config,
+ const TheBigThing& the_big_thing)
+ {
+ return any_to_json(output_options, value_for_as_any(output_options, column, backup_config,
+ the_big_thing));
+ }
+
+
+ // The loop over the backup-configs could be moved from the three output_*
+ // functions to command_list. One disadvantage is that the output for the table
+ // mode is delayed until all backup-configs have been probed.
+
+
+ void
+ output_table(const GlobalOptions& global_options, const vector<Column>& columns,
+ const BackupConfigs& backup_configs, ProxySnappers* snappers)
+ {
+ OutputOptions output_options(global_options.utc(), global_options.iso(), true);
+
+ unsigned int errors = 0;
+
+ bool first_table = true;
+
+ for (const BackupConfig& backup_config : backup_configs)
+ {
+ if (!first_table)
+ cout << endl;
+
+ if (backup_configs.size() > 1)
+ {
+ cout << "Backup-config:" << backup_config.name << ", config:" << backup_config.config
+ << ", source-path:" << backup_config.source_path << ", target-mode:"
+ << toString(backup_config.target_mode) << endl;
+ }
+
+ try
+ {
+ TheBigThings the_big_things(backup_config, snappers, global_options.verbose());
+
+ TableFormatter formatter(global_options.table_style());
+
+ for (Column column : columns)
+ formatter.header().push_back(header_for(column));
+
+ for (const TheBigThing& the_big_thing : the_big_things)
+ {
+ vector<string> row;
+
+ for (Column column : columns)
+ row.push_back(value_for_as_string(output_options, column, backup_config, the_big_thing));
+
+ formatter.rows().push_back(row);
+ }
+
+ first_table = false;
+
+ cout << formatter;
+ }
+ catch (const DBus::ErrorException& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << error_description(e) << endl;
+
+ ++errors;
+ }
+ catch (const Exception& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << e.what() << endl;
+
+ ++errors;
+ }
+ }
+
+ if (errors != 0)
+ {
+ string error = sformat(_("Running list failed for %d of %ld backup config.",
+ "Running list failed for %d of %ld backup configs.",
+ backup_configs.size()), errors, backup_configs.size());
+
+ SN_THROW(Exception(error));
+ }
+ }
+
+
+ void
+ output_csv(const GlobalOptions& global_options, const vector<Column>& columns,
+ const BackupConfigs& backup_configs, ProxySnappers* snappers)
+ {
+ OutputOptions output_options(global_options.utc(), global_options.iso(), false);
+
+ CsvFormatter formatter(global_options.separator(), global_options.headers());
+
+ for (Column column : columns)
+ formatter.header().push_back(toString(column));
+
+ for (const BackupConfig& backup_config : backup_configs)
+ {
+ TheBigThings the_big_things(backup_config, snappers, global_options.verbose());
+
+ for (const TheBigThing& the_big_thing : the_big_things)
+ {
+ vector<string> row;
+
+ for (Column column : columns)
+ row.push_back(value_for_as_string(output_options, column, backup_config, the_big_thing));
+
+ formatter.rows().push_back(row);
+ }
+ }
+
+ cout << formatter;
+ }
+
+
+ void
+ output_json(const GlobalOptions& global_options, const vector<Column>& columns,
+ const BackupConfigs& backup_configs, ProxySnappers* snappers)
+ {
+ OutputOptions output_options(global_options.utc(), global_options.iso(), false);
+
+ JsonFormatter formatter;
+
+ json_object* json_snapshots = json_object_new_array();
+ json_object_object_add(formatter.root(), "snapshots", json_snapshots);
+
+ for (const BackupConfig& backup_config : backup_configs)
+ {
+ TheBigThings the_big_things(backup_config, snappers, global_options.verbose());
+
+ for (const TheBigThing& the_big_thing : the_big_things)
+ {
+ json_object* json_snapshot = json_object_new_object();
+ json_object_array_add(json_snapshots, json_snapshot);
+
+ for (const Column& column : columns)
+ {
+ json_object* tmp = value_for_as_json(output_options, column, backup_config, the_big_thing);
+ if (tmp)
+ json_object_object_add(json_snapshot, toString(column).c_str(), tmp);
+ }
+ }
+ }
+
+ cout << formatter;
+ }
+
+ }
+
+
+ void
+ command_list(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers)
+ {
+ ParsedOpts opts = get_opts.parse("list", GetOpts::no_options);
+
+ vector<Column> columns = { Column::NUMBER, Column::DATE, Column::SOURCE_STATE, Column::TARGET_STATE };
+
+ if (global_options.output_format() != GlobalOptions::OutputFormat::TABLE)
+ columns.insert(columns.begin(), Column::NAME);
+
+ if (get_opts.has_args())
+ {
+ SN_THROW(OptionsException(_("Command 'list' does not take arguments.")));
+ }
+
+ switch (global_options.output_format())
+ {
+ case GlobalOptions::OutputFormat::TABLE:
+ output_table(global_options, columns, backup_configs, snappers);
+ break;
+
+ case GlobalOptions::OutputFormat::CSV:
+ output_csv(global_options, columns, backup_configs, snappers);
+ break;
+
+ case GlobalOptions::OutputFormat::JSON:
+ output_json(global_options, columns, backup_configs, snappers);
+ break;
+ }
+ }
+
+
+ template <> struct EnumInfo<Column> { static const vector<string> names; };
+
+ const vector<string> EnumInfo<Column>::names({
+ "name", "number", "date", "source-state", "source-uuid", "source-parent-uuid", "source-received-uuid",
+ "source-creation-time", "target-state", "target-uuid", "target-parent-uuid", "target-received-uuid",
+ "target-creation-time"
+ });
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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 <iostream>
+
+#include <snapper/AppUtil.h>
+
+#include "../proxy/errors.h"
+#include "../utils/text.h"
+
+#include "BackupConfig.h"
+#include "GlobalOptions.h"
+#include "TheBigThing.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ void
+ help_transfer_and_delete()
+ {
+ cout << " " << _("Transfer and delete:") << '\n'
+ << "\t" << _("snbk transfer-and-delete") << '\n'
+ << '\n';
+ }
+
+
+ void
+ command_transfer_and_delete(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers)
+ {
+ ParsedOpts opts = get_opts.parse("transfer-and-delete", GetOpts::no_options);
+
+ if (get_opts.has_args())
+ {
+ SN_THROW(OptionsException(_("Command 'transfer-and-delete' does not take arguments.")));
+ }
+
+ unsigned int errors = 0;
+
+ for (const BackupConfig& backup_config : backup_configs)
+ {
+ if (!global_options.quiet())
+ cout << sformat(_("Running transfer and delete for backup config '%s'."),
+ backup_config.name.c_str()) << endl;
+
+ try
+ {
+ TheBigThings the_big_things(backup_config, snappers, global_options.verbose());
+
+ the_big_things.transfer(backup_config, global_options.quiet(), global_options.quiet());
+ the_big_things.remove(backup_config, global_options.quiet(), global_options.quiet());
+ }
+ catch (const DBus::ErrorException& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << error_description(e) << endl;
+
+ ++errors;
+ }
+ catch (const Exception& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << e.what() << '\n';
+
+ cerr << sformat(_("Running transfer and delete for backup config '%s' failed."),
+ backup_config.name.c_str()) << endl;
+
+ ++errors;
+ }
+ }
+
+ if (errors != 0)
+ {
+ string error = sformat(_("Running transfer and delete failed for %d of %ld backup config.",
+ "Running transfer and delete failed for %d of %ld backup configs.",
+ backup_configs.size()), errors, backup_configs.size());
+
+ SN_THROW(Exception(error));
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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 <iostream>
+#include <regex>
+
+#include <snapper/AppUtil.h>
+
+#include "../proxy/errors.h"
+#include "../utils/text.h"
+
+#include "BackupConfig.h"
+#include "GlobalOptions.h"
+#include "TheBigThing.h"
+#include "cmd.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ void
+ help_transfer()
+ {
+ cout << " " << _("Transfer:") << '\n'
+ << "\t" << _("snbk transfer [numbers]") << '\n'
+ << '\n';
+ }
+
+
+ void
+ command_transfer(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers)
+ {
+ static const regex num_regex("[0-9]+", regex::extended);
+
+ ParsedOpts opts = get_opts.parse("transfer", GetOpts::no_options);
+
+ vector<unsigned int> nums;
+
+ while (get_opts.has_args())
+ {
+ string arg = get_opts.pop_arg();
+
+ if (!regex_match(arg, num_regex))
+ SN_THROW(Exception(_("Failed to parse number.")));
+
+ nums.push_back(stoi(arg));
+ }
+
+ unsigned int errors = 0;
+
+ for (const BackupConfig& backup_config : backup_configs)
+ {
+ if (!global_options.quiet())
+ cout << sformat(_("Running transfer for backup config '%s'."),
+ backup_config.name.c_str()) << endl;
+
+ try
+ {
+ TheBigThings the_big_things(backup_config, snappers, global_options.verbose());
+
+ if (nums.empty())
+ {
+ the_big_things.transfer(backup_config, global_options.quiet(), global_options.quiet());
+ }
+ else
+ {
+ for (unsigned int num : nums)
+ {
+ TheBigThings::iterator it = the_big_things.find(num);
+ if (it == the_big_things.end())
+ {
+ string error = sformat(_("Snapshot number %d not found."), num);
+ SN_THROW(Exception(error));
+ }
+
+ it->transfer(backup_config, the_big_things, global_options.quiet());
+ }
+ }
+ }
+ catch (const DBus::ErrorException& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << error_description(e) << endl;
+
+ ++errors;
+ }
+ catch (const Exception& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << e.what() << '\n';
+
+ cerr << sformat(_("Running transfer for backup config '%s' failed."),
+ backup_config.name.c_str()) << endl;
+
+ ++errors;
+ }
+ }
+
+ if (errors != 0)
+ {
+ string error = sformat(_("Running transfer failed for %d of %ld backup config.",
+ "Running transfer failed for %d of %ld backup configs.",
+ backup_configs.size()), errors, backup_configs.size());
+
+ SN_THROW(Exception(error));
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) [2011-2015] Novell, Inc.
+ * Copyright (c) [2016-2024] 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 "../proxy/proxy.h"
+
+
+namespace snapper
+{
+
+ void
+ help_list_configs();
+
+ void
+ command_list_configs(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers);
+
+
+ void
+ help_list();
+
+ void
+ command_list(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers);
+
+
+ void
+ help_transfer();
+
+ void
+ command_transfer(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers);
+
+
+ void
+ help_delete();
+
+ void
+ command_delete(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers);
+
+
+ void
+ help_transfer_and_delete();
+
+ void
+ command_transfer_and_delete(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
+ ProxySnappers* snappers);
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 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 <iostream>
+
+#include "snapper/Logger.h"
+#include <snapper/SnapperTmpl.h>
+#include <snapper/Enum.h>
+#include "snapper/SnapperDefines.h"
+
+#include "../utils/text.h"
+#include "../utils/GetOpts.h"
+
+#include "GlobalOptions.h"
+#include "BackupConfig.h"
+#include "cmd.h"
+
+
+using namespace std;
+using namespace snapper;
+
+
+struct Cmd
+{
+ typedef void (*cmd_func_t)(const GlobalOptions& global_options, GetOpts& get_opts,
+ BackupConfigs& backup_configs, ProxySnappers* snappers);
+
+ typedef void (*help_func_t)();
+
+ Cmd(const string& name, cmd_func_t cmd_func, help_func_t help_func, bool needs_snapper)
+ : name(name), cmd_func(cmd_func), help_func(help_func), needs_snapper(needs_snapper)
+ {}
+
+ Cmd(const string& name, const vector<string>& aliases, cmd_func_t cmd_func, help_func_t help_func,
+ bool needs_snapper)
+ : name(name), aliases(aliases), cmd_func(cmd_func), help_func(help_func),
+ needs_snapper(needs_snapper)
+ {}
+
+ const string name;
+ const vector<string> aliases;
+ const cmd_func_t cmd_func;
+ const help_func_t help_func;
+ const bool needs_snapper;
+};
+
+
+static bool log_debug = false;
+
+
+void
+log_do(LogLevel level, const string& component, const char* file, const int line, const char* func,
+ const string& text)
+{
+ cerr << text << endl;
+}
+
+
+bool
+log_query(LogLevel level, const string& component)
+{
+ return log_debug || level == ERROR;
+}
+
+
+void help() __attribute__ ((__noreturn__));
+
+
+void
+help(const vector<Cmd>& cmds, GetOpts& get_opts)
+{
+ get_opts.parse("help", GetOpts::no_options);
+ if (get_opts.has_args())
+ {
+ cerr << _("Command 'help' does not take arguments.") << endl;
+ exit(EXIT_FAILURE);
+ }
+
+ cout << _("usage: snbk [--global-options] <command> [command-arguments]") << '\n'
+ << endl;
+
+ GlobalOptions::help_global_options();
+
+ for (const Cmd& cmd : cmds)
+ (*cmd.help_func)();
+
+ exit(EXIT_SUCCESS);
+}
+
+
+vector<string>
+get_backup_configs(const GlobalOptions& global_options, const Cmd* cmd)
+{
+ if (cmd->name == "list-configs")
+ return read_backup_config_names();
+
+ if (global_options.backup_config())
+ return { global_options.backup_config().value() };
+
+ const vector<string> names = read_backup_config_names();
+ if (names.empty())
+ SN_THROW(Exception(_("No backup configs found.")));
+
+ return names;
+}
+
+
+int
+main(int argc, char** argv)
+{
+ try
+ {
+ locale::global(locale(""));
+ }
+ catch (const runtime_error& e)
+ {
+ cerr << _("Failed to set locale.") << endl;
+ }
+
+ setLogDo(&log_do);
+ setLogQuery(&log_query);
+
+ const vector<Cmd> cmds = {
+ Cmd("list-configs", command_list_configs, help_list_configs, false),
+ Cmd("list", { "ls" }, command_list, help_list, true),
+ Cmd("transfer", command_transfer, help_transfer, true),
+ Cmd("delete", { "remove", "rm" }, command_delete, help_delete, true),
+ Cmd("transfer-and-delete", command_transfer_and_delete, help_transfer_and_delete, true),
+ };
+
+ try
+ {
+ GetOpts get_opts(argc, argv);
+
+ GlobalOptions global_options(get_opts);
+
+ if (global_options.debug())
+ {
+ log_debug = true;
+ }
+
+ if (global_options.version())
+ {
+ cout << "snbk " << Snapper::compileVersion() << endl;
+ exit(EXIT_SUCCESS);
+ }
+
+ if (global_options.help())
+ {
+ help(cmds, get_opts);
+ }
+
+ if (!get_opts.has_args())
+ {
+ cerr << _("No command provided.") << endl
+ << _("Try 'snbk --help' for more information.") << endl;
+ exit(EXIT_FAILURE);
+ }
+
+ const char* command = get_opts.pop_arg();
+
+ vector<Cmd>::const_iterator cmd = cmds.begin();
+ while (cmd != cmds.end() && (cmd->name != command && !contains(cmd->aliases, command)))
+ ++cmd;
+
+ if (cmd == cmds.end())
+ {
+ cerr << sformat(_("Unknown command '%s'."), command) << endl
+ << _("Try 'snbk --help' for more information.") << endl;
+ exit(EXIT_FAILURE);
+ }
+
+ try
+ {
+ const vector<string> names = get_backup_configs(global_options, &*cmd);
+
+ BackupConfigs backup_configs;
+
+ for (const string& name : names)
+ {
+ BackupConfig backup_config(name);
+
+ if (global_options.target_mode() &&
+ backup_config.target_mode != global_options.target_mode().value())
+ continue;
+
+ if (global_options.automatic() && !backup_config.automatic)
+ continue;
+
+ backup_configs.push_back(backup_config);
+ }
+
+ unique_ptr<ProxySnappers> snappers;
+
+ if (cmd->needs_snapper)
+ {
+ snappers.reset(new ProxySnappers(global_options.no_dbus() ? ProxySnappers::createLib("/") :
+ ProxySnappers::createDbus()));
+ }
+
+ (*cmd->cmd_func)(global_options, get_opts, backup_configs, snappers.get());
+ }
+ catch (const Exception& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << e.what() << endl;
+
+ exit(EXIT_FAILURE);
+ }
+ }
+ catch (const OptionsException& e)
+ {
+ SN_CAUGHT(e);
+
+ cerr << e.what() << '\n'
+ << _("Try 'snbk --help' for more information.") << endl;
+
+ exit(EXIT_FAILURE);
+ }
+
+ exit(EXIT_SUCCESS);
+}
TableFormatter.cc TableFormatter.h \
CsvFormatter.cc CsvFormatter.h \
JsonFormatter.cc JsonFormatter.h \
- OutputOptions.h
+ OutputOptions.cc OutputOptions.h
libutils_la_LIBADD = ../../snapper/libsnapper.la -ltinfo
--- /dev/null
+/*
+ * Copyright (c) [2011-2015] Novell, Inc.
+ * Copyright (c) [2016-2024] 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 "OutputOptions.h"
+
+#include <snapper/Exception.h>
+
+#include "text.h"
+
+
+namespace snapper
+{
+
+ using namespace std;
+
+
+ string
+ any_to_string(const OutputOptions& output_options, const boost::any& value)
+ {
+ if (value.type() == typeid(nullptr_t))
+ {
+ return "";
+ }
+
+ if (value.type() == typeid(bool))
+ {
+ if (output_options.human)
+ return boost::any_cast<bool>(value) ? _("yes") : _("no");
+ else
+ return boost::any_cast<bool>(value) ? "yes" : "no";
+ }
+
+ if (value.type() == typeid(unsigned int))
+ {
+ return to_string(boost::any_cast<unsigned int>(value));
+ }
+
+ if (value.type() == typeid(string))
+ {
+ return boost::any_cast<string>(value).c_str();
+ }
+
+ SN_THROW(Exception("invalid type in any_to_string"));
+ __builtin_unreachable();
+ }
+
+
+ json_object*
+ any_to_json(const OutputOptions& output_options, const boost::any& value)
+ {
+ if (value.type() == typeid(nullptr_t))
+ {
+ return nullptr;
+ }
+
+ if (value.type() == typeid(bool))
+ {
+ return json_object_new_boolean(boost::any_cast<bool>(value));
+ }
+
+ if (value.type() == typeid(unsigned int))
+ {
+ return json_object_new_int(boost::any_cast<unsigned int>(value));
+ }
+
+ if (value.type() == typeid(string))
+ {
+ return json_object_new_string(boost::any_cast<string>(value).c_str());
+ }
+
+ SN_THROW(Exception("invalid type in any_to_json"));
+ __builtin_unreachable();
+ }
+
+}
*/
+#include <string>
+#include <boost/any.hpp>
+#include <json-c/json.h>
+
+
namespace snapper
{
+ using std::string;
+
+
/**
* Just a collection of some variables defining the output.
*/
};
+
+ // TODO extend functions and use in client/snapper
+
+ string
+ any_to_string(const OutputOptions& output_options, const boost::any& value);
+
+
+ json_object*
+ any_to_json(const OutputOptions& output_options, const boost::any& value);
+
}
enum class Id
{
- NONE, NAME, TYPE, NUMBER, PRE_NUMBER, POST_NUMBER, DESCRIPTION, USERDATA
+ NONE, NAME, TYPE, NUMBER, PRE_NUMBER, POST_NUMBER, DESCRIPTION, USERDATA, SSH_HOST, SSH_USER, SSH_PORT,
+ SSH_IDENTITY
};
client/mksubvolume/Makefile
client/installation-helper/Makefile
client/systemd-helper/Makefile
+ client/snbk/Makefile
scripts/Makefile
pam/Makefile
data/Makefile
doc/snapper.xml:doc/snapper.xml.in
doc/snapperd.xml:doc/snapperd.xml.in
doc/snapper-configs.xml:doc/snapper-configs.xml.in
+ doc/snbk.xml:doc/snbk.xml.in
+ doc/snapper-backup-configs.xml:doc/snapper-backup-configs.xml.in
doc/snapper-zypp-plugin.xml:doc/snapper-zypp-plugin.xml.in
doc/snapper-zypp-plugin.conf.xml:doc/snapper-zypp-plugin.conf.xml.in
doc/pam_snapper.xml:doc/pam_snapper.xml.in
EXTRA_DIST = sysconfig.snapper base.txt lvm.txt x11.txt snapper.logrotate \
default-config org.opensuse.Snapper.conf org.opensuse.Snapper.service \
zypp-plugin.conf timeline.service timeline.timer cleanup.service \
- cleanup.timer boot.service boot.timer snapperd.service
+ cleanup.timer boot.service boot.timer backup.service backup.timer \
+ snapperd.service
install-data-local:
install -D -m 644 snapper.logrotate $(DESTDIR)/etc/logrotate.d/snapper
install -d -m 755 $(DESTDIR)/etc/snapper/configs
+ install -d -m 755 $(DESTDIR)/etc/snapper/backup-configs
+ install -d -m 755 $(DESTDIR)/etc/snapper/certs
install -d -m 755 $(DESTDIR)/usr/lib/snapper/plugins
install -D -m 644 cleanup.timer $(DESTDIR)/usr/lib/systemd/system/snapper-cleanup.timer
install -D -m 644 boot.service $(DESTDIR)/usr/lib/systemd/system/snapper-boot.service
install -D -m 644 boot.timer $(DESTDIR)/usr/lib/systemd/system/snapper-boot.timer
+ install -D -m 644 backup.service $(DESTDIR)/usr/lib/systemd/system/snapper-backup.service
+ install -D -m 644 backup.timer $(DESTDIR)/usr/lib/systemd/system/snapper-backup.timer
install -D -m 644 snapperd.service $(DESTDIR)/usr/lib/systemd/system/snapperd.service
endif
--- /dev/null
+[Unit]
+Description=Backup of Snapper Snapshots
+Documentation=man:snbk(8)
+After=nss-user-lookup.target
+
+[Service]
+Type=simple
+WorkingDirectory=/root
+ExecStart=/usr/sbin/snbk --verbose --automatic transfer-and-delete
+
+CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_SYS_ADMIN CAP_SYS_MODULE CAP_IPC_LOCK CAP_SYS_NICE
+LockPersonality=true
+NoNewPrivileges=false
+ProtectHostname=true
+RestrictRealtime=true
--- /dev/null
+
+[Unit]
+Description=Backup of Snapper Snapshots
+Documentation=man:snbk(5)
+
+[Timer]
+OnCalendar=hourly
+
+[Install]
+WantedBy=timers.target
+
snapper.xml
snapperd.xml
snapper-configs.xml
+snbk.xml
+snapper-backup-configs.xml
snapper-zypp-plugin.xml
snapper-zypp-plugin.conf.xml
pam_snapper.xml
snapper.8
snapperd.8
snapper-configs.5
+snbk.8
+snapper-backup-configs.5
snapper-zypp-plugin.8
snapper-zypp-plugin.conf.5
pam_snapper.8
snapper.html
snapperd.html
snapper-configs.html
+snbk.html
+snapper-backup-configs.html
snapper-zypp-plugin.html
snapper-zypp-plugin.conf.html
pam_snapper.html
if ENABLE_DOC
-man_MANS = snapper.8 snapperd.8 snapper-configs.5
+man_MANS = snapper.8 snapperd.8 snapper-configs.5 snbk.8 snapper-backup-configs.5
if HAVE_PAM
man_MANS += pam_snapper.8
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<refentry id='snapper-backup-configs5'>
+
+ <refentryinfo>
+ <date>2024-11-05</date>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>snapper-backup-configs</refentrytitle>
+ <manvolnum>5</manvolnum>
+ <refmiscinfo class='date'>2024-11-05</refmiscinfo>
+ <refmiscinfo class='version'>@VERSION@</refmiscinfo>
+ <refmiscinfo class='manual'>Filesystem Snapshot Management</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>snapper-backup-configs</refname>
+ <refpurpose>Configuration files for snapper backup configs</refpurpose>
+ </refnamediv>
+
+ <refsect1 id='description'>
+ <title>DESCRIPTION</title>
+ <para>Each file <filename>/etc/snapper/backup-configs/*.json</filename> describes a
+ snapper backup config.</para>
+ <para>The file uses JSON syntax containing a single object with
+ key-value pairs.</para>
+ </refsect1>
+
+ <refsect1 id='key-value-pairs'>
+ <title>KEY VALUE PAIRS</title>
+
+ <para>The following is a list of keys that can be present in the
+ configuration file.</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><option>config</option></term>
+ <listitem>
+ <para>Name of the snapper config.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>target-mode</option></term>
+ <listitem>
+ <para>Either local or ssh-push.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>source-path</option></term>
+ <listitem>
+ <para>Path of the subvolume or mount point.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>target-path</option></term>
+ <listitem>
+ <para>Path of the subvolume or mount point.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>automatic</option></term>
+ <listitem>
+ <para>Boolean for enabling automatic transfer and delete
+ for the backup config using a systemd timer service.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>ssh-host</option></term>
+ <listitem>
+ <para>Name of the target host. Required for target mode
+ ssh-push.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>ssh-port</option></term>
+ <listitem>
+ <para>Port of the target host. Optional for target mode
+ ssh-push.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>ssh-user</option></term>
+ <listitem>
+ <para>User on the target host. Optional for target mode
+ ssh-push.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>ssh-identity</option></term>
+ <listitem>
+ <para>An ssh-identity to access the host without requiring a
+ password or passphrase. Optional for target mode ssh-push.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id='examples'>
+ <title>EXAMPLES</title>
+ <para>An example backup config for local backups:
+ <programlisting>
+{
+ "config": "root",
+ "target-mode": "local",
+ "automatic": true,
+ "source-path": "/",
+ "target-path": "/backups/root"
+}
+ </programlisting>
+ </para>
+
+ <para>An example backup config for remote backups via ssh-push:
+ <programlisting>
+{
+ "config": "root",
+ "target-mode": "ssh-push",
+ "automatic": false,
+ "source-path": "/",
+ "target-path": "/backups/eberich/root",
+ "ssh-host": "backups.example.com",
+ "ssh-identity": "/etc/snapper/certs/id_ecdsa"
+}
+ </programlisting>
+ </para>
+ </refsect1>
+
+ <refsect1 id='homepage'>
+ <title>HOMEPAGE</title>
+ <para><ulink url='http://snapper.io/'>http://snapper.io/</ulink></para>
+ </refsect1>
+
+ <refsect1 id='authors'>
+ <title>AUTHORS</title>
+ <para>Arvin Schnell <email>aschnell@suse.com</email></para>
+ </refsect1>
+
+ <refsect1 id='see_also'>
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry><refentrytitle>snbk</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>snapper</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ </para>
+ </refsect1>
+
+</refentry>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<refentry id='snbk8' xmlns:xlink="http://www.w3.org/1999/xlink">
+
+ <refentryinfo>
+ <date>2024-11-05</date>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>snbk</refentrytitle>
+ <manvolnum>8</manvolnum>
+ <refmiscinfo class='date'>2024-11-05</refmiscinfo>
+ <refmiscinfo class='version'>@VERSION@</refmiscinfo>
+ <refmiscinfo class='manual'>Filesystem Snapshot Management</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>snbk</refname>
+ <refpurpose>Command-line program to backup snapshots of snapper</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv id='synopsis'>
+ <cmdsynopsis>
+ <command>snbk</command>
+ <arg choice='opt'><replaceable>--global-opts</replaceable></arg>
+ <arg choice='plain'><replaceable>command</replaceable></arg>
+ <arg choice='opt'><replaceable>command-arguments</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>snbk</command>
+ <arg choice='req'>--help</arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id='description'>
+ <title>DESCRIPTION</title>
+ <para>Snbk is a command-line program to backup snapshot of snapper. It can
+ transfer and delete backup snapshots on local and remote btrfs filesystems.</para>
+ </refsect1>
+
+ <refsect1 id='concepts'>
+ <title>CONCEPTS</title>
+
+ <refsect2 id='backup-configurations'>
+ <title>Backup Configurations</title>
+ <para>For each snapper config there can be several backup
+ configs. Each backup config defines backups of the snapper snapshots at
+ one location, either local or remote, see
+ <citerefentry><refentrytitle>snapper-backup-configs</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+ for possible settings.</para>
+ </refsect2>
+
+ <refsect2 id='snapshot-statuses'>
+ <title>Snapshot Statuses</title>
+
+ <para>Each snapshot has a status on the source and on the
+ target. Possible values for the source are:</para>
+
+ <glosslist>
+ <glossentry>
+ <glossterm>(empty)</glossterm>
+ <glossdef>
+ <para>The snapshot is missing and thus considered obsolete
+ on the target. It will be deleted on the target by the next delete
+ command.</para>
+ </glossdef>
+ </glossentry>
+ <glossentry>
+ <glossterm>read-only</glossterm>
+ <glossdef>
+ <para>The snapshot is read-only.</para>
+ </glossdef>
+ </glossentry>
+ <glossentry>
+ <glossterm>read-write</glossterm>
+ <glossdef>
+ <para>The snapshot is read-write and thus cannot be backed-up.</para>
+ </glossdef>
+ </glossentry>
+ </glosslist>
+
+ <para>Possible values for the target are:</para>
+ <glosslist>
+ <glossentry>
+ <glossterm>(empty)</glossterm>
+ <glossdef>
+ <para>The snapshot is missing. If the source snapshot is
+ read-only it will be transferred to the target by the
+ next transfer command.</para>
+ </glossdef>
+ </glossentry>
+ <glossentry>
+ <glossterm>valid</glossterm>
+ <glossdef>
+ <para>The snapshot is valid. That implies it is read-only.</para>
+ </glossdef>
+ </glossentry>
+ <glossentry>
+ <glossterm>invalid</glossterm>
+ <glossdef>
+ <para>The snapshot is invalid. Either the received UUID is
+ wrong or it is read-write. That can happen if the transfer
+ was interrupted. The next transfer command will try to
+ transfer the snapshot again.</para>
+ </glossdef>
+ </glossentry>
+ </glosslist>
+ </refsect2>
+
+ </refsect1>
+
+ <refsect1 id='global_options'>
+ <title>GLOBAL OPTIONS</title>
+ <variablelist>
+ <varlistentry>
+ <term><option>-q, --quiet</option></term>
+ <listitem>
+ <para>Suppress normal output. Error messages will still be printed, though.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>-v, --verbose</option></term>
+ <listitem>
+ <para>Increase verbosity.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--debug</option></term>
+ <listitem>
+ <para>Turn on debugging.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--utc</option></term>
+ <listitem>
+ <para>Display dates and times in UTC. By default, local time is used.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--iso</option></term>
+ <listitem>
+ <para>Display dates and times in ISO format. ISO format is always used for machine-readable
+ outputs.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>-t, --table-style <replaceable>style</replaceable></option></term>
+ <listitem>
+ <para>Specifies table style. Table style is identified by an integer number.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--machine-readable <replaceable>format</replaceable></option></term>
+ <listitem>
+ <para>Specifies a machine-readable output format. Possible options are csv and json.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--csvout</option></term>
+ <listitem>
+ <para>Sets CSV output format. See
+ <link xlink:href="https://tools.ietf.org/html/rfc4180">RFC 4180</link>
+ for the details, except lines end with a LF, not CR+LF.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--jsonout</option></term>
+ <listitem>
+ <para>Sets JSON output format.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--separator <replaceable>character</replaceable></option></term>
+ <listitem>
+ <para>Specifies the character separator for CSV output format.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--no-headers</option></term>
+ <listitem>
+ <para>Suppress headers for CSV output format.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>-b, --backup-config <replaceable>name</replaceable></option></term>
+ <listitem>
+ <para>Use specified configuration instead of all configurations.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--no-dbus</option></term>
+ <listitem>
+ <para>Operate without a DBus connection.</para>
+ <para>Use with caution.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--target-mode <replaceable>name</replaceable></option></term>
+ <listitem>
+ <para>Only operate on backup configs with the specified target mode.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--automatic</option></term>
+ <listitem>
+ <para>Only operate on backup configs which have the
+ automatic flag set.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--version</option></term>
+ <listitem>
+ <para>Print version and exit.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id='commands'>
+ <title>COMMANDS</title>
+
+ <para>Snbk provides a number of <emphasis>commands</emphasis>. Each
+ command accepts the options listed in the <link
+ linkend='global_options'>GLOBAL OPTIONS</link> section. These options
+ must be specified <emphasis>before</emphasis> the command name. In
+ addition, many commands have specific arguments, which are listed in
+ this section. These command-specific arguments must be specified
+ <emphasis>after</emphasis> the name of the command.</para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><option>help</option></term>
+ <listitem>
+ <para>Show short help text.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>list-configs</option></term>
+ <listitem>
+ <para>List available configurations.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>list (ls)</option></term>
+ <listitem>
+ <para>List snapshots.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>transfer [<replaceable>number</replaceable>]</option></term>
+ <listitem>
+ <para>Transfer all missing snapshots or the specified
+ snapshot to the target.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>delete (remove|rm) [<replaceable>number</replaceable>]</option></term>
+ <listitem>
+ <para>Delete all obsolete snapshots or the specified
+ snapshot from the target.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>transfer-and-delete</option></term>
+ <listitem>
+ <para>Combines transfer and delete.</para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id='restore'>
+ <title>RESTORE</title>
+ <para>So far a restore has to be done manually. There are several
+ methods to do a restore, e.g. the backup can be mounted and then
+ copied or rsync can be used. Here we provide an example on how to use
+ btrfs send and receive to restore a snapshot on the source
+ system. In general using btrfs send and receive is a bit
+ tricky.</para>
+ <para>When using target-mode local:
+ <programlisting>
+# mkdir /.snapshots/42
+# cp /backups/root/42/info.xml /.snapshots/42/
+# btrfs send /backups/root/42/snapshot | btrfs receive /.snapshots/42
+ </programlisting>
+ When using target-mode ssh-push:
+ <programlisting>
+# mkdir /.snapshots/42
+# scp backups.example.com:/backups/eberich/root/42/info.xml /.snapshots/42
+# ssh backups.example.com btrfs send /backups/eberich/root/42/snapshot | btrfs receive /.snapshots/42
+ </programlisting>
+ </para>
+ <para>If the system was reinstalled it is unfortunately in general
+ not possibly to simply use the restored snapshot as the new default
+ snapshot since some files, e.g. /etc/fstab, likely need
+ modifications. Also unfortunately for other subvolumes than root a
+ rollback is not supported.</para>
+ <para>If there are snapshots on the source to speed up the
+ operation you can also use the -p option for btrfs send.</para>
+ </refsect1>
+
+ <refsect1 id='notes'>
+ <title>NOTES</title>
+ <para>The content of snapshots transferred must not be changed on
+ the source system. Normally this is ensured since the snapshots are
+ read-only. But it is possible to change snapshots to read-write. This
+ can cause error during transfers in the future.</para>
+ </refsect1>
+
+ <refsect1 id='permissions'>
+ <title>PERMISSIONS</title>
+ <para>Since the target-mode ssh-push needs root permissions on the
+ target it is recommended to use a dedicated machine or container
+ as a target.</para>
+ </refsect1>
+
+ <refsect1 id='files'>
+ <title>FILES</title>
+ <variablelist>
+ <varlistentry>
+ <term><filename>/etc/snapper/backup-configs</filename></term>
+ <listitem>
+ <para>Directory containing configuration files.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id='exit_status'>
+ <title>EXIT STATUS</title>
+ <para>Normally the exit status is 0. If an error occurred the exit
+ status is 1.</para>
+ </refsect1>
+
+ <refsect1 id='homepage'>
+ <title>HOMEPAGE</title>
+ <para><ulink url='http://snapper.io/'>http://snapper.io/</ulink></para>
+ </refsect1>
+
+ <refsect1 id='authors'>
+ <title>AUTHORS</title>
+ <para>Arvin Schnell <email>aschnell@suse.com</email></para>
+ </refsect1>
+
+ <refsect1 id='see_also'>
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry><refentrytitle>snapper-backup-configs</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>snapper</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+ <citerefentry role="nolink"><refentrytitle>btrfs-send</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ </para>
+ </refsect1>
+
+</refentry>
+-------------------------------------------------------------------
+Tue Nov 05 10:50:50 CET 2024 - aschnell@suse.com
+
+- provide backup program for btrfs snapshots
+
-------------------------------------------------------------------
Fri Oct 25 08:48:46 CEST 2024 - aschnell@suse.com
%else
%config(noreplace) %{_sysconfdir}/logrotate.d/snapper
%endif
-%{_unitdir}/snapper*.*
+%{_unitdir}/snapper-{timeline,cleanup,boot}.*
+%{_unitdir}/snapperd.service
%if 0%{?suse_version} <= 1500
%dir %{_datadir}/dbus-1/system.d
%endif
test -f /etc/snapper/${i}.rpmsave && mv -v /etc/snapper/${i}.rpmsave /etc/snapper/${i} ||:
done
+%package -n snapper-backup
+Requires: snapper = %version
+Summary: A backup program for snapper
+Group: System/Packages
+
+%description -n snapper-backup
+A backup program for snapshots created by snapper.
+
+%files -n snapper-backup
+%{_sbindir}/snbk
+%dir %{_sysconfdir}/snapper/backup-configs
+%dir %{_sysconfdir}/snapper/certs
+%{_unitdir}/snapper-{backup}.*
+%{_mandir}/*/snbk.8*
+%{_mandir}/*/snapper-backup-configs.5*
+
%package -n pam_snapper
Requires: pam
Requires: snapper = %version
Summary: PAM module for calling snapper
+Requires: util-linux-systemd
Group: System/Packages
%description -n pam_snapper
#define SYSCONFIG_FILE CONF_DIR "/snapper"
#define CONFIGS_DIR "/etc/snapper/configs"
+#define BACKUP_CONFIGS_DIR "/etc/snapper/backup-configs"
#define ETC_CONFIG_TEMPLATE_DIR "/etc/snapper/config-templates"
#define USR_CONFIG_TEMPLATE_DIR "/usr/share/snapper/config-templates"
// commands
#define SH_BIN "/bin/sh"
+#define SSH_BIN "/usr/bin/ssh"
#define BTRFS_BIN "/usr/sbin/btrfs"
#define SYSTEMCTL_BIN "/usr/bin/systemctl"
+#define FINDMNT_BIN "/usr/bin/findmnt"
+#define REALPATH_BIN "/usr/bin/realpath"
+
+#define CP_BIN "/usr/bin/cp"
+#define SCP_BIN "/usr/bin/scp"
+#define MKDIR_BIN "/usr/bin/mkdir"
+
+#define RM_BIN "/usr/bin/rm"
+#define RMDIR_BIN "/usr/bin/rmdir"
+
// keys from the config files
#define KEY_TIMELINE_CREATE "TIMELINE_CREATE"
+// regexes
+
+#define UUID_REGEX "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
+
+
#endif