]> git.ipfire.org Git - thirdparty/snapper.git/commitdiff
Add snbk visualize command to produce tree diagrams of snapshots (#1085)
authorJames Lai <jamesljlster@gmail.com>
Fri, 16 Jan 2026 14:21:31 +0000 (22:21 +0800)
committerGitHub <noreply@github.com>
Fri, 16 Jan 2026 14:21:31 +0000 (14:21 +0000)
* Add snbk visualize command

* Add bash completion for snbk visualize

* Add documentation for snbk visualize command

* Remove access by reference for Rankdir enumeration

* Prevent graph text from being translated

client/snbk/Makefile.am
client/snbk/TreeView.cc
client/snbk/TreeView.h
client/snbk/cmd-visualize.cc [new file with mode: 0644]
client/snbk/cmd.h
client/snbk/snbk.cc
doc/snbk.xml.in
scripts/completion/snbk-completion.bash

index d7095150f2c11439fd6556fc5cfecc40b082b099..42e34c60f7a04e47d9d84aea840e007439bba3a4 100644 (file)
@@ -15,6 +15,7 @@ snbk_SOURCES =                                                \
        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         \
index 1c27e5916ff63cac92fc455cc4030d6c5ea871a9..54af02f5549206af12a9d37b08424d38356b4c46 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) [2024-2025] SUSE LLC
+ * Copyright (c) [2024-2026] SUSE LLC
  *
  * All Rights Reserved.
  *
@@ -100,7 +100,7 @@ namespace snapper
                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" : "");
        }
@@ -126,9 +126,8 @@ namespace snapper
                        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);
@@ -293,15 +292,15 @@ namespace snapper
        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);
     }
@@ -311,4 +310,11 @@ namespace snapper
        "none", "direct-parent", "implicit-parent"
     });
 
+    const vector<string> EnumInfo<TreeView::Rankdir>::names({
+        "TB",
+        "LR",
+        "BT",
+        "RL",
+    });
+
 }
index f402e3a9c317b2462708e137f5df383e91fded03..f6b88b1c5d5ec3b8c3b3a12d5376a7b21446c920 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) [2024-2025] SUSE LLC
+ * Copyright (c) [2024-2026] SUSE LLC
  *
  * All Rights Reserved.
  *
@@ -52,6 +52,15 @@ namespace snapper
            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
        {
@@ -104,7 +113,7 @@ namespace snapper
        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:
 
@@ -138,7 +147,7 @@ namespace snapper
         * 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.
@@ -159,6 +168,11 @@ namespace snapper
        static const vector<string> names;
     };
 
+    template <> struct EnumInfo<TreeView::Rankdir>
+    {
+       static const vector<string> names;
+    };
+
 } // namespace snapper
 
 #endif
diff --git a/client/snbk/cmd-visualize.cc b/client/snbk/cmd-visualize.cc
new file mode 100644 (file)
index 0000000..b3e6af6
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * 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
index 681c004310df311ca1303aedde1d756be1c8dbab..a4b0451a2797737c832d4e87a1da0bbbe0f8710d 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Copyright (c) [2011-2015] Novell, Inc.
- * Copyright (c) [2016-2024] SUSE LLC
+ * Copyright (c) [2016-2026] SUSE LLC
  *
  * All Rights Reserved.
  *
@@ -76,4 +76,10 @@ namespace snapper
     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);
+
 }
index 753bf34070025305f407948b4eb813b37ff7b465..7a70f60f9fa42ff4e031a2169706883f2fc42643 100644 (file)
@@ -128,6 +128,7 @@ main(int argc, char** argv)
        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
index af79b8b0867d1fb4bd5627f1fca2007de307a04b..1b95061351c10c26e287f91ebea7a3a6933732bc 100644 (file)
@@ -2,13 +2,13 @@
 <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>
 
index 5a8b4969bd51083db80f626f69a374dec15c0477..0dcdccd7261fe2af160ff6df7413def895dc9214 100644 (file)
@@ -32,7 +32,8 @@ _snbk()
         "transfer"
        "restore"
        "delete"
-       "transfer-and-delete")
+       "transfer-and-delete"
+       "visualize")
 
     local command i
     for (( i=0; i < ${#words[@]}-1; i++ )); do
@@ -69,6 +70,10 @@ _snbk()
     # 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
@@ -76,6 +81,25 @@ _snbk()
         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" ) )