From 96bab8fd63d29a8e3a905bb75167862fc0e5e0f1 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sat, 1 Apr 2023 19:44:29 +0800 Subject: [PATCH] networkctl: add verb edit and cat to operate on network configs This adds two verbs, edit and cat, to networkctl for operating on network configs (namely .network, .netdev and .link files). Specially, if the config name is prefixed by @, it will be treated as network interface name, and operations will be performed on config files associated with the link. Closes #26906 --- man/networkctl.xml | 51 ++++ src/network/networkctl.c | 510 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 560 insertions(+), 1 deletion(-) diff --git a/man/networkctl.xml b/man/networkctl.xml index 94ec3dc6312..497d88a15f8 100644 --- a/man/networkctl.xml +++ b/man/networkctl.xml @@ -355,6 +355,38 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR) which match the file are reconfigured. + + + edit + FILE|@DEVICE… + + Edit network configuration files, which include .network, + .netdev, and .link files. If no network config file + matching the given name is found, a new one will be created under /etc/. + Specially, if the name is prefixed by @, it will be treated as + a network interface, and editing will be performed on the network config files associated + with it. Additionally, the interface name can be suffixed with :network (default) + or :link, in order to choose the type of network config to operate on. + + If is specified, edit the drop-in file instead of + the main configuration file. Unless is specified, + systemd-networkd will be reloaded after the edit of the + .network or .netdev files finishes. + The same applies for .link files and systemd-udevd. + Note that the changed link settings are not automatically applied after reloading. + To achieve that, trigger uevents for the corresponding interface. Refer to + systemd.link5 + for more information. + + + + + cat + FILE|@DEVICE… + + Show network configuration files. This command honors + the @ prefix in the same way as edit. + @@ -405,6 +437,25 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR) + + + NAME + + + When used with edit, edit the drop-in file NAME + instead of the main configuration file. + + + + + + + + When used with edit, systemd-networkd + or systemd-udevd will not be reloaded after the editing finishes. + + + diff --git a/src/network/networkctl.c b/src/network/networkctl.c index 9cd4074fb8e..97e6cc7f9ed 100644 --- a/src/network/networkctl.c +++ b/src/network/networkctl.c @@ -26,7 +26,10 @@ #include "bus-common-errors.h" #include "bus-error.h" #include "bus-locator.h" +#include "bus-wait-for-jobs.h" +#include "conf-files.h" #include "device-util.h" +#include "edit-util.h" #include "escape.h" #include "ether-addr-util.h" #include "ethtool-util.h" @@ -50,6 +53,8 @@ #include "pager.h" #include "parse-argument.h" #include "parse-util.h" +#include "path-lookup.h" +#include "path-util.h" #include "pretty-print.h" #include "set.h" #include "socket-netlink.h" @@ -64,6 +69,7 @@ #include "terminal-util.h" #include "unit-def.h" #include "verbs.h" +#include "virt.h" #include "wifi-util.h" /* Kernel defines MODULE_NAME_LEN as 64 - sizeof(unsigned long). So, 64 is enough. */ @@ -74,12 +80,16 @@ static PagerFlags arg_pager_flags = 0; static bool arg_legend = true; +static bool arg_no_reload = false; static bool arg_all = false; static bool arg_stats = false; static bool arg_full = false; static unsigned arg_lines = 10; +static char *arg_drop_in = NULL; static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +STATIC_DESTRUCTOR_REGISTER(arg_drop_in, freep); + static int check_netns_match(sd_bus *bus) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; struct stat st; @@ -109,7 +119,22 @@ static int check_netns_match(sd_bus *bus) { } static bool networkd_is_running(void) { - return access("/run/systemd/netif/state", F_OK) >= 0; + static int cached = -1; + int r; + + if (cached < 0) { + r = access("/run/systemd/netif/state", F_OK); + if (r < 0) { + if (errno != ENOENT) + log_debug_errno(errno, + "Failed to determine whether networkd is running, assuming it's not: %m"); + + cached = false; + } else + cached = true; + } + + return cached; } static int acquire_bus(sd_bus **ret) { @@ -2919,6 +2944,457 @@ static int verb_reconfigure(int argc, char *argv[], void *userdata) { return 0; } +typedef enum ReloadFlags { + RELOAD_NETWORKD = 1 << 0, + RELOAD_UDEVD = 1 << 1, +} ReloadFlags; + +static int get_config_files_by_name(const char *name, char **ret_path, char ***ret_dropins) { + _cleanup_free_ char *path = NULL; + int r; + + assert(name); + assert(ret_path); + + STRV_FOREACH(i, NETWORK_DIRS) { + _cleanup_free_ char *p = NULL; + + p = path_join(*i, name); + if (!p) + return -ENOMEM; + + r = RET_NERRNO(access(p, F_OK)); + if (r >= 0) { + path = TAKE_PTR(p); + break; + } + + if (r != -ENOENT) + log_debug_errno(r, "Failed to determine whether '%s' exists, ignoring: %m", p); + } + + if (!path) + return -ENOENT; + + if (ret_dropins) { + _cleanup_free_ char *dropin_dirname = NULL; + + dropin_dirname = strjoin(name, ".d"); + if (!dropin_dirname) + return -ENOMEM; + + r = conf_files_list_dropins(ret_dropins, dropin_dirname, /* root = */ NULL, NETWORK_DIRS); + if (r < 0) + return r; + } + + *ret_path = TAKE_PTR(path); + + return 0; +} + +static int get_dropin_by_name( + const char *name, + char * const *dropins, + char **ret) { + + assert(name); + assert(dropins); + assert(ret); + + STRV_FOREACH(i, dropins) + if (path_equal_filename(*i, name)) { + _cleanup_free_ char *d = NULL; + + d = strdup(*i); + if (!d) + return -ENOMEM; + + *ret = TAKE_PTR(d); + return 1; + } + + *ret = NULL; + return 0; +} + +static int get_network_files_by_link( + sd_netlink **rtnl, + const char *link, + char **ret_path, + char ***ret_dropins) { + + _cleanup_strv_free_ char **dropins = NULL; + _cleanup_free_ char *path = NULL; + int r, ifindex; + + assert(rtnl); + assert(link); + assert(ret_path); + assert(ret_dropins); + + ifindex = rtnl_resolve_interface_or_warn(rtnl, link); + if (ifindex < 0) + return ifindex; + + r = sd_network_link_get_network_file(ifindex, &path); + if (r == -ENODATA) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "Link '%s' has no associated network file.", link); + if (r < 0) + return log_error_errno(r, "Failed to get network file for link '%s': %m", link); + + r = sd_network_link_get_network_file_dropins(ifindex, &dropins); + if (r < 0 && r != -ENODATA) + return log_error_errno(r, "Failed to get network drop-ins for link '%s': %m", link); + + *ret_path = TAKE_PTR(path); + *ret_dropins = TAKE_PTR(dropins); + + return 0; +} + +static int get_link_files_by_link(const char *link, char **ret_path, char ***ret_dropins) { + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + _cleanup_strv_free_ char **dropins_split = NULL; + _cleanup_free_ char *p = NULL; + const char *path, *dropins; + int r; + + assert(link); + assert(ret_path); + assert(ret_dropins); + + r = sd_device_new_from_ifname(&device, link); + if (r < 0) + return log_error_errno(r, "Failed to create sd-device object for link '%s': %m", link); + + r = sd_device_get_property_value(device, "ID_NET_LINK_FILE", &path); + if (r == -ENOENT) + return log_error_errno(r, "Link '%s' has no associated link file.", link); + if (r < 0) + return log_error_errno(r, "Failed to get link file for link '%s': %m", link); + + r = sd_device_get_property_value(device, "ID_NET_LINK_FILE_DROPINS", &dropins); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to get link drop-ins for link '%s': %m", link); + if (r >= 0) { + r = strv_split_full(&dropins_split, dropins, ":", EXTRACT_CUNESCAPE); + if (r < 0) + return log_error_errno(r, "Failed to parse link drop-ins for link '%s': %m", link); + } + + p = strdup(path); + if (!p) + return log_oom(); + + *ret_path = TAKE_PTR(p); + *ret_dropins = TAKE_PTR(dropins_split); + + return 0; +} + +static int get_config_files_by_link_config( + const char *link_config, + sd_netlink **rtnl, + char **ret_path, + char ***ret_dropins, + ReloadFlags *ret_reload) { + + _cleanup_strv_free_ char **dropins = NULL, **link_config_split = NULL; + _cleanup_free_ char *path = NULL; + const char *ifname, *type; + ReloadFlags reload; + size_t n; + int r; + + assert(link_config); + assert(rtnl); + assert(ret_path); + assert(ret_dropins); + + link_config_split = strv_split(link_config, ":"); + if (!link_config_split) + return log_oom(); + + n = strv_length(link_config_split); + if (n == 0 || isempty(link_config_split[0])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No link name is given."); + if (n > 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid link config '%s'.", link_config); + + ifname = link_config_split[0]; + type = n == 2 ? link_config_split[1] : "network"; + + if (streq(type, "network")) { + if (!networkd_is_running()) + return log_error_errno(SYNTHETIC_ERRNO(ESRCH), + "Cannot get network file for link if systemd-networkd is not running."); + + r = get_network_files_by_link(rtnl, ifname, &path, &dropins); + if (r < 0) + return r; + + reload = RELOAD_NETWORKD; + } else if (streq(type, "link")) { + r = get_link_files_by_link(ifname, &path, &dropins); + if (r < 0) + return r; + + reload = RELOAD_UDEVD; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid config type '%s' for link '%s'.", type, ifname); + + *ret_path = TAKE_PTR(path); + *ret_dropins = TAKE_PTR(dropins); + + if (ret_reload) + *ret_reload = reload; + + return 0; +} + +static int add_config_to_edit( + EditFileContext *context, + const char *path, + char * const *dropins) { + + _cleanup_free_ char *new_path = NULL, *dropin_path = NULL, *old_dropin = NULL; + _cleanup_strv_free_ char **comment_paths = NULL; + int r; + + assert(context); + assert(path); + assert(!arg_drop_in || dropins); + + if (path_startswith(path, "/usr")) { + _cleanup_free_ char *name = NULL; + + r = path_extract_filename(path, &name); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from '%s': %m", path); + + new_path = path_join(NETWORK_DIRS[0], name); + if (!new_path) + return log_oom(); + } + + if (!arg_drop_in) + return edit_files_add(context, new_path ?: path, path, NULL); + + r = get_dropin_by_name(arg_drop_in, dropins, &old_dropin); + if (r < 0) + return log_error_errno(r, "Failed to acquire drop-in '%s': %m", arg_drop_in); + + if (r > 0 && !path_startswith(old_dropin, "/usr")) + /* An existing drop-in is found and not in /usr/. Let's edit it directly. */ + dropin_path = TAKE_PTR(old_dropin); + else { + /* No drop-in was found or an existing drop-in resides in /usr/. Let's create + * a new drop-in file. */ + dropin_path = strjoin(new_path ?: path, ".d/", arg_drop_in); + if (!dropin_path) + return log_oom(); + } + + comment_paths = strv_new(path); + if (!comment_paths) + return log_oom(); + + r = strv_extend_strv(&comment_paths, dropins, /* filter_duplicates = */ false); + if (r < 0) + return log_oom(); + + return edit_files_add(context, dropin_path, old_dropin, comment_paths); +} + +static int udevd_reload(sd_bus *bus) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL; + const char *job_path; + int r; + + assert(bus); + + r = bus_wait_for_jobs_new(bus, &w); + if (r < 0) + return log_error_errno(r, "Could not watch jobs: %m"); + + r = bus_call_method(bus, + bus_systemd_mgr, + "ReloadUnit", + &error, + &reply, + "ss", + "systemd-udevd.service", + "replace"); + if (r < 0) + return log_error_errno(r, "Failed to reload systemd-udevd: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "o", &job_path); + if (r < 0) + return bus_log_parse_error(r); + + r = bus_wait_for_jobs_one(w, job_path, /* quiet = */ true, NULL); + if (r == -ENOEXEC) { + log_debug("systemd-udevd is not running, skipping reload."); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to reload systemd-udevd: %m"); + + return 1; +} + +static int verb_edit(int argc, char *argv[], void *userdata) { + _cleanup_(edit_file_context_done) EditFileContext context = { + .marker_start = DROPIN_MARKER_START, + .marker_end = DROPIN_MARKER_END, + .remove_parent = !!arg_drop_in, + }; + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + ReloadFlags reload = 0; + int r; + + if (!on_tty()) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit network config files if not on a tty."); + + r = mac_selinux_init(); + if (r < 0) + return r; + + STRV_FOREACH(name, strv_skip(argv, 1)) { + _cleanup_strv_free_ char **dropins = NULL; + _cleanup_free_ char *path = NULL; + const char *link_config; + + link_config = startswith(*name, "@"); + if (link_config) { + ReloadFlags flags; + + r = get_config_files_by_link_config(link_config, &rtnl, &path, &dropins, &flags); + if (r < 0) + return r; + + reload |= flags; + + r = add_config_to_edit(&context, path, dropins); + if (r < 0) + return r; + + continue; + } + + if (ENDSWITH_SET(*name, ".network", ".netdev")) + reload |= RELOAD_NETWORKD; + else if (endswith(*name, ".link")) + reload |= RELOAD_UDEVD; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid network config name '%s'.", *name); + + r = get_config_files_by_name(*name, &path, &dropins); + if (r == -ENOENT) { + if (arg_drop_in) + return log_error_errno(r, "Cannot find network config '%s'.", *name); + + log_debug("No existing network config '%s' found, creating a new file.", *name); + + path = path_join(NETWORK_DIRS[0], *name); + if (!path) + return log_oom(); + + r = edit_files_add(&context, path, NULL, NULL); + if (r < 0) + return r; + continue; + } + if (r < 0) + return log_error_errno(r, "Failed to get the path of network config '%s': %m", *name); + + r = add_config_to_edit(&context, path, dropins); + if (r < 0) + return r; + } + + r = do_edit_files_and_install(&context); + if (r < 0) + return r; + + if (arg_no_reload) + return 0; + + if (!sd_booted() || running_in_chroot() > 0) { + log_debug("System is not booted with systemd or is running in chroot, skipping reload."); + return 0; + } + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to system bus: %m"); + + if (FLAGS_SET(reload, RELOAD_UDEVD)) { + r = udevd_reload(bus); + if (r < 0) + return r; + } + + if (FLAGS_SET(reload, RELOAD_NETWORKD)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + if (!networkd_is_running()) { + log_debug("systemd-networkd is not running, skipping reload."); + return 0; + } + + r = bus_call_method(bus, bus_network_mgr, "Reload", &error, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to reload systemd-networkd: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_cat(int argc, char *argv[], void *userdata) { + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + int r, ret = 0; + + pager_open(arg_pager_flags); + + STRV_FOREACH(name, strv_skip(argv, 1)) { + _cleanup_strv_free_ char **dropins = NULL; + _cleanup_free_ char *path = NULL; + const char *link_config; + + link_config = startswith(*name, "@"); + if (link_config) { + r = get_config_files_by_link_config(link_config, &rtnl, &path, &dropins, /* ret_reload = */ NULL); + if (r < 0) + return ret < 0 ? ret : r; + } else { + r = get_config_files_by_name(*name, &path, &dropins); + if (r == -ENOENT) { + log_error_errno(r, "Cannot find network config file '%s'.", *name); + ret = ret < 0 ? ret : r; + continue; + } + if (r < 0) { + log_error_errno(r, "Failed to get the path of network config '%s': %m", *name); + return ret < 0 ? ret : r; + } + } + + r = cat_files(path, dropins, /* flags = */ 0); + if (r < 0) + return ret < 0 ? ret : r; + } + + return ret; +} + static int help(void) { _cleanup_free_ char *link = NULL; int r; @@ -2941,6 +3417,8 @@ static int help(void) { " forcerenew DEVICES... Trigger DHCP reconfiguration of all connected clients\n" " reconfigure DEVICES... Reconfigure interfaces\n" " reload Reload .network and .netdev files\n" + " edit FILES|DEVICES... Edit network configuration files\n" + " cat FILES|DEVICES... Show network configuration files\n" "\nOptions:\n" " -h --help Show this help\n" " --version Show package version\n" @@ -2952,6 +3430,9 @@ static int help(void) { " -n --lines=INTEGER Number of journal entries to show\n" " --json=pretty|short|off\n" " Generate JSON output\n" + " --no-reload Do not reload systemd-networkd or systemd-udevd\n" + " after editing network config\n" + " --drop-in=NAME Edit specified drop-in instead of main config file\n" "\nSee the %s for details.\n", program_invocation_short_name, ansi_highlight(), @@ -2967,6 +3448,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_NO_PAGER, ARG_NO_LEGEND, ARG_JSON, + ARG_NO_RELOAD, + ARG_DROP_IN, }; static const struct option options[] = { @@ -2979,6 +3462,8 @@ static int parse_argv(int argc, char *argv[]) { { "full", no_argument, NULL, 'l' }, { "lines", required_argument, NULL, 'n' }, { "json", required_argument, NULL, ARG_JSON }, + { "no-reload", no_argument, NULL, ARG_NO_RELOAD }, + { "drop-in", required_argument, NULL, ARG_DROP_IN }, {} }; @@ -3005,6 +3490,27 @@ static int parse_argv(int argc, char *argv[]) { arg_legend = false; break; + case ARG_NO_RELOAD: + arg_no_reload = true; + break; + + case ARG_DROP_IN: + if (isempty(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty drop-in file name."); + + if (!endswith(optarg, ".conf")) + arg_drop_in = strjoin(optarg, ".conf"); + else + arg_drop_in = strdup(optarg); + if (!arg_drop_in) + return log_oom(); + + if (!filename_is_valid(arg_drop_in)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid drop-in file name '%s'.", arg_drop_in); + + break; + case 'a': arg_all = true; break; @@ -3053,6 +3559,8 @@ static int networkctl_main(int argc, char *argv[]) { { "forcerenew", 2, VERB_ANY, VERB_ONLINE_ONLY, link_force_renew }, { "reconfigure", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_reconfigure }, { "reload", 1, 1, VERB_ONLINE_ONLY, verb_reload }, + { "edit", 2, VERB_ANY, 0, verb_edit }, + { "cat", 2, VERB_ANY, 0, verb_cat }, {} }; -- 2.47.3