summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLubomir Rintel <lkundrak@v3.sk>2022-05-04 09:19:01 +0200
committerLubomir Rintel <lkundrak@v3.sk>2022-06-15 12:26:08 +0200
commit1c17e556276ee786da374684b0752b3128e16938 (patch)
treea3b352cbade22294149bce0ca162de5677f5ec63
parent47eaf963e31dca8e82d6d9c9454c000528fe5fa7 (diff)
downloadNetworkManager-lr/nmcli-checkpoint.tar.gz
nmcli/devices: add "checkpoint" commandlr/nmcli-checkpoint
This is an interface to the Checkpoint/Restore functionality that's available for quite some time. It runs a command with a checkpoint taken and rolls back unless success is confirmed before the checkpoint times out: $ nmcli dev checkpoint eth0 -- nmcli dev dis eth0 Device 'eth0' successfully disconnected. Type "Yes" to commit the changes: No Checkpoint was removed. The details about how it's used are documented in nmcli(1) and nmcli-examples(7).
-rw-r--r--man/nmcli-examples.xml29
-rw-r--r--man/nmcli.xml30
-rw-r--r--src/nmcli/devices.c223
3 files changed, 279 insertions, 3 deletions
diff --git a/man/nmcli-examples.xml b/man/nmcli-examples.xml
index 0afa9797a3..1fd7b76675 100644
--- a/man/nmcli-examples.xml
+++ b/man/nmcli-examples.xml
@@ -9,7 +9,7 @@
<!--
nmcli-examples(7) manual page
- Copyright 2005 - 2016 Red Hat, Inc.
+ Copyright 2005 - 2022 Red Hat, Inc.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.1
@@ -640,6 +640,33 @@ Connection 'ethernet-4' (de89cdeb-a3e1-4d53-8fa0-c22546c775f4) successfully
</para>
</example>
+ <example><title>Device Checkpoint and Restore</title>
+
+<screen><prompt>$ </prompt><userinput>nmcli dev checkpoint eth0 -- nmcli dev dis eth0</userinput>
+Device 'eth0' successfully disconnected.
+Type "Yes" to commit the changes: No
+Checkpoint was removed.</screen>
+ <para>
+ In this example the device eth0 was disconnected with the eth0 checkpoint
+ taken. The user didn't confirm that the change is good, so the eth0 was
+ brought back to the state it was when the checkpoint was taken.
+ </para>
+ <para>
+ If the command being run unintentionaly brings down the remote connection
+ (such as a
+ <citerefentry><refentrytitle>ssh</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ session) to the very machine it's being run on, the user wouldn't be able to
+ confirm the success and the connectivity would end up being restored
+ after a timeout.
+ </para>
+ <para>
+ If, on the other hand, the command results in a success, the user could just
+ confirm, causing the checkpoint to be abandoned without a rollback:
+ </para>
+<screen><prompt>$ </prompt><userinput>nmcli dev checkpoint -- ip link del br0</userinput>
+Type "Yes" to commit the changes: <userinput>Yes</userinput></screen>
+ </example>
+
</refsect1>
<refsect1>
diff --git a/man/nmcli.xml b/man/nmcli.xml
index ba89b29112..768087ea46 100644
--- a/man/nmcli.xml
+++ b/man/nmcli.xml
@@ -9,7 +9,7 @@
<!--
nmcli(1) manual page
- Copyright 2010 - 2018 Red Hat, Inc.
+ Copyright 2010 - 2022 Red Hat, Inc.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.1
@@ -1396,6 +1396,7 @@
<arg choice='plain'><command>monitor</command></arg>
<arg choice='plain'><command>wifi</command></arg>
<arg choice='plain'><command>lldp</command></arg>
+ <arg choice='plain'><command>checkpoint</command></arg>
</group>
<arg rep='repeat'><replaceable>ARGUMENTS</replaceable></arg>
</cmdsynopsis>
@@ -1838,6 +1839,33 @@
in the connection settings.</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term>
+ <command>checkpoint</command>
+ <arg><option>--timeout</option> <replaceable>seconds</replaceable></arg>
+ <arg rep='repeat'><replaceable>ifname</replaceable></arg>
+ <arg choice='plain'><option>--</option></arg>
+ <arg rep='repeat' choice='plain'><replaceable>COMMAND</replaceable></arg>
+ </term>
+
+ <listitem>
+ <para>Runs the command with a configuration checkpoint taken and asks for a
+ confirmation when finished. When the confirmation is not given, the
+ checkpoint is automatically restored after timeout.</para>
+
+ <para>This allows doing disruptive configuration changes over remote
+ connections with an option of restoring the network configuration to a
+ known good state in case of an error.</para>
+
+ <para>If the a list of interface names is specified, the checkpoint is
+ taken, the checkpoint is takes only on the specified devices. Otherwise
+ a checkpoint is taken for all devices.</para>
+
+ <para>Currently the timeout defaults to 15 seconds. This may change in
+ a future version.</para>
+ </listitem>
+ </varlistentry>
</variablelist>
</refsect1>
diff --git a/src/nmcli/devices.c b/src/nmcli/devices.c
index 6d032aeff7..eaa27a245e 100644
--- a/src/nmcli/devices.c
+++ b/src/nmcli/devices.c
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
- * Copyright (C) 2010 - 2018 Red Hat, Inc.
+ * Copyright (C) 2010 - 2022 Red Hat, Inc.
*/
#include "libnm-client-aux-extern/nm-default-client.h"
@@ -1043,6 +1043,18 @@ usage_device_lldp(void)
}
static void
+usage_device_checkpoint(void)
+{
+ g_printerr(_("Usage: nmcli device checkpoint { ARGUMENTS | help }\n"
+ "\n"
+ "ARGUMENTS := [--timeout <seconds>] -- COMMAND...\n"
+ "\n"
+ "Runs the command with a configuration checkpoint taken and asks for a\n"
+ "confirmation when finished. When the confirmation is not given, the\n"
+ "checkpoint is automatically restored after timeout.\n\n"));
+}
+
+static void
quit(void)
{
if (nm_clear_g_source(&progress_id))
@@ -5009,6 +5021,214 @@ do_device_lldp(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *a
nmc_do_cmd(nmc, device_lldp_cmds, *argv, argc, argv);
}
+/*****************************************************************************/
+
+typedef struct {
+ NmCli *nmc;
+ NMCheckpoint *checkpoint;
+ char **argv;
+ guint removed_id;
+ guint child_id;
+ gboolean removed;
+} CheckpointCbInfo;
+
+static void
+free_checkpoint_info(CheckpointCbInfo *info)
+{
+ g_clear_object(&info->checkpoint);
+ g_strfreev(info->argv);
+ g_slice_free(CheckpointCbInfo, info);
+}
+
+static void
+checkpoints_changed_cb(GObject *object, GParamSpec *pspec, CheckpointCbInfo *info)
+{
+ const GPtrArray *checkpoints;
+ guint i;
+
+ checkpoints = nm_client_get_checkpoints(info->nmc->client);
+ for (i = 0; i < checkpoints->len; i++) {
+ if (checkpoints->pdata[i] == info->checkpoint) {
+ /* Our checkpoint still exists. */
+ return;
+ }
+ }
+
+ g_string_printf(info->nmc->return_text, _("Checkpoint was removed."));
+ info->nmc->return_value = NMC_RESULT_ERROR_TIMEOUT_EXPIRED;
+
+ info->removed = TRUE;
+
+ if (!info->child_id) {
+ /* The command is done, we're in the confirmation prompt. */
+ g_print("%s\n", _("No"));
+ g_main_loop_quit(loop);
+ }
+}
+
+static void
+checkpoint_destroy_cb(GObject *object, GAsyncResult *result, void *user_data)
+{
+ NmCli *nmc = (NmCli *) user_data;
+ gs_free_error GError *error = NULL;
+
+ if (!nm_client_checkpoint_destroy_finish(nmc->client, result, &error)) {
+ g_string_printf(nmc->return_text,
+ _("Error: Destroying a checkpoint failed: %s"),
+ error->message);
+ nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+ }
+
+ g_main_loop_quit(loop);
+}
+
+static void
+child_watch_cb(GPid pid, gint wait_status, gpointer user_data)
+{
+ CheckpointCbInfo *info = (CheckpointCbInfo *) user_data;
+ NmCli *nmc = info->nmc;
+ char *line;
+
+ info->child_id = 0;
+ if (info->removed) {
+ g_main_loop_quit(loop);
+ goto out;
+ }
+
+ while (g_main_loop_is_running(loop)) {
+ line = nmc_readline(&nmc->nmc_config, "Type \"%s\" to commit the changes: ", _("Yes"));
+ if (g_strcmp0(line, _("Yes")) == 0) {
+ g_signal_handler_disconnect(nmc->client, info->removed_id);
+ nm_client_checkpoint_destroy(nmc->client,
+ nm_object_get_path(NM_OBJECT(info->checkpoint)),
+ NULL,
+ checkpoint_destroy_cb,
+ nmc);
+ break;
+ }
+ }
+ nmc_cleanup_readline();
+out:
+ free_checkpoint_info(info);
+}
+
+static void
+checkpoint_create_cb(GObject *object, GAsyncResult *result, void *user_data)
+{
+ NMClient *client = NM_CLIENT(object);
+ CheckpointCbInfo *info = (CheckpointCbInfo *) user_data;
+ gs_free_error GError *error = NULL;
+ GPid pid;
+
+ info->checkpoint = nm_client_checkpoint_create_finish(client, result, &error);
+ if (!info->checkpoint) {
+ g_string_printf(info->nmc->return_text,
+ _("Error: Creating a checkpoint failed: %s"),
+ error->message);
+ info->nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+ g_main_loop_quit(loop);
+ goto err;
+ }
+
+ if (!g_spawn_async(NULL,
+ info->argv,
+ NULL,
+ G_SPAWN_LEAVE_DESCRIPTORS_OPEN | G_SPAWN_SEARCH_PATH
+ | G_SPAWN_CHILD_INHERITS_STDIN | G_SPAWN_DO_NOT_REAP_CHILD,
+ NULL,
+ info,
+ &pid,
+ &error)) {
+ g_string_printf(info->nmc->return_text, _("Error: %s"), error->message);
+ info->nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+ g_main_loop_quit(loop);
+ goto err;
+ }
+
+ info->child_id = g_child_watch_add(pid, child_watch_cb, info);
+ info->removed_id = g_signal_connect(client,
+ "notify::" NM_CLIENT_CHECKPOINTS,
+ G_CALLBACK(checkpoints_changed_cb),
+ info);
+
+ return;
+
+err:
+ free_checkpoint_info(info);
+}
+
+static void
+do_device_checkpoint(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+ NMClient *client = nmc->client;
+ long unsigned int timeout = 15;
+ int option;
+ CheckpointCbInfo *info;
+ const GPtrArray *devices = NULL;
+ gs_unref_ptrarray GPtrArray *devices_free = NULL;
+
+ while ((option = next_arg(nmc, &argc, &argv, "--timeout", NULL)) > 0) {
+ switch (option) {
+ case 1: /* --timeout */
+ argc--;
+ argv++;
+ if (!argc) {
+ g_string_printf(nmc->return_text, _("Error: %s argument is missing."), *(argv - 1));
+ nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+ return;
+ }
+ if (!nmc_string_to_uint(*argv, TRUE, 0, G_MAXUINT32, &timeout)) {
+ g_string_printf(nmc->return_text, _("Error: '%s' is not a valid timeout."), *argv);
+ nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+ return;
+ }
+ break;
+ default:
+ nm_assert_not_reached();
+ break;
+ }
+ }
+
+ if (argc) {
+ if (strcmp(*argv, "--") == 0) {
+ devices = nm_client_get_devices(client);
+ argc--;
+ argv++;
+ } else {
+ devices = devices_free = get_device_list(nmc, &argc, &argv);
+ if (!devices) {
+ g_string_printf(nmc->return_text, _("Error: not all devices found."));
+ nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+ return;
+ }
+ }
+ }
+
+ if (argc == 0) {
+ g_string_printf(nmc->return_text, _("Error: Expected a command to run after '--'"));
+ nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+ return;
+ }
+
+ if (nmc->complete)
+ return;
+
+ info = g_slice_new0(CheckpointCbInfo);
+ info->nmc = nmc;
+ info->argv = nm_strv_dup(argv, argc, TRUE);
+
+ nmc->should_wait++;
+ nm_client_checkpoint_create(client,
+ devices,
+ (guint32) timeout,
+ NM_CHECKPOINT_CREATE_FLAG_NONE,
+ NULL,
+ checkpoint_create_cb,
+ info);
+}
+
+/*****************************************************************************/
+
static gboolean
is_single_word(const char *line)
{
@@ -5055,6 +5275,7 @@ void
nmc_command_func_device(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
{
static const NMCCommand cmds[] = {
+ {"checkpoint", do_device_checkpoint, usage_device_checkpoint, TRUE, TRUE},
{"connect", do_device_connect, usage_device_connect, TRUE, TRUE},
{"disconnect", do_devices_disconnect, usage_device_disconnect, TRUE, TRUE},
{"delete", do_devices_delete, usage_device_delete, TRUE, TRUE},