diff options
author | Lubomir Rintel <lkundrak@v3.sk> | 2022-05-04 09:19:01 +0200 |
---|---|---|
committer | Lubomir Rintel <lkundrak@v3.sk> | 2022-06-15 12:26:08 +0200 |
commit | 1c17e556276ee786da374684b0752b3128e16938 (patch) | |
tree | a3b352cbade22294149bce0ca162de5677f5ec63 | |
parent | 47eaf963e31dca8e82d6d9c9454c000528fe5fa7 (diff) | |
download | NetworkManager-1c17e556276ee786da374684b0752b3128e16938.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.xml | 29 | ||||
-rw-r--r-- | man/nmcli.xml | 30 | ||||
-rw-r--r-- | src/nmcli/devices.c | 223 |
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}, |