mirror of
https://gitlab.freedesktop.org/NetworkManager/NetworkManager
synced 2024-10-02 22:38:01 +00:00
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:
parent
47eaf963e3
commit
1c17e55627
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
Loading…
Reference in a new issue