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