From 0d3787deacc96d74b94b75c7b62282973c6df014 Mon Sep 17 00:00:00 2001 From: Mike Yuan <me@yhndnzj.com> Date: Sun, 8 Sep 2024 18:09:35 +0200 Subject: [PATCH] networkctl: support editing netdev files by link and cat ":all" Also, don't abuse RET_GATHER in verb_cat(), where the failures are most likely unrelated to each other. Closes #34281 --- man/networkctl.xml | 12 +- src/network/networkctl-config-file.c | 273 +++++++++++++++------ test/units/TEST-74-AUX-UTILS.networkctl.sh | 11 + 3 files changed, 219 insertions(+), 77 deletions(-) diff --git a/man/networkctl.xml b/man/networkctl.xml index 9c7e35b4b00..9e2a65b879a 100644 --- a/man/networkctl.xml +++ b/man/networkctl.xml @@ -434,8 +434,9 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR) <filename>/run/</filename>, depending on whether <option>--runtime</option> is specified. Specially, if the name is prefixed by <literal>@</literal>, 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 <literal>:network</literal> (default) - or <literal>:link</literal>, in order to choose the type of network config to operate on.</para> + with it. Additionally, the interface name can be suffixed with <literal>:network</literal> (default), + <literal>:link</literal>, or <literal>:netdev</literal>, in order to choose the type of network config + to operate on.</para> <para>If <option>--drop-in=</option> is specified, edit the drop-in file instead of the main configuration file. Unless <option>--no-reload</option> is specified, @@ -460,9 +461,10 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR) <optional><replaceable>FILE</replaceable>|<replaceable>@DEVICE</replaceable>…</optional> </term> <listitem> - <para>Show network configuration files. This command honors the <literal>@</literal> prefix in the - same way as <command>edit</command>. When no argument is specified, - <citerefentry><refentrytitle>networkd.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry> + <para>Show network configuration files. This command honors the <literal>@</literal> prefix in a + similar way as <command>edit</command>, with support for an additional suffix <literal>:all</literal> + for showing all types of configuration files associated with the interface at once. When no argument + is specified, <citerefentry><refentrytitle>networkd.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry> and its drop-in files will be shown.</para> <xi:include href="version-info.xml" xpointer="v254"/> diff --git a/src/network/networkctl-config-file.c b/src/network/networkctl-config-file.c index 6d60d7eb99e..eeddfb22811 100644 --- a/src/network/networkctl-config-file.c +++ b/src/network/networkctl-config-file.c @@ -23,6 +23,7 @@ #include "path-util.h" #include "pretty-print.h" #include "selinux-util.h" +#include "string-table.h" #include "strv.h" #include "virt.h" @@ -31,6 +32,22 @@ typedef enum ReloadFlags { RELOAD_UDEVD = 1 << 1, } ReloadFlags; +typedef enum LinkConfigType { + CONFIG_NETWORK, + CONFIG_LINK, + CONFIG_NETDEV, + _CONFIG_MAX, + _CONFIG_INVALID = -EINVAL, +} LinkConfigType; + +static const char* const link_config_type_table[_CONFIG_MAX] = { + [CONFIG_NETWORK] = "network", + [CONFIG_LINK] = "link", + [CONFIG_NETDEV] = "netdev", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(link_config_type, LinkConfigType); + static int get_config_files_by_name( const char *name, bool allow_masked, @@ -115,28 +132,25 @@ static int get_dropin_by_name( } static int get_network_files_by_link( - sd_netlink **rtnl, const char *link, + int ifindex, + bool ignore_missing, char **ret_path, char ***ret_dropins) { _cleanup_strv_free_ char **dropins = NULL; _cleanup_free_ char *path = NULL; - int r, ifindex; + int r; - assert(rtnl); assert(link); + assert(ifindex > 0); 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); + return log_full_errno(ignore_missing ? LOG_DEBUG : LOG_ERR, 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); @@ -150,7 +164,40 @@ static int get_network_files_by_link( return 0; } -static int get_link_files_by_link(const char *link, char **ret_path, char ***ret_dropins) { +static int get_netdev_files_by_link( + const char *link, + int ifindex, + bool ignore_missing, + char **ret_path, + char ***ret_dropins) { + + _cleanup_strv_free_ char **dropins = NULL; + _cleanup_free_ char *path = NULL; + int r; + + assert(link); + assert(ifindex > 0); + assert(ret_path); + assert(ret_dropins); + + r = sd_network_link_get_netdev_file(ifindex, &path); + if (r == -ENODATA) + return log_full_errno(ignore_missing ? LOG_DEBUG : LOG_ERR, SYNTHETIC_ERRNO(ENOENT), + "Link '%s' has no associated netdev file.", link); + if (r < 0) + return log_error_errno(r, "Failed to get netdev file for link '%s': %m", link); + + r = sd_network_link_get_netdev_file_dropins(ifindex, &dropins); + if (r < 0 && r != -ENODATA) + return log_error_errno(r, "Failed to get netdev 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, bool ignore_missing, 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; @@ -167,7 +214,8 @@ static int get_link_files_by_link(const char *link, char **ret_path, char ***ret 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); + return log_full_errno(ignore_missing ? LOG_DEBUG : LOG_ERR, 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); @@ -191,62 +239,72 @@ static int get_link_files_by_link(const char *link, char **ret_path, char ***ret } static int get_config_files_by_link_config( - const char *link_config, + const char *ifname, + LinkConfigType type, + bool ignore_missing, sd_netlink **rtnl, char **ret_path, - char ***ret_dropins, - ReloadFlags *ret_reload) { + char ***ret_dropins) { - _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(ifname); + assert(type >= 0 && type < _CONFIG_MAX); assert(rtnl); assert(ret_path); assert(ret_dropins); - link_config_split = strv_split(link_config, ":"); - if (!link_config_split) - return log_oom(); + if (type == CONFIG_LINK) + return get_link_files_by_link(ifname, ignore_missing, ret_path, ret_dropins); - 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); + if (!networkd_is_running()) + return log_full_errno(ignore_missing ? LOG_DEBUG : LOG_ERR, SYNTHETIC_ERRNO(ESRCH), + "Cannot get network/netdev file for link if systemd-networkd is not running."); - ifname = link_config_split[0]; - type = n == 2 ? link_config_split[1] : "network"; + int ifindex = rtnl_resolve_interface_or_warn(rtnl, ifname); + if (ifindex < 0) + return ifindex; - 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."); + if (type == CONFIG_NETWORK) + r = get_network_files_by_link(ifname, ifindex, ignore_missing, ret_path, ret_dropins); + else if (type == CONFIG_NETDEV) + r = get_netdev_files_by_link(ifname, ifindex, ignore_missing, ret_path, ret_dropins); + else + assert_not_reached(); - r = get_network_files_by_link(rtnl, ifname, &path, &dropins); - if (r < 0) - return r; + return r; +} - reload = RELOAD_NETWORKD; - } else if (streq(type, "link")) { - r = get_link_files_by_link(ifname, &path, &dropins); - if (r < 0) - return r; +static int parse_link_config(const char *link_config, char **ret_ifname, LinkConfigType *ret_type) { + const char *p = ASSERT_PTR(link_config); + _cleanup_free_ char *ifname = NULL; + int r; - reload = RELOAD_UDEVD; - } else - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid config type '%s' for link '%s'.", type, ifname); + assert(ret_ifname); + assert(ret_type); - *ret_path = TAKE_PTR(path); - *ret_dropins = TAKE_PTR(dropins); + r = extract_first_word(&p, &ifname, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r <= 0) + return log_error_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), + "Failed to extract link name from '%s': %m", link_config); - if (ret_reload) - *ret_reload = reload; + if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid link name: %s", ifname); + + LinkConfigType t; + + if (isempty(p)) + t = CONFIG_NETWORK; + else if (streq(p, "all")) + t = _CONFIG_MAX; + else { + t = link_config_type_from_string(p); + if (t < 0) + return log_error_errno(t, "Invalid config type '%s' for link '%s'.", p, ifname); + } + + *ret_ifname = TAKE_PTR(ifname); + *ret_type = t; return 0; } @@ -427,18 +485,29 @@ int verb_edit(int argc, char *argv[], void *userdata) { link_config = startswith(*name, "@"); if (link_config) { - ReloadFlags flags; + _cleanup_free_ char *ifname = NULL; + LinkConfigType type; - r = get_config_files_by_link_config(link_config, &rtnl, &path, &dropins, &flags); + r = parse_link_config(link_config, &ifname, &type); if (r < 0) return r; + if (type == _CONFIG_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Config type 'all' cannot be used with 'edit'."); - reload |= flags; + r = get_config_files_by_link_config(ifname, type, + /* ignore_missing = */ false, + &rtnl, + &path, &dropins); + if (r < 0) + return r; r = add_config_to_edit(&context, path, dropins); if (r < 0) return r; + reload |= type == CONFIG_LINK ? RELOAD_UDEVD : RELOAD_NETWORKD; + continue; } @@ -482,6 +551,66 @@ int verb_edit(int argc, char *argv[], void *userdata) { return reload_daemons(reload); } +static int cat_files_by_link_one( + const char *ifname, + LinkConfigType type, + sd_netlink **rtnl, + bool ignore_missing, + bool *first) { + + _cleanup_strv_free_ char **dropins = NULL; + _cleanup_free_ char *path = NULL; + int r; + + assert(ifname); + assert(type >= 0 && type < _CONFIG_MAX); + assert(rtnl); + assert(first); + + r = get_config_files_by_link_config(ifname, type, ignore_missing, rtnl, &path, &dropins); + if (ignore_missing && IN_SET(r, -ENOENT, -ESRCH)) + return 0; + if (r < 0) + return r; + + if (!*first) + putchar('\n'); + + r = cat_files(path, dropins, /* flags = */ CAT_FORMAT_HAS_SECTIONS); + if (r < 0) + return r; + + *first = false; + + return 0; +} + +static int cat_files_by_link_config(const char *link_config, sd_netlink **rtnl, bool *first) { + _cleanup_free_ char *ifname = NULL; + LinkConfigType type; + int r; + + assert(link_config); + assert(rtnl); + assert(first); + + r = parse_link_config(link_config, &ifname, &type); + if (r < 0) + return r; + + if (type == _CONFIG_MAX) { + for (LinkConfigType i = 0; i < _CONFIG_MAX; i++) { + r = cat_files_by_link_one(ifname, i, rtnl, /* ignore_missing = */ true, first); + if (r < 0) + return r; + } + + return 0; + } + + return cat_files_by_link_one(ifname, type, rtnl, /* ignore_missing = */ false, first); +} + int verb_cat(int argc, char *argv[], void *userdata) { _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; char **args = strv_skip(argv, 1); @@ -494,37 +623,37 @@ int verb_cat(int argc, char *argv[], void *userdata) { bool first = true; STRV_FOREACH(name, args) { - _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); + r = cat_files_by_link_config(link_config, &rtnl, &first); if (r < 0) - return RET_GATHER(ret, r); - } else { - r = get_config_files_by_name(*name, /* allow_masked = */ false, &path, &dropins); - if (r == -ENOENT) { - RET_GATHER(ret, log_error_errno(r, "Cannot find network config file '%s'.", *name)); - continue; - } - if (r == -ERFKILL) { - RET_GATHER(ret, log_debug_errno(r, "Network config '%s' is masked, ignoring.", *name)); - continue; - } - if (r < 0) { - log_error_errno(r, "Failed to get the path of network config '%s': %m", *name); - return RET_GATHER(ret, r); - } + return r; + continue; } + _cleanup_strv_free_ char **dropins = NULL; + _cleanup_free_ char *path = NULL; + + r = get_config_files_by_name(*name, /* allow_masked = */ false, &path, &dropins); + if (r == -ENOENT) { + RET_GATHER(ret, log_error_errno(r, "Cannot find network config file '%s'.", *name)); + continue; + } + if (r == -ERFKILL) { + RET_GATHER(ret, log_debug_errno(r, "Network config '%s' is masked, ignoring.", *name)); + continue; + } + if (r < 0) + return log_error_errno(r, "Failed to get the path of network config '%s': %m", *name); + if (!first) putchar('\n'); r = cat_files(path, dropins, /* flags = */ CAT_FORMAT_HAS_SECTIONS); if (r < 0) - return RET_GATHER(ret, r); + return r; first = false; } diff --git a/test/units/TEST-74-AUX-UTILS.networkctl.sh b/test/units/TEST-74-AUX-UTILS.networkctl.sh index 8c62de9155a..0576d6c0556 100755 --- a/test/units/TEST-74-AUX-UTILS.networkctl.sh +++ b/test/units/TEST-74-AUX-UTILS.networkctl.sh @@ -99,10 +99,18 @@ cmp "/usr/lib/systemd/network/$LINK_NAME" "/etc/systemd/network/$LINK_NAME" systemctl unmask systemd-networkd systemctl stop systemd-networkd (! networkctl cat @test2) +(! networkctl cat @test2:netdev) systemctl start systemd-networkd SYSTEMD_LOG_LEVEL=debug /usr/lib/systemd/systemd-networkd-wait-online -i test2:carrier --timeout 20 + networkctl cat @test2:network | cmp - <(networkctl cat "$NETWORK_NAME") +networkctl cat @test2:netdev | cmp - <(networkctl cat "$NETDEV_NAME") +for c in "$NETWORK_NAME" "$NETDEV_NAME"; do + assert_in "$(networkctl cat "$c" | head -n1)" "$(networkctl cat @test2:all)" +done + +(! networkctl edit @test2:all) EDITOR='cp' script -ec 'networkctl edit @test2 --drop-in test2.conf' /dev/null cmp "+4" "/etc/systemd/network/${NETWORK_NAME}.d/test2.conf" @@ -112,6 +120,9 @@ SYSTEMD_LOG_LEVEL=debug /usr/lib/systemd/systemd-networkd-wait-online -i test2:c ip_link="$(ip link show test2)" if systemctl --quiet is-active systemd-udevd; then + networkctl cat @test2:link | cmp - <(networkctl cat "$LINK_NAME") + assert_in "$(networkctl cat "$LINK_NAME" | head -n1)" "$(networkctl cat @test2:all)" + assert_in 'alias test_alias' "$ip_link" fi