Commit 0c65b289 authored by Jiří Klimeš's avatar Jiří Klimeš

cli: add 'nmcli connection clone' for cloning connections (bgo #757627)

Synopsis:
nmcli connection clone [--temporary] [id|uuid|path] <ID> <new name>

It copies the <ID> connection as <new name>. The command is very useful
if there is a connection, but another one is needed for a related
configuration. One can copy the existing profile and modify it for the
new situation.

For example:
$ nmcli con clone main-eth second-eth
$ nmcli con modify second-eth connection.interface-name em4

https://bugzilla.gnome.org/show_bug.cgi?id=757627
parent f9024443
......@@ -251,6 +251,7 @@ usage (void)
" down [id | uuid | path | apath] <ID> ...\n\n"
" add COMMON_OPTIONS TYPE_SPECIFIC_OPTIONS SLAVE_OPTIONS IP_OPTIONS [-- ([+|-]<setting>.<property> <value>)+]\n\n"
" modify [--temporary] [id | uuid | path] <ID> ([+|-]<setting>.<property> <value>)+\n\n"
" clone [--temporary] [id | uuid | path ] <ID> <new name>\n\n"
" edit [id | uuid | path] <ID>\n"
" edit [type <new_con_type>] [con-name <new_con_name>]\n\n"
" delete [id | uuid | path] <ID>\n\n"
......@@ -427,6 +428,18 @@ usage_connection_modify (void)
"nmcli con mod bond0 -bond.options downdelay\n\n"));
}
static void
usage_connection_clone (void)
{
g_printerr (_("Usage: nmcli connection clone { ARGUMENTS | help }\n"
"\n"
"ARGUMENTS := [--temporary] [id | uuid | path] <ID> <new name>\n"
"\n"
"Clone an existing connection profile. The newly created connection will be\n"
"the exact copy of the <ID>, except the uuid property (will be generated) and\n"
"id (provided as <new name> argument).\n\n"));
}
static void
usage_connection_edit (void)
{
......@@ -488,6 +501,8 @@ usage_connection_second_level (const char *cmd)
usage_connection_add ();
else if (matches (cmd, "modify") == 0)
usage_connection_modify ();
else if (matches (cmd, "clone") == 0)
usage_connection_clone ();
else if (matches (cmd, "edit") == 0)
usage_connection_edit ();
else if (matches (cmd, "delete") == 0)
......@@ -9145,6 +9160,142 @@ finish:
return nmc->return_value;
}
typedef struct {
NmCli *nmc;
char *orig_id;
char *orig_uuid;
char *con_id;
} CloneConnectionInfo;
static void
clone_connection_cb (GObject *client,
GAsyncResult *result,
gpointer user_data)
{
CloneConnectionInfo *info = (CloneConnectionInfo *) user_data;
NmCli *nmc = info->nmc;
NMRemoteConnection *connection;
GError *error = NULL;
connection = nm_client_add_connection_finish (NM_CLIENT (client), result, &error);
if (error) {
g_string_printf (nmc->return_text,
_("Error: Failed to add '%s' connection: %s"),
info->con_id, error->message);
g_error_free (error);
nmc->return_value = NMC_RESULT_ERROR_CON_ACTIVATION;
} else {
g_print (_("%s (%s) cloned as %s (%s).\n"),
info->orig_id,
info->orig_uuid,
nm_connection_get_id (NM_CONNECTION (connection)),
nm_connection_get_uuid (NM_CONNECTION (connection)));
g_object_unref (connection);
}
g_free (info->con_id);
g_free (info->orig_id);
g_free (info->orig_uuid);
g_slice_free (CloneConnectionInfo, info);
quit ();
}
static NMCResultCode
do_connection_clone (NmCli *nmc, gboolean temporary, int argc, char **argv)
{
NMConnection *connection = NULL;
NMConnection *new_connection = NULL;
NMSettingConnection *s_con;
CloneConnectionInfo *info;
const char *name;
const char *new_name;
char *name_ask = NULL;
char *new_name_ask = NULL;
const char *selector = NULL;
char *uuid;
if (argc == 0) {
if (nmc->ask) {
name = name_ask = nmc_readline (PROMPT_CONNECTION);
new_name = new_name_ask = nmc_readline ("New connection name: ");
} else {
g_string_printf (nmc->return_text, _("Error: No arguments provided."));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
} else {
if ( strcmp (*argv, "id") == 0
|| strcmp (*argv, "uuid") == 0
|| strcmp (*argv, "path") == 0) {
selector = *argv;
if (next_arg (&argc, &argv) != 0) {
g_string_printf (nmc->return_text, _("Error: %s argument is missing."),
selector);
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
name = *argv;
}
name = *argv;
if (!name) {
g_string_printf (nmc->return_text, _("Error: connection ID is missing."));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
if (next_arg (&argc, &argv) != 0) {
g_string_printf (nmc->return_text, _("Error: <new name> argument is missing."));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
new_name = *argv;
}
connection = nmc_find_connection (nmc->connections, selector, name, NULL);
if (!connection) {
g_string_printf (nmc->return_text, _("Error: Unknown connection '%s'."), name);
nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
goto finish;
}
/* Copy the connection */
new_connection = nm_simple_connection_new_clone (connection);
s_con = nm_connection_get_setting_connection (new_connection);
g_assert (s_con);
uuid = nm_utils_uuid_generate ();
g_object_set (s_con,
NM_SETTING_CONNECTION_ID, new_name,
NM_SETTING_CONNECTION_UUID, uuid,
NULL);
g_free (uuid);
/* Merge secrets into the new connection */
update_secrets_in_connection (NM_REMOTE_CONNECTION (connection), new_connection);
info = g_slice_new0 (CloneConnectionInfo);
info->nmc = nmc;
info->orig_id = g_strdup (nm_connection_get_id (connection));
info->orig_uuid = g_strdup (nm_connection_get_uuid (connection));
info->con_id = g_strdup (nm_connection_get_id (new_connection));
/* Add the new cloned connection to NetworkManager */
add_new_connection (!temporary,
nmc->client,
new_connection,
clone_connection_cb,
info);
nmc->should_wait = TRUE;
finish:
if (new_connection)
g_object_unref (new_connection);
g_free (name_ask);
g_free (new_name_ask);
return nmc->return_value;
}
static void
delete_cb (GObject *con, GAsyncResult *result, gpointer user_data)
{
......@@ -9604,6 +9755,15 @@ do_connections (NmCli *nmc, int argc, char **argv)
next_arg (&argc, &argv);
}
nmc->return_value = do_connection_modify (nmc, temporary, argc, argv);
} else if (matches (*argv, "clone") == 0) {
gboolean temporary = FALSE;
next_arg (&argc, &argv);
if (nmc_arg_is_option (*argv, "temporary")) {
temporary = TRUE;
next_arg (&argc, &argv);
}
nmc->return_value = do_connection_clone (nmc, temporary, argc, argv);
} else {
usage ();
g_string_printf (nmc->return_text, _("Error: '%s' is not valid 'connection' command."), *argv);
......
......@@ -870,7 +870,7 @@ _nmcli()
;;
c|co|con|conn|conne|connec|connect|connecti|connectio|connection)
if [[ ${#words[@]} -eq 2 ]]; then
_nmcli_compl_COMMAND "$command" show up down add modify edit delete reload load
_nmcli_compl_COMMAND "$command" show up down add modify clone edit delete reload load
elif [[ ${#words[@]} -gt 2 ]]; then
case "$command" in
s|sh|sho|show)
......@@ -1252,6 +1252,34 @@ _nmcli()
return 0
fi
;;
c|cl|clo|clon|clone)
if [[ ${#words[@]} -eq 3 ]]; then
_nmcli_compl_COMMAND_nl "${words[2]}" "$(printf "id\nuuid\npath\n%s" "$(_nmcli_con_show NAME)")" temporary
elif [[ ${#words[@]} -gt 3 ]]; then
_nmcli_array_delete_at words 0 1
LONG_OPTIONS=(help temporary)
HELP_ONLY_AS_FIRST=1
_nmcli_compl_OPTIONS
case $? in
0)
return 0
;;
1)
if [[ "$HELP_ONLY_AS_FIRST" == 1 ]]; then
_nmcli_compl_COMMAND_nl "${words[2]}" "$(printf "id\nuuid\npath\n%s" "$(_nmcli_con_show NAME)")" "${LONG_OPTIONS[@]}"
fi
return 0
;;
esac
OPTIONS=(id uuid path)
_nmcli_compl_ARGS_CONNECTION && return 0
return 0
fi
;;
de|del|dele|delet|delete)
if [[ ${#words[@]} -eq 3 ]]; then
_nmcli_compl_COMMAND_nl "${words[2]}" "$(printf "id\nuuid\npath\n%s" "$(_nmcli_con_show NAME)")"
......
......@@ -739,6 +739,20 @@ The changes to the connection profile will be saved persistently by
NetworkManager, unless \fI--temporary\fP option is provided, in which case the
changes won't persist over NetworkManager restart.
.TP
.B clone [--temporary] [ id | uuid | path ] <ID> <new name>
.br
Clone a connection. The connection to be cloned is identified by its
name, UUID or D-Bus path. If <ID> is ambiguous, a keyword \fIid\fP,
\fIuuid\fP or \fIpath\fP can be used. See \fBconnection show\fP above for
the description of the <ID>-specifying keywords. \fI<new name>\fP is the name
of the new cloned connection. The new connection will be the exact copy except
the connection.id (\fI<new name>\fP) and connection.uuid (generated)
properties.
.br
The new connection profile will be saved as persistent unless \fI--temporary\fP
option is specified, in which case the new profile won't outlive NetworkManager
restart.
.TP
.B delete [ id | uuid | path ] <ID> ...
.br
Delete a configured connection. The connection to be deleted is identified by
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment