cmd-restore.cc \
cmd-delete.cc \
cmd-transfer-and-delete.cc \
+ cmd-visualize.cc \
BackupConfig.cc BackupConfig.h \
TheBigThing.cc TheBigThing.h \
GlobalOptions.cc GlobalOptions.h \
/*
- * Copyright (c) [2024-2025] SUSE LLC
+ * Copyright (c) [2024-2026] SUSE LLC
*
* All Rights Reserved.
*
label = label + "\\n" + boost::join(properties, ", ");
}
- return sformat(_("%s [ label=\"%s\", style=\"%s\", shape=\"box\"]"),
+ return sformat("%s [ label=\"%s\", style=\"%s\", shape=\"box\"]",
get_node_id(node).c_str(), label.c_str(),
node->is_virtual() ? "dashed" : "");
}
SN_THROW(Exception("Invalid parent type."));
}
- cout << sformat(_(" %s -> %s [style=\"%s\"]"),
- get_node_id(node).c_str(), get_node_id(child).c_str(),
- link_style)
+ cout << sformat(" %s -> %s [style=\"%s\"]", get_node_id(node).c_str(),
+ get_node_id(child).c_str(), link_style)
<< '\n';
print_graph_graphviz_recursive(child);
node->parent_type = parent_type;
}
- void TreeView::print_graph_graphviz(const ProxyNode* node, const string& rankdir)
+ void TreeView::print_graph_graphviz(const ProxyNode* node, const Rankdir rankdir)
{
cout << "digraph {" << '\n';
- cout << sformat(_(" rankdir=\"%s\"\n"), rankdir.c_str());
+ cout << sformat(" rankdir=\"%s\"\n", toString(rankdir).c_str());
print_graph_graphviz_recursive(node);
cout << "}" << '\n';
}
- void TreeView::print_graph_graphviz(const string& rankdir) const
+ void TreeView::print_graph_graphviz(const Rankdir rankdir) const
{
TreeView::print_graph_graphviz(virtual_root.get(), rankdir);
}
"none", "direct-parent", "implicit-parent"
});
+ const vector<string> EnumInfo<TreeView::Rankdir>::names({
+ "TB",
+ "LR",
+ "BT",
+ "RL",
+ });
+
}
/*
- * Copyright (c) [2024-2025] SUSE LLC
+ * Copyright (c) [2024-2026] SUSE LLC
*
* All Rights Reserved.
*
IMPLICIT_PARENT /** Manually assigned. */
};
+ /** Direction to draw directed graphs (Graphviz rankdir attribute). */
+ enum class Rankdir
+ {
+ TB,
+ LR,
+ BT,
+ RL
+ };
+
/** Proxy class to the nodes (snapshots). */
class ProxyNode
{
find_nearest_valid_node(const string& start_uuid) const;
/** Print the tree graph in Graphviz DOT Language. */
- void print_graph_graphviz(const string& rankdir = "LR") const;
+ void print_graph_graphviz(const Rankdir rankdir = Rankdir::LR) const;
private:
* Print the tree graph in Graphviz DOT Language, starting from the given node.
*/
static void print_graph_graphviz(const ProxyNode* node,
- const string& rankdir = "LR");
+ const Rankdir rankdir = Rankdir::LR);
/**
* A static function that sets the parent relationship for the given two nodes.
static const vector<string> names;
};
+ template <> struct EnumInfo<TreeView::Rankdir>
+ {
+ static const vector<string> names;
+ };
+
} // namespace snapper
#endif
--- /dev/null
+/*
+ * Copyright (c) 2026 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 "../misc.h"
+#include "../utils/help.h"
+
+#include "BackupConfig.h"
+#include "GlobalOptions.h"
+#include "TheBigThing.h"
+
+
+namespace snapper
+{
+ using namespace std;
+
+ namespace
+ {
+
+ enum class Mode
+ {
+ SOURCE_TREE,
+ TARGET_TREE
+ };
+
+ } // namespace
+
+
+ template <> struct EnumInfo<Mode>
+ {
+ static const vector<string> names;
+ };
+
+ const vector<string> EnumInfo<Mode>::names({
+ "source-tree",
+ "target-tree",
+ });
+
+
+ void help_visualize()
+ {
+ cout << " " << _("Produce a specific graph in Graphviz DOT format:") << '\n'
+ << "\t" << _("snbk visualize <mode>") << '\n'
+ << "\n"
+ << "\t" << _("Supported modes:") << '\n'
+ << "\t"
+ << _("- source-tree: Produce a tree diagram of the snapshots on the source.")
+ << '\n'
+ << "\t"
+ << _("- target-tree: Produce a tree diagram of the snapshots on the target.")
+ << '\n'
+ << '\n'
+ << _(" Options for the 'visualize' command:") << '\n';
+
+ print_options({
+ { _("--rankdir, -r"),
+ _("The 'rankdir' diagram attribute of Graphviz. Defaults to 'LR'.") },
+ });
+ }
+
+ void command_visualize(const GlobalOptions& global_options, GetOpts& get_opts,
+ BackupConfigs& backup_configs, ProxySnappers* snappers)
+ {
+ // Drawing a graph for multiple backup configs is not supported.
+ if (backup_configs.size() != 1)
+ {
+ SN_THROW(OptionsException(_("A backup-config must be specified to run this "
+ "command.")));
+ }
+
+ BackupConfig& backup_config = backup_configs.front();
+
+ // Check and parse arguments
+ const vector<Option> options = { Option("rankdir", required_argument, 'r') };
+ ParsedOpts opts = get_opts.parse("visualize", options);
+ if (get_opts.num_args() != 1)
+ {
+ SN_THROW(OptionsException(_("Command 'visualize' needs one argument.")));
+ }
+
+ Mode mode;
+ const char* arg = get_opts.pop_arg();
+ if (!toValue(arg, mode, false))
+ {
+ string error = sformat(_("Unknown mode '%s'."), arg) + '\n' +
+ possible_enum_values<Mode>();
+ SN_THROW(OptionsException(error));
+ }
+
+ TreeView::Rankdir rankdir = TreeView::Rankdir::LR;
+ ParsedOpts::const_iterator opt;
+ if ((opt = opts.find("rankdir")) != opts.end())
+ {
+ if (!toValue(opt->second, rankdir, false))
+ {
+ string error = sformat(_("Unknown rankdir '%s'."), opt->second.c_str()) +
+ '\n' + possible_enum_values<TreeView::Rankdir>();
+ SN_THROW(OptionsException(error));
+ }
+ }
+
+ // Execute command
+ TheBigThings the_big_things(backup_config, snappers, false);
+ switch (mode)
+ {
+ case Mode::SOURCE_TREE:
+ the_big_things.source_tree.print_graph_graphviz(rankdir);
+ break;
+
+ case Mode::TARGET_TREE:
+ the_big_things.target_tree.print_graph_graphviz(rankdir);
+ break;
+ };
+ }
+
+} // namespace snapper
/*
* Copyright (c) [2011-2015] Novell, Inc.
- * Copyright (c) [2016-2024] SUSE LLC
+ * Copyright (c) [2016-2026] SUSE LLC
*
* All Rights Reserved.
*
command_transfer_and_delete(const GlobalOptions& global_options, GetOpts& get_opts, BackupConfigs& backup_configs,
ProxySnappers* snappers);
+
+ void help_visualize();
+
+ void command_visualize(const GlobalOptions& global_options, GetOpts& get_opts,
+ BackupConfigs& backup_configs, ProxySnappers* snappers);
+
}
Cmd("restore", command_restore, help_restore, true),
Cmd("delete", { "remove", "rm" }, command_delete, help_delete, true),
Cmd("transfer-and-delete", command_transfer_and_delete, help_transfer_and_delete, true),
+ Cmd("visualize", command_visualize, help_visualize, true),
};
try
<refentry id='snbk8' xmlns:xlink="http://www.w3.org/1999/xlink">
<refentryinfo>
- <date>2025-12-18</date>
+ <date>2026-01-16</date>
</refentryinfo>
<refmeta>
<refentrytitle>snbk</refentrytitle>
<manvolnum>8</manvolnum>
- <refmiscinfo class='date'>2025-12-18</refmiscinfo>
+ <refmiscinfo class='date'>2026-01-16</refmiscinfo>
<refmiscinfo class='version'>@VERSION@</refmiscinfo>
<refmiscinfo class='manual'>Filesystem Snapshot Management</refmiscinfo>
</refmeta>
</glosslist>
</refsect2>
+ <refsect2 id='visualization'>
+ <title>Visualization</title>
+ <para>
+ Snbk provides a <emphasis>visualize</emphasis> command for producing graphs in
+ Graphviz DOT format for visualization purposes. Currently, producing tree diagrams
+ for snapshots on both the source and the target is supported. The command output
+ can be piped to the Graphviz tools to generate an image.
+ </para>
+ <para>
+ Here is an example of producing a tree diagram for snapshots on the source side
+ with the 'root' backup config:
+ <programlisting>
+# snbk -b root visualize source-tree | dot -T png -o root-source-tree.png
+ </programlisting>
+ </para>
+ <para>
+ In the tree diagram for snapshots, a node has the properties
+ <emphasis>virtual</emphasis> and <emphasis>valid</emphasis>. A virtual node means
+ the corresponding Btrfs subvolume is not managed by a Snapper snapshot. A valid
+ node means the node exists on both the source and target sides, and can be used as
+ a valid Btrfs send parent to reduce disk usage during snapshot transfer or
+ restoration. The root node of the diagram is a specialized virtual node, which is
+ not a Btrfs subvolume, representing the Btrfs filesystem on the source or target.
+ </para>
+ </refsect2>
+
</refsect1>
<refsect1 id='global_options'>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>visualize [options] <replaceable>mode</replaceable></option></term>
+ <listitem>
+ <para>
+ Produce a Graphviz graph with the specified <replaceable>mode</replaceable>.
+ See <link linkend='visualization'>Visualization</link> for more detail.
+ </para>
+ <variablelist>
+ <varlistentry>
+ <term>
+ <option>-r, --rankdir</option> <replaceable>rankdir</replaceable>
+ </term>
+ <listitem>
+ <para>
+ The 'rankdir' diagram attribute of Graphviz. Defaults to 'LR'.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>source-tree</option></term>
+ <listitem>
+ <para>Produce a tree diagram of the snapshots on the source.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>target-tree</option></term>
+ <listitem>
+ <para>Produce a tree diagram of the snapshots on the target.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
"transfer"
"restore"
"delete"
- "transfer-and-delete")
+ "transfer-and-delete"
+ "visualize")
local command i
for (( i=0; i < ${#words[@]}-1; i++ )); do
# supported options per command
if [[ "$cur" == -* ]]; then
case $command in
+ visualize)
+ COMPREPLY=( $( compgen -W '--rankdir -r' -- "$cur" ) )
+ return 0
+ ;;
*)
COMPREPLY=( $( compgen -W "$GLOBAL_SNBK_OPTIONS" -- "$cur" ) )
return 0
esac
fi
+ # specific command arguments
+ if [[ -n $command ]]; then
+ case $command in
+ visualize)
+ case "$prev" in
+ --rankdir|-r)
+ COMPREPLY=( $( compgen -W 'TB LR BT RL' -- "$cur" ) )
+ ;;
+ source-tree|target-tree)
+ ;;
+ *)
+ COMPREPLY=( $( compgen -W 'source-tree target-tree' -- "$cur" ) )
+ ;;
+ esac
+ return 0
+ ;;
+ esac
+ fi
+
# no command yet, show what commands we have
if [ "$command" = "" ]; then
#COMPREPLY=( $( compgen -W '${COMMANDS[@]} ${GLOBAL_SNBK_OPTIONS[@]}' -- "$cur" ) )