diff --git a/Makefile.am b/Makefile.am index 72f224fc20..b2c7e9a746 100644 --- a/Makefile.am +++ b/Makefile.am @@ -5318,6 +5318,7 @@ EXTRA_DIST += \ src/tests/client/test-client.check-on-disk/test_002.expected \ src/tests/client/test-client.check-on-disk/test_003.expected \ src/tests/client/test-client.check-on-disk/test_004.expected \ + src/tests/client/test-client.check-on-disk/test_offline.expected \ \ src/tests/client/meson.build \ $(NULL) diff --git a/NEWS b/NEWS index 69c62edef2..14aea8d9f7 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,9 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! * Drop unused, internal systemd DHCPv4 client. This is long replaced by nettools' n-dhcp4 implementation. +* The nmcli command now supports --offline argument with "add" and + "modify" commands, allowing operation on keyfile-formatted connection + profiles without the service running (e.g. during system provisioning). ============================================= NetworkManager-1.38 diff --git a/man/nmcli-examples.xml b/man/nmcli-examples.xml index 5744ad12bb..0afa9797a3 100644 --- a/man/nmcli-examples.xml +++ b/man/nmcli-examples.xml @@ -614,6 +614,32 @@ Connection 'ethernet-4' (de89cdeb-a3e1-4d53-8fa0-c22546c775f4) successfully $ nmcli connection add type bluetooth con-name "My Bluetooth Hotspot" autoconnect no ifname btnap0 bluetooth.type nap ipv4.method shared ipv6.method shared + Offline use +$ nmcli --offline con add type ethernet ' + conn.id eth0 \ + conn.interface-name eth0 \ + >/sysroot/etc/NetworkManager/system-connections/eth0.nmconnection + + Creates a connection file in keyfile format without using the NetworkManager service. + This allows for use of familiar nmcli syntax in situations + where the service is not running, such as during system installation of image + provisioning and ensures the resulting file is correctly formatted. + +$ nmcli --offline con modify type ethernet ' + conn.id eth0-ipv6 \ + ipv4.method disabled \ + </sysroot/etc/NetworkManager/system-connections/eth0.nmconnection \ + >/sysroot/etc/NetworkManager/system-connections/eth0-ipv6.nmconnection + + Read and write a connection file without using the NetworkManager service, modifying + some properties along the way. + + + This allows templating of the connection profiles using familiar + nmcli syntax in situations where the service is not running. + + + diff --git a/man/nmcli.xml b/man/nmcli.xml index d4f27b0b37..ba89b29112 100644 --- a/man/nmcli.xml +++ b/man/nmcli.xml @@ -314,6 +314,23 @@ + + + + + + + Work without a daemon. Makes connection add and + connection modify commands accept and produce connection + data via standard input/output. Ordinarily, nmcli would communicate with + the NetworkManager service. + + The connection data format (keyfile) is described in + nm-settings-keyfile5 + manual. + + + @@ -928,7 +945,7 @@ - ID + ID option value @@ -962,7 +979,14 @@ The connection is identified by its name, UUID or D-Bus path. If ID is ambiguous, a keyword , - or can be used. + or can be used. + The ID is not used with the global + option. + + When the global is used, the + command reads the connection from the standard input and prints the + modified connection to standard output instead of making the + the NetworkManager daemon act upon specified connection. @@ -1096,6 +1120,10 @@ + + When the global is used, the + command prints the resulting connection to standard output instead of + actually adding the connection via the NetworkManager daemon. diff --git a/src/nmcli/common.c b/src/nmcli/common.c index f42f76e4c2..710914e211 100644 --- a/src/nmcli/common.c +++ b/src/nmcli/common.c @@ -16,6 +16,8 @@ #include #include #endif +#include + #include "libnm-client-aux-extern/nm-libnm-aux.h" #include "libnmc-base/nm-vpn-helpers.h" @@ -469,7 +471,8 @@ nmc_find_connection(const GPtrArray *connections, goto found; } - if (NM_IN_STRSET(filter_type, NULL, "filename")) { + if (NM_IS_REMOTE_CONNECTION(connections->pdata[i]) + && NM_IN_STRSET(filter_type, NULL, "filename")) { v = nm_remote_connection_get_filename(NM_REMOTE_CONNECTION(connections->pdata[i])); if (complete && (filter_type || *filter_val)) nmc_complete_strings(filter_val, v); @@ -1273,12 +1276,157 @@ got_client(GObject *source_object, GAsyncResult *res, gpointer user_data) nm_g_slice_free(call); } +typedef struct { + GString *str; + char buf[512]; + CmdCall *call; +} CmdStdinData; + +static void read_offline_connection_next(GInputStream *stream, CmdStdinData *data); + +static void +read_offline_connection_chunk(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GInputStream *stream = G_INPUT_STREAM(source_object); + CmdStdinData *data = user_data; + CmdCall *call = data->call; + gs_unref_object GTask *task = NULL; + nm_auto_unref_keyfile GKeyFile *keyfile = NULL; + gs_free char *base_dir = NULL; + GError *error = NULL; + gssize bytes_read; + NMConnection *connection; + NmCli *nmc; + + bytes_read = g_input_stream_read_finish(stream, res, &error); + if (bytes_read > 0) { + /* We need to read more. */ + g_string_append_len(data->str, data->buf, bytes_read); + read_offline_connection_next(stream, data); + return; + } + + /* End reached. */ + + task = g_steal_pointer(&call->task); + nmc = g_task_get_task_data(task); + nmc->should_wait--; + + if (bytes_read == -1) { + g_task_return_error(task, error); + goto finish; + } + + keyfile = g_key_file_new(); + if (!g_key_file_load_from_data(keyfile, + data->str->str, + data->str->len, + G_KEY_FILE_NONE, + &error)) { + g_task_return_error(task, error); + goto finish; + } + + base_dir = g_get_current_dir(); + connection = + nm_keyfile_read(keyfile, base_dir, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, &error); + if (!connection) { + g_task_return_error(task, error); + goto finish; + } + + g_ptr_array_add(nmc->offline_connections, connection); + call->cmd->func(call->cmd, nmc, call->argc, (const char *const *) call->argv); + g_task_return_boolean(task, TRUE); + +finish: + g_strfreev(call->argv); + nm_g_slice_free(call); + g_string_free(data->str, TRUE); + nm_g_slice_free(data); +} + +static void +read_offline_connection_next(GInputStream *stream, CmdStdinData *data) +{ + g_input_stream_read_async(stream, + data->buf, + sizeof(data->buf), + G_PRIORITY_DEFAULT, + NULL, + read_offline_connection_chunk, + data); +} + +static void +read_offline_connection(CmdCall *call) +{ + gs_unref_object GInputStream *stream = NULL; + CmdStdinData *data; + + stream = g_unix_input_stream_new(STDIN_FILENO, TRUE); + data = g_slice_new(CmdStdinData); + data->call = call; + data->str = g_string_new_len(NULL, sizeof(data->buf)); + + read_offline_connection_next(stream, data); +} + +static NMConnection * +dummy_offline_connection(void) +{ + NMConnection *connection; + + connection = nm_simple_connection_new(); + nm_connection_add_setting(connection, nm_setting_connection_new()); + return connection; +} + static void call_cmd(NmCli *nmc, GTask *task, const NMCCommand *cmd, int argc, const char *const *argv) { CmdCall *call; - if (nmc->client || !cmd->needs_client) { + if (nmc->offline) { + if (!cmd->supports_offline) { + g_task_return_new_error(task, + NMCLI_ERROR, + NMC_RESULT_ERROR_USER_INPUT, + _("Error: command doesn't support --offline mode.")); + g_object_unref(task); + return; + } + + if (!nmc->offline_connections) + nmc->offline_connections = g_ptr_array_new_full(1, g_object_unref); + + if (cmd->needs_offline_conn) { + g_return_if_fail(nmc->offline_connections->len == 0); + + if (nmc->complete) { + g_ptr_array_add(nmc->offline_connections, dummy_offline_connection()); + cmd->func(cmd, nmc, argc, argv); + g_task_return_boolean(task, TRUE); + g_object_unref(task); + return; + } + + nmc->should_wait++; + call = g_slice_new(CmdCall); + *call = (CmdCall){ + .cmd = cmd, + .argc = argc, + .argv = nm_strv_dup(argv, argc, TRUE), + .task = task, + }; + read_offline_connection(call); + return; + } else { + cmd->func(cmd, nmc, argc, argv); + g_task_return_boolean(task, TRUE); + g_object_unref(task); + } + } else if (nmc->client || !cmd->needs_client) { /* Check whether NetworkManager is running */ if (cmd->needs_nm_running && !nm_client_get_nm_running(nmc->client)) { g_task_return_new_error(task, diff --git a/src/nmcli/connections.c b/src/nmcli/connections.c index ecf8e2e298..648583eb1d 100644 --- a/src/nmcli/connections.c +++ b/src/nmcli/connections.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" @@ -18,6 +18,7 @@ #include #endif #include +#include #include "libnm-glib-aux/nm-dbus-aux.h" #include "libnmc-base/nm-client-utils.h" @@ -137,6 +138,131 @@ NM_AUTO_DEFINE_FCN(AddConnectionInfo *, /*****************************************************************************/ +static guint progress_id = 0; /* ID of event source for displaying progress */ + +static void +quit(void) +{ + if (nm_clear_g_source(&progress_id)) + nmc_terminal_erase_line(); + g_main_loop_quit(loop); +} + +typedef struct { + char *data; + gsize written; + gsize length; + NmCli *nmc; +} PrintConnData; + +static void print_connection_chunk(GOutputStream *stream, PrintConnData *print_conn_data); + +static void +print_connection_done(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GOutputStream *stream = G_OUTPUT_STREAM(source_object); + PrintConnData *print_conn_data = user_data; + NmCli *nmc = print_conn_data->nmc; + GError *error = NULL; + gssize written; + + written = g_output_stream_write_finish(stream, res, &error); + if (written == -1) { + g_string_printf(nmc->return_text, + _("Error: Error writting connection: %s"), + error->message); + nmc->return_value = NMC_RESULT_ERROR_UNKNOWN; + nmc->should_wait--; + quit(); + return; + } + + print_conn_data->written += written; + if (print_conn_data->written != print_conn_data->length) { + g_return_if_fail(written); + g_return_if_fail(print_conn_data->written < print_conn_data->length); + + print_connection_chunk(stream, print_conn_data); + return; + } + + g_free(print_conn_data->data); + g_slice_free(PrintConnData, print_conn_data); + + nmc->should_wait--; + quit(); +} + +static void +print_connection_chunk(GOutputStream *stream, PrintConnData *print_conn_data) +{ + g_output_stream_write_async(stream, + print_conn_data->data + print_conn_data->written, + print_conn_data->length - print_conn_data->written, + G_PRIORITY_DEFAULT, + NULL, + print_connection_done, + print_conn_data); +} + +static void +nmc_print_connection_and_quit(NmCli *nmc, NMConnection *connection) +{ + gs_free_error GError *error = NULL; + nm_auto_unref_keyfile GKeyFile *keyfile = NULL; + gs_unref_object GOutputStream *stream = NULL; + PrintConnData *print_conn_data; + + if (!nm_connection_normalize(connection, NULL, NULL, &error)) + goto error; + + keyfile = nm_keyfile_write(connection, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, &error); + if (!keyfile) + goto error; + + stream = g_unix_output_stream_new(STDOUT_FILENO, FALSE); + print_conn_data = g_slice_new(PrintConnData); + print_conn_data->data = g_key_file_to_data(keyfile, &print_conn_data->length, NULL); + print_conn_data->written = 0; + print_conn_data->nmc = nmc; + print_connection_chunk(stream, print_conn_data); + return; + +error: + g_string_printf(nmc->return_text, _("Error: Error writting connection: %s"), error->message); + nmc->return_value = NMC_RESULT_ERROR_UNKNOWN; + nmc->should_wait--; + quit(); +} + +static const GPtrArray * +nmc_get_connections(const NmCli *nmc) +{ + if (nmc->offline) { + g_return_val_if_fail(!nmc->client, nmc->offline_connections); + return nmc->offline_connections; + } else { + g_return_val_if_fail(nmc->client, NULL); + return nm_client_get_connections(nmc->client); + } +} + +static const GPtrArray * +nmc_get_active_connections(const NmCli *nmc) +{ + static const GPtrArray offline_active_connections = {.len = 0}; + + if (nmc->offline) { + g_return_val_if_fail(!nmc->client, &offline_active_connections); + return &offline_active_connections; + } else { + g_return_val_if_fail(nmc->client, &offline_active_connections); + return nm_client_get_active_connections(nmc->client); + } +} + +/*****************************************************************************/ + /* Essentially a version of nm_setting_connection_get_connection_type() that * prefers an alias instead of the settings name when in pretty print mode. * That is so that we print "wifi" instead of "802-11-wireless" in "nmcli c". */ @@ -942,8 +1068,6 @@ const NmcMetaGenericInfo *const nmc_fields_con_active_details_groups[] = { #define CON_SHOW_DETAIL_GROUP_PROFILE "profile" #define CON_SHOW_DETAIL_GROUP_ACTIVE "active" -static guint progress_id = 0; /* ID of event source for displaying progress */ - /* for readline TAB completion in editor */ typedef struct { NmCli *nmc; @@ -1305,14 +1429,6 @@ usage_connection_migrate(void) "such as \"keyfile\" (default) or \"ifcfg-rh\".\n\n")); } -static void -quit(void) -{ - if (nm_clear_g_source(&progress_id)) - nmc_terminal_erase_line(); - g_main_loop_quit(loop); -} - static char * construct_header_name(const char *base, const char *spec) { @@ -1951,7 +2067,7 @@ con_show_get_items(NmCli *nmc, gboolean active_only, gboolean show_active_fields row_hash = g_hash_table_new(nm_direct_hash, NULL); - arr = nm_client_get_connections(nmc->client); + arr = nmc_get_connections(nmc); for (i = 0; i < arr->len; i++) { /* Note: libnm will not expose connection that are invisible * to the user but currently inactive. @@ -1970,7 +2086,7 @@ con_show_get_items(NmCli *nmc, gboolean active_only, gboolean show_active_fields _metagen_con_show_row_data_new_for_connection(c, show_active_fields)); } - arr = nm_client_get_active_connections(nmc->client); + arr = nmc_get_active_connections(nmc); for (i = 0; i < arr->len; i++) { NMActiveConnection *ac = arr->pdata[i]; @@ -2118,6 +2234,11 @@ get_connection(NmCli *nmc, NM_SET_OUT(out_selector, NULL); NM_SET_OUT(out_value, NULL); + if (nmc->offline_connections && nmc->offline_connections->len) + return nmc->offline_connections->pdata[0]; + + g_return_val_if_fail(!nmc->offline, NULL); + if (*argc == 0) { g_set_error_literal(error, NMCLI_ERROR, @@ -2258,7 +2379,7 @@ do_connections_show(const NMCCommand *cmd, NmCli *nmc, int argc, const char *con } else { gboolean new_line = FALSE; gboolean without_fields = (nmc->required_fields == NULL); - const GPtrArray *active_cons = nm_client_get_active_connections(nmc->client); + const GPtrArray *active_cons = nmc_get_active_connections(nmc); /* multiline mode is default for 'connection show ' */ if (!nmc->mode_specified) @@ -2314,7 +2435,7 @@ do_connections_show(const NMCCommand *cmd, NmCli *nmc, int argc, const char *con } /* Try to find connection by id, uuid or path first */ - connections = nm_client_get_connections(nmc->client); + connections = nmc_get_connections(nmc); con = nmc_find_connection(connections, selector, *argv, @@ -2451,7 +2572,7 @@ get_default_active_connection(NmCli *nmc, NMDevice **device) g_return_val_if_fail(device, NULL); g_return_val_if_fail(*device == NULL, NULL); - connections = nm_client_get_active_connections(nmc->client); + connections = nmc_get_active_connections(nmc); for (i = 0; i < connections->len; i++) { NMActiveConnection *candidate = g_ptr_array_index(connections, i); const GPtrArray *devices; @@ -3275,7 +3396,7 @@ do_connection_down(const NMCCommand *cmd, NmCli *nmc, int argc, const char *cons } /* Get active connections */ - active_cons = nm_client_get_active_connections(nmc->client); + active_cons = nmc_get_active_connections(nmc); while (arg_num > 0) { const char *selector = NULL; @@ -3925,7 +4046,7 @@ set_default_interface_name(NmCli *nmc, NMSettingConnection *s_con) const GPtrArray *connections; gs_free char *ifname = NULL; - connections = nm_client_get_connections(nmc->client); + connections = nmc_get_connections(nmc); ifname = unique_master_iface_ifname(connections, default_name); g_object_set(s_con, NM_SETTING_CONNECTION_INTERFACE_NAME, ifname, NULL); } @@ -4488,7 +4609,7 @@ set_connection_master(NmCli *nmc, } slave_type = nm_setting_connection_get_slave_type(s_con); - connections = nm_client_get_connections(nmc->client); + connections = nmc_get_connections(nmc); value = normalized_master_for_slave(connections, value, slave_type, &slave_type); if (!set_property(nmc->client, @@ -5264,12 +5385,9 @@ connection_warnings(NmCli *nmc, NMConnection *connection) if (deprecated) g_printerr(_("Warning: %s.\n"), deprecated); - connections = nm_client_get_connections(nmc->client); - if (!connections) - return; - - id = nm_connection_get_id(connection); - found = 0; + connections = nmc_get_connections(nmc); + id = nm_connection_get_id(connection); + found = 0; for (i = 0; i < connections->len; i++) { NMConnection *candidate = NM_CONNECTION(connections->pdata[i]); @@ -5347,15 +5465,6 @@ add_connection(NMClient *client, user_data); } -static void -update_connection(NMRemoteConnection *connection, - gboolean temporary, - GAsyncReadyCallback callback, - gpointer user_data) -{ - nm_remote_connection_commit_changes_async(connection, !temporary, NULL, callback, user_data); -} - static gboolean is_single_word(const char *line) { @@ -5660,6 +5769,20 @@ again: return TRUE; } +static void +nmc_add_connection(NmCli *nmc, NMConnection *connection, gboolean temporary) +{ + if (nmc->offline) { + nmc_print_connection_and_quit(nmc, connection); + } else { + add_connection(nmc->client, + connection, + temporary, + add_connection_cb, + _add_connection_info_new(nmc, NULL, connection)); + } +} + static void do_connection_add(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv) { @@ -5747,7 +5870,7 @@ read_properties: gs_free char *default_name = NULL; const GPtrArray *connections; - connections = nm_client_get_connections(nmc->client); + connections = nmc_get_connections(nmc); try_name = ifname ? g_strdup_printf("%s-%s", get_name_alias_toplevel(type, slave_type), ifname) : g_strdup(get_name_alias_toplevel(type, slave_type)); @@ -5812,11 +5935,7 @@ read_properties: } } - add_connection(nmc->client, - connection, - !save_bool, - add_connection_cb, - _add_connection_info_new(nmc, NULL, connection)); + nmc_add_connection(nmc, connection, !save_bool); nmc->should_wait++; finish: @@ -5838,7 +5957,7 @@ uuid_display_hook(char **array, int len, int max_len) char *tmp; const char *id; for (i = 1; i <= len; i++) { - connections = nm_client_get_connections(nmc_tab_completion.nmc->client); + connections = nmc_get_connections(nmc_tab_completion.nmc); con = nmc_find_connection(connections, "uuid", array[i], NULL, FALSE); id = con ? nm_connection_get_id(con) : NULL; if (id) { @@ -6172,7 +6291,7 @@ gen_vpn_uuids(const char *text, int state) const char **uuids; char *ret; - connections = nm_client_get_connections(nm_cli_global_readline->client); + connections = nmc_get_connections(nm_cli_global_readline); if (connections->len < 1) return NULL; @@ -6189,7 +6308,7 @@ gen_vpn_ids(const char *text, int state) const char **ids; char *ret; - connections = nm_client_get_connections(nm_cli_global_readline->client); + connections = nmc_get_connections(nm_cli_global_readline); if (connections->len < 1) return NULL; @@ -8335,7 +8454,11 @@ editor_menu_main(NmCli *nmc, NMConnection *connection, const char *connection_ty /* Save/update already saved (existing) connection */ nm_connection_replace_settings_from_connection(NM_CONNECTION(rem_con), connection); - update_connection(rem_con, temporary, update_connection_editor_cb, NULL); + nm_remote_connection_commit_changes_async(rem_con, + !temporary, + NULL, + update_connection_editor_cb, + NULL); handler_id = g_signal_connect(rem_con, NM_CONNECTION_CHANGED, @@ -8749,7 +8872,7 @@ do_connection_edit(const NMCCommand *cmd, NmCli *nmc, int argc, const char *cons /* Use ' ' and '.' as word break characters */ rl_completer_word_break_characters = ". "; - connections = nm_client_get_connections(nmc->client); + connections = nmc_get_connections(nmc); if (!con) { if (con_id && !con_uuid && !con_path && !con_filename) { @@ -8930,11 +9053,24 @@ modify_connection_cb(GObject *connection, GAsyncResult *result, gpointer user_da quit(); } +static void +nmc_update_connection(NmCli *nmc, NMConnection *connection, gboolean temporary) +{ + if (nmc->offline) { + nmc_print_connection_and_quit(nmc, connection); + } else { + nm_remote_connection_commit_changes_async(NM_REMOTE_CONNECTION(connection), + !temporary, + NULL, + modify_connection_cb, + nmc); + } +} + static void do_connection_modify(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv) { NMConnection *connection = NULL; - NMRemoteConnection *rc = NULL; gs_free_error GError *error = NULL; gboolean temporary = FALSE; @@ -8950,25 +9086,19 @@ do_connection_modify(const NMCCommand *cmd, NmCli *nmc, int argc, const char *co return; } - rc = nm_client_get_connection_by_uuid(nmc->client, nm_connection_get_uuid(connection)); - if (!rc) { - g_string_printf(nmc->return_text, - _("Error: Unknown connection '%s'."), - nm_connection_get_uuid(connection)); - nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND; - return; - } - - if (!nmc_process_connection_properties(nmc, NM_CONNECTION(rc), &argc, &argv, TRUE, &error)) { - g_string_assign(nmc->return_text, error->message); - nmc->return_value = error->code; - return; + /* Don't insist on having argument if we're running in offline mode. */ + if (!nmc->offline || argc > 0) { + if (!nmc_process_connection_properties(nmc, connection, &argc, &argv, TRUE, &error)) { + g_string_assign(nmc->return_text, error->message); + nmc->return_value = error->code; + return; + } } if (nmc->complete) return; - update_connection(rc, temporary, modify_connection_cb, nmc); + nmc_update_connection(nmc, connection, temporary); nmc->should_wait++; } @@ -9266,7 +9396,7 @@ do_connection_monitor(const NMCCommand *cmd, NmCli *nmc, int argc, const char *c /* nmc_do_cmd() should not call this with argc=0. */ g_return_if_fail(!nmc->complete); - connections = nm_client_get_connections(nmc->client); + connections = nmc_get_connections(nmc); } else { while (argc > 0) { if (!get_connection(nmc, &argc, &argv, NULL, NULL, &found_cons, &error)) { @@ -9767,7 +9897,7 @@ do_connection_migrate(const NMCCommand *cmd, NmCli *nmc, int argc, const char *c if (!found_cons) { /* No connections specified explicitly? Fine, add all. */ found_cons = g_ptr_array_new(); - connections = nm_client_get_connections(nmc->client); + connections = nmc_get_connections(nmc); for (i = 0; i < connections->len; i++) { connection = connections->pdata[i]; g_ptr_array_add(found_cons, connection); @@ -9814,7 +9944,7 @@ gen_func_connection_names(const char *text, int state) const char **connection_names; char *ret; - connections = nm_client_get_connections(nm_cli_global_readline->client); + connections = nmc_get_connections(nm_cli_global_readline); if (connections->len == 0) return NULL; @@ -9840,7 +9970,7 @@ gen_func_active_connection_names(const char *text, int state) if (!nm_cli_global_readline->client) return NULL; - acs = nm_client_get_active_connections(nm_cli_global_readline->client); + acs = nmc_get_active_connections(nm_cli_global_readline); if (!acs || acs->len == 0) return NULL; @@ -9904,12 +10034,12 @@ nmc_command_func_connection(const NMCCommand *cmd, NmCli *nmc, int argc, const c {"show", do_connections_show, usage_connection_show, TRUE, TRUE}, {"up", do_connection_up, usage_connection_up, TRUE, TRUE}, {"down", do_connection_down, usage_connection_down, TRUE, TRUE}, - {"add", do_connection_add, usage_connection_add, TRUE, TRUE}, + {"add", do_connection_add, usage_connection_add, TRUE, TRUE, TRUE}, {"edit", do_connection_edit, usage_connection_edit, TRUE, TRUE}, {"delete", do_connection_delete, usage_connection_delete, TRUE, TRUE}, {"reload", do_connection_reload, usage_connection_reload, FALSE, FALSE}, {"load", do_connection_load, usage_connection_load, TRUE, TRUE}, - {"modify", do_connection_modify, usage_connection_modify, TRUE, TRUE}, + {"modify", do_connection_modify, usage_connection_modify, TRUE, TRUE, TRUE, TRUE}, {"clone", do_connection_clone, usage_connection_clone, TRUE, TRUE}, {"import", do_connection_import, usage_connection_import, TRUE, TRUE}, {"export", do_connection_export, usage_connection_export, TRUE, TRUE}, diff --git a/src/nmcli/nmcli.c b/src/nmcli/nmcli.c index 96b8ec4a13..bc37a7f0f8 100644 --- a/src/nmcli/nmcli.c +++ b/src/nmcli/nmcli.c @@ -726,7 +726,7 @@ process_command_line(NmCli *nmc, int argc, char **argv_orig) {"monitor", nmc_command_func_monitor, NULL, TRUE, FALSE}, {"networking", nmc_command_func_networking, NULL, FALSE, FALSE}, {"radio", nmc_command_func_radio, NULL, FALSE, FALSE}, - {"connection", nmc_command_func_connection, NULL, FALSE, FALSE}, + {"connection", nmc_command_func_connection, NULL, FALSE, FALSE, TRUE}, {"device", nmc_command_func_device, NULL, FALSE, FALSE}, {"agent", nmc_command_func_agent, NULL, FALSE, FALSE}, {NULL, nmc_command_func_overview, usage, TRUE, TRUE}, @@ -761,15 +761,16 @@ process_command_line(NmCli *nmc, int argc, char **argv_orig) if (argc == 1 && nmc->complete) { nmc_complete_strings(argv[0], + "--overview", + "--offline", "--terse", "--pretty", "--mode", - "--overview", "--colors", "--escape", "--fields", - "--nocheck", "--get-values", + "--nocheck", "--wait", "--version", "--help"); @@ -783,6 +784,8 @@ process_command_line(NmCli *nmc, int argc, char **argv_orig) if (matches_arg(nmc, &argc, &argv, "-overview", NULL)) { nmc->nmc_config_mutable.overview = TRUE; + } else if (matches_arg(nmc, &argc, &argv, "-offline", NULL)) { + nmc->offline = TRUE; } else if (matches_arg(nmc, &argc, &argv, "-terse", NULL)) { if (nmc->nmc_config.print_output == NMC_PRINT_TERSE) { g_string_printf(nmc->return_text, @@ -1011,6 +1014,8 @@ nmc_cleanup(NmCli *nmc) nm_clear_g_free(&nmc->palette_buffer); + nm_clear_pointer(&nmc->offline_connections, g_ptr_array_unref); + nmc_polkit_agent_fini(nmc); } diff --git a/src/nmcli/nmcli.h b/src/nmcli/nmcli.h index 157aae9982..922e501418 100644 --- a/src/nmcli/nmcli.h +++ b/src/nmcli/nmcli.h @@ -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. */ #ifndef NMC_NMCLI_H @@ -84,23 +84,26 @@ typedef struct _NmcMetaGenericInfo NmcMetaGenericInfo; struct _NmcOutputField { const NMMetaAbstractInfo *info; - int width; /* Width in screen columns */ - void *value; /* Value of current field - char* or char** (NULL-terminated array) */ - gboolean value_is_array; /* Whether value is char** instead of char* */ - gboolean free_value; /* Whether to free the value */ - NmcOfFlags flags; /* Flags - whether and how to print values/field names/headers */ - NMMetaColor color; /* Use this color to print value */ + + int width; /* Width in screen columns */ + void *value; /* Value of current field - char* or + * char** (NULL-terminated array) */ + bool value_is_array : 1; /* Whether value is char** instead of char* */ + bool free_value : 1; /* Whether to free the value */ + + NmcOfFlags flags; /* Flags - whether and how to print values/field names/headers */ + NMMetaColor color; /* Use this color to print value */ }; typedef struct _NmcConfig { - NMCPrintOutput print_output; /* Output mode */ - bool use_colors; /* Whether to use colors for output: option '--color' */ - bool multiline_output; /* Multiline output instead of default tabular */ - bool escape_values; /* Whether to escape ':' and '\' in terse tabular mode */ - bool in_editor; /* Whether running the editor - nmcli con edit' */ - bool - show_secrets; /* Whether to display secrets (both input and output): option '--show-secrets' */ - bool overview; /* Overview mode (hide default values) */ + NMCPrintOutput print_output; /* Output mode */ + bool use_colors; /* Whether to use colors for output: option '--color' */ + bool multiline_output : 1; /* Multiline output instead of default tabular */ + bool escape_values : 1; /* Whether to escape ':' and '\' in terse tabular mode */ + bool in_editor : 1; /* Whether running the editor - nmcli con edit' */ + bool show_secrets : 1; /* Whether to display secrets (both input + * and output): option '--show-secrets' */ + bool overview : 1; /* Overview mode (hide default values) */ NmcColorPalette palette; } NmcConfig; @@ -128,21 +131,27 @@ typedef struct _NmCli { GHashTable *pwds_hash; /* Hash table with passwords in passwd-file */ struct _NMPolkitListener *pk_listener; /* polkit agent listener */ - int should_wait; /* Semaphore indicating whether nmcli should not end or not yet */ - gboolean nowait_flag; /* '--nowait' option; used for passing to callbacks */ - gboolean mode_specified; /* Whether tabular/multiline mode was specified via '--mode' option */ + int should_wait; /* Semaphore indicating whether nmcli should not end or not yet */ + + bool nowait_flag : 1; /* '--nowait' option; used for passing to callbacks */ + bool mode_specified : 1; /* Whether tabular/multiline mode was specified via '--mode' option */ + bool offline : 1; /* Communicate the connection data over stdin/stdout + * instead of talking to the daemon. */ + bool ask : 1; /* Ask for missing parameters: option '--ask' */ + bool complete : 1; /* Autocomplete the command line */ + bool editor_status_line : 1; /* Whether to display status line in connection editor */ + bool editor_save_confirmation : 1; /* Whether to ask for confirmation on + * saving connections with 'autoconnect=yes' */ + union { const NmcConfig nmc_config; NmcConfig nmc_config_mutable; }; - char *required_fields; /* Required fields in output: '--fields' option */ - gboolean ask; /* Ask for missing parameters: option '--ask' */ - gboolean complete; /* Autocomplete the command line */ - gboolean editor_status_line; /* Whether to display status line in connection editor */ - gboolean - editor_save_confirmation; /* Whether to ask for confirmation on saving connections with 'autoconnect=yes' */ + char *required_fields; /* Required fields in output: '--fields' option */ char *palette_buffer; /* Buffer with sequences for terminal-colors.d(5)-based coloring. */ + + GPtrArray *offline_connections; } NmCli; extern const NmCli *const nm_cli_global_readline; @@ -176,8 +185,15 @@ typedef struct _NMCCommand { const char *cmd; void (*func)(const struct _NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv); void (*usage)(void); - bool needs_client; - bool needs_nm_running; + + bool needs_client : 1; /* Ensure a client instance is there before calling + * the handler (unless --offline has been given). */ + bool needs_nm_running : 1; /* Client instance exists *and* the service is + * actually present on the bus. */ + bool supports_offline : 1; /* Run the handler without a client even if the + * comand usually requires one if --offline option was used. */ + bool needs_offline_conn : 1; /* With --online, read in a keyfile from + * standard input before dispatching the handler. */ } NMCCommand; void nmc_command_func_agent(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv); diff --git a/src/tests/client/test-client.check-on-disk/test_offline.expected b/src/tests/client/test-client.check-on-disk/test_offline.expected new file mode 100644 index 0000000000..1e95bfc4e6 --- /dev/null +++ b/src/tests/client/test-client.check-on-disk/test_offline.expected @@ -0,0 +1,137 @@ +size: 258 +location: src/tests/client/test-client.py:test_offline()/1 +cmd: $NMCLI g +lang: C +returncode: 1 +stderr: 136 bytes +>>> +Error: Could not create NMClient object: Key/Value pair 0, ?invalid?, in address element ?very:invalid? does not contain an equal sign. + +<<< +size: 319 +location: src/tests/client/test-client.py:test_offline()/2 +cmd: $NMCLI --offline c add type ethernet +lang: C +returncode: 0 +stdout: 169 bytes +>>> +[connection] +id=ethernet +uuid=UUID-WAS-HERE-BUT-IS-NO-MORE-SADLY +type=ethernet + +[ethernet] + +[ipv4] +method=auto + +[ipv6] +addr-gen-mode=stable-privacy +method=auto + +[proxy] + +<<< +size: 183 +location: src/tests/client/test-client.py:test_offline()/3 +cmd: $NMCLI --offline c show +lang: C +returncode: 2 +stderr: 47 bytes +>>> +Error: command doesn't support --offline mode. + +<<< +size: 178 +location: src/tests/client/test-client.py:test_offline()/4 +cmd: $NMCLI --offline g +lang: C +returncode: 2 +stderr: 47 bytes +>>> +Error: command doesn't support --offline mode. + +<<< +size: 176 +location: src/tests/client/test-client.py:test_offline()/5 +cmd: $NMCLI --offline +lang: C +returncode: 2 +stderr: 47 bytes +>>> +Error: command doesn't support --offline mode. + +<<< +size: 443 +location: src/tests/client/test-client.py:test_offline()/6 +cmd: $NMCLI --offline c add type wifi ssid lala 802-1x.eap pwd 802-1x.identity foo 802-1x.password bar +lang: C +returncode: 0 +stdout: 232 bytes +>>> +[connection] +id=wifi +uuid=UUID-WAS-HERE-BUT-IS-NO-MORE-SADLY +type=wifi + +[wifi] +mode=infrastructure +ssid=lala + +[802-1x] +eap=pwd; +identity=foo +password=bar + +[ipv4] +method=auto + +[ipv6] +addr-gen-mode=stable-privacy +method=auto + +[proxy] + +<<< +size: 481 +location: src/tests/client/test-client.py:test_offline()/7 +cmd: $NMCLI --offline c add type wifi ssid lala 802-1x.eap pwd 802-1x.identity foo 802-1x.password bar 802-1x.password-flags agent-owned +lang: C +returncode: 0 +stdout: 236 bytes +>>> +[connection] +id=wifi +uuid=UUID-WAS-HERE-BUT-IS-NO-MORE-SADLY +type=wifi + +[wifi] +mode=infrastructure +ssid=lala + +[802-1x] +eap=pwd; +identity=foo +password-flags=1 + +[ipv4] +method=auto + +[ipv6] +addr-gen-mode=stable-privacy +method=auto + +[proxy] + +<<< +size: 199 +location: src/tests/client/test-client.py:test_offline()/8 +cmd: $NMCLI --complete-args --offline conn modify ipv6.ad +lang: C +returncode: 0 +stdout: 34 bytes +>>> +ipv6.addresses +ipv6.addr-gen-mode + +<<< diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index d025d12ead..2e0387a9ff 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -202,6 +202,14 @@ class Util: t = basestring return isinstance(s, t) + @staticmethod + def is_regex_pattern(s): + if Util.python_has_version(3): + t = re.Pattern + else: + t = re._pattern_type + return isinstance(s, t) + @staticmethod def memoize_nullary(nullary_func): result = [] @@ -347,12 +355,21 @@ class Util: v_search = replace[0]() except TypeError: v_search = replace[0] + + v_replace = replace[1] + v_replace = v_replace.encode("utf-8") + + if Util.is_regex_pattern(v_search): + text2 = [] + for t in text: + text2.append(v_search.sub(v_replace, t)) + text = text2 + continue + assert v_search is None or Util.is_string(v_search) if not v_search: continue - v_replace = replace[1] v_search = v_search.encode("utf-8") - v_replace = v_replace.encode("utf-8") text2 = [] for t in text: if isinstance(t, tuple): @@ -657,7 +674,13 @@ class AsyncProcess: class NmTestBase(unittest.TestCase): - pass + def __init__(self, *args, **kwargs): + self._calling_num = {} + self._skip_test_for_l10n_diff = [] + self._async_jobs = [] + self._results = [] + self.srv = None + return unittest.TestCase.__init__(self, *args, **kwargs) MAX_JOBS = 15 @@ -837,9 +860,6 @@ class TestNmcli(NmTestBase): self.fail("invalid language %s" % (lang)) env = {} - if extra_env is not None: - for k, v in extra_env.items(): - env[k] = v for k in ["LD_LIBRARY_PATH", "DBUS_SESSION_BUS_ADDRESS"]: val = os.environ.get(k, None) if val is not None: @@ -856,6 +876,9 @@ class TestNmcli(NmTestBase): env["NM_TEST_CALLING_NUM"] = str(calling_num) if fatal_warnings is _DEFAULT_ARG or fatal_warnings: env["G_DEBUG"] = "fatal-warnings" + if extra_env is not None: + for k, v in extra_env.items(): + env[k] = v args = [conf.get(ENV_NM_TEST_CLIENT_NMCLI_PATH)] + list(args) @@ -1012,20 +1035,13 @@ class TestNmcli(NmTestBase): def async_wait(self): return self.async_start(wait_all=True) - def _nm_test_pre(self): - self._calling_num = {} - self._skip_test_for_l10n_diff = [] - self._async_jobs = [] - self._results = [] - - self.srv = NMStubServer(self._testMethodName) - def _nm_test_post(self): self.async_wait() - self.srv.shutdown() - self.srv = None + if self.srv is not None: + self.srv.shutdown() + self.srv = None self._calling_num = None @@ -1130,7 +1146,14 @@ class TestNmcli(NmTestBase): def nm_test(func): def f(self): - self._nm_test_pre() + self.srv = NMStubServer(self._testMethodName) + func(self) + self._nm_test_post() + + return f + + def nm_test_no_dbus(func): + def f(self): func(self) self._nm_test_post() @@ -1672,6 +1695,97 @@ class TestNmcli(NmTestBase): replace_cmd=replace_uuids, ) + @nm_test_no_dbus + def test_offline(self): + + # Make sure we're not using D-Bus + no_dbus_env = { + "DBUS_SYSTEM_BUS_ADDRESS": "very:invalid", + "DBUS_SESSION_BUS_ADDRESS": "very:invalid", + } + + # This check just makes sure the above works and the + # "nmcli g" command indeed fails talking to D-Bus + self.call_nmcli( + ["g"], + extra_env=no_dbus_env, + ) + + replace_uuids = [ + ( + re.compile(b"uuid=.*"), + "uuid=UUID-WAS-HERE-BUT-IS-NO-MORE-SADLY", + ) + ] + + self.call_nmcli( + ["--offline", "c", "add", "type", "ethernet"], + extra_env=no_dbus_env, + replace_stdout=replace_uuids, + ) + + self.call_nmcli( + ["--offline", "c", "show"], + extra_env=no_dbus_env, + ) + + self.call_nmcli( + ["--offline", "g"], + extra_env=no_dbus_env, + ) + + self.call_nmcli( + ["--offline"], + extra_env=no_dbus_env, + ) + + self.call_nmcli( + [ + "--offline", + "c", + "add", + "type", + "wifi", + "ssid", + "lala", + "802-1x.eap", + "pwd", + "802-1x.identity", + "foo", + "802-1x.password", + "bar", + ], + extra_env=no_dbus_env, + replace_stdout=replace_uuids, + ) + + self.call_nmcli( + [ + "--offline", + "c", + "add", + "type", + "wifi", + "ssid", + "lala", + "802-1x.eap", + "pwd", + "802-1x.identity", + "foo", + "802-1x.password", + "bar", + "802-1x.password-flags", + "agent-owned", + ], + extra_env=no_dbus_env, + replace_stdout=replace_uuids, + ) + + self.call_nmcli( + ["--complete-args", "--offline", "conn", "modify", "ipv6.ad"], + extra_env=no_dbus_env, + ) + ###############################################################################