nmcli/devices: add "checkpoint" command

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).
This commit is contained in:
Lubomir Rintel 2022-05-04 09:19:01 +02:00
parent 47eaf963e3
commit 1c17e55627
3 changed files with 279 additions and 3 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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"
@ -1042,6 +1042,18 @@ usage_device_lldp(void)
"used to list neighbors for a particular interface.\n\n"));
}
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)
{
@ -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},