cli: add 'nmcli connection clone' for cloning connections (bgo #757627)
authorJiří Klimeš <jklimes@redhat.com>
Thu, 5 Nov 2015 11:03:49 +0000 (12:03 +0100)
committerJiří Klimeš <jklimes@redhat.com>
Wed, 18 Nov 2015 08:37:44 +0000 (09:37 +0100)
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

clients/cli/connections.c
clients/cli/nmcli-completion
man/nmcli.1.in

index cb95694..94350b0 100644 (file)
@@ -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);
index c864ee4..90365a0 100644 (file)
@@ -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)")"
index 1881310..ed07324 100644 (file)
@@ -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