]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Merge pull request #29888 from mrc0mmand/network-generator
authorLuca Boccassi <bluca@debian.org>
Tue, 7 Nov 2023 00:10:43 +0000 (00:10 +0000)
committerGitHub <noreply@github.com>
Tue, 7 Nov 2023 00:10:43 +0000 (00:10 +0000)
test: add a couple of tests for systemd-network-generator

src/network/generator/network-generator.c
test/test-network-generator-conversion.sh

index d9fca213844bdc88b3844e11ccb8fd0841d81e3e..9bc7aa19eb6401f576811b1f956d6aeba02c35a4 100644 (file)
@@ -528,11 +528,13 @@ static int parse_cmdline_ip_mtu_mac(Context *context, const char *ifname, int fa
         else
                 mtu = strndupa_safe(value, p - value);
 
-        r = network_set_mtu(context, ifname, family, mtu);
-        if (r < 0)
-                return r;
+        if (!isempty(mtu)) {
+                r = network_set_mtu(context, ifname, family, mtu);
+                if (r < 0)
+                        return r;
+        }
 
-        if (!p)
+        if (!p || isempty(p + 1))
                 return 0;
 
         r = network_set_mac_address(context, ifname, p + 1);
@@ -611,9 +613,47 @@ static int parse_netmask_or_prefixlen(int family, const char **value, unsigned c
         return 0;
 }
 
+static int parse_ip_dns_address_one(Context *context, const char *ifname, int family, const char **value) {
+        const char *p = *value, *q, *buf;
+        int r;
+
+        if (isempty(p))
+                return 0;
+
+        if (family == AF_INET6) {
+                if (p[0] != '[')
+                        return -EINVAL;
+
+                q = strchr(p + 1, ']');
+                if (!q)
+                        return -EINVAL;
+
+                buf = strndupa_safe(p + 1, q - p - 1);
+                p = q + 1;
+        } else {
+                q = strchr(p, ':');
+                if (!q)
+                        buf = *value;
+                else
+                        buf = strndupa_safe(*value, q - *value);
+
+                p += strlen(buf);
+        }
+
+        r = network_set_dns(context, ifname, buf);
+        if (r < 0)
+                return r;
+
+        if (p[0] == ':')
+                p++;
+
+        *value = p;
+        return 0;
+}
+
 static int parse_cmdline_ip_address(Context *context, int family, const char *value) {
         union in_addr_union addr = {}, peer = {}, gateway = {};
-        const char *hostname = NULL, *ifname, *dhcp_type, *dns, *p;
+        const char *hostname = NULL, *ifname, *dhcp_type, *p;
         unsigned char prefixlen;
         int r;
 
@@ -689,20 +729,12 @@ static int parse_cmdline_ip_address(Context *context, int family, const char *va
 
         /* Next, try [<dns1>][:<dns2>] */
         value = p + 1;
-        p = strchr(value, ':');
-        if (!p) {
-                r = network_set_dns(context, ifname, value);
-                if (r < 0)
-                        return r;
-        } else {
-                dns = strndupa_safe(value, p - value);
-                r = network_set_dns(context, ifname, dns);
-                if (r < 0)
-                        return r;
-                r = network_set_dns(context, ifname, p + 1);
-                if (r < 0)
-                        return r;
-        }
+        r = parse_ip_dns_address_one(context, ifname, family, &value);
+        if (r < 0)
+                return r;
+        r = parse_ip_dns_address_one(context, ifname, family, &value);
+        if (r < 0)
+                return r;
 
         return 0;
 }
index 3b6b1ec8ba8b2900d041f5aa42b6cae1637ba668..7c853e8987d3faaf92b8b7ed2866f4917bd5781f 100755 (executable)
 #!/usr/bin/env bash
 # SPDX-License-Identifier: LGPL-2.1-or-later
-set -ex
+# shellcheck disable=SC2235
+set -eux
+set -o pipefail
 
-if [[ -n "$1" ]]; then
-    generator=$1
+# TODO/FIXME:
+#   - we should probably have something like "udevadm verify" but for .network files
+#     (networkctl verify?) so we can check that all directives are in correct sections
+#   - according to dracut.cmdline(7) <peer> address can also be followed by /CIDR,
+#     but this doesn't seem to work with sd-network-generator
+
+if [[ -n "${1:-}" ]]; then
+    GENERATOR_BIN=$1
 elif [[ -x /usr/lib/systemd/systemd-network-generator ]]; then
-    generator=/usr/lib/systemd/systemd-network-generator
+    GENERATOR_BIN=/usr/lib/systemd/systemd-network-generator
 elif [[ -x /lib/systemd/systemd-network-generator ]]; then
-    generator=/lib/systemd/systemd-network-generator
+    GENERATOR_BIN=/lib/systemd/systemd-network-generator
 else
     exit 1
 fi
 
-src="$(dirname "$0")/testdata/test-network-generator-conversion"
+# See: https://github.com/systemd/systemd/pull/29888#issuecomment-1796187440
+unset NOTIFY_SOCKET
+
+WORK_DIR="$(mktemp --directory --tmpdir "test-network-generator-conversion.XXXXXX")"
+# shellcheck disable=SC2064
+trap "rm -rf '$WORK_DIR'" EXIT
+
+# Convert octal netmask to CIDR notation (e.g. 255.255.255.0 => 24)
+netmask_to_cidr() (
+    set +x
+
+    local netmask="${1:?}"
+    local x bits=0
+
+    # shellcheck disable=SC2086
+    x="0$(printf "%o" ${netmask//./ })"
+    while [[ "$x" -gt 0 ]]; do
+        ((bits += x % 2))
+        ((x >>= 1))
+    done
+
+    echo "$bits"
+)
+
+run_network_generator() {
+    local stderr
+
+    rm -rf "${WORK_DIR:?}"/*
+    stderr="$WORK_DIR/stderr"
+    if ! "$GENERATOR_BIN" --root "$WORK_DIR" 2>"$stderr"; then
+        echo >&2 "Generator failed when parsing $SYSTEMD_PROC_CMDLINE"
+        cat "$stderr"
+        return 1
+    fi
+
+    if [[ -s "$stderr" ]]; then
+        echo >&2 "Generator generated unexpected messages on stderr"
+        cat "$stderr"
+        return 1
+    fi
+
+    ls -l "$WORK_DIR/run/systemd/network/"
+
+    rm -f "$stderr"
+    return 0
+}
+
+check_dhcp() {
+    local dhcp="${1:?}"
+    local network_file="${2:?}"
+
+    case "$dhcp" in
+        dhcp)
+            grep -q "^DHCP=ipv4$" "$network_file"
+            ;;
+        dhcp6)
+            grep -q "^DHCP=ipv6$" "$network_file"
+            ;;
+        on|any)
+            grep -q "^DHCP=yes$" "$network_file"
+            ;;
+        none|off)
+            grep -q "^DHCP=no$" "$network_file"
+            grep -q "^LinkLocalAddressing=no$" "$network_file"
+            grep -q "^IPv6AcceptRA=no$" "$network_file"
+            ;;
+        auto6|ibft)
+            grep -q "^DHCP=no$" "$network_file"
+            ;;
+        either6)
+            grep -q "^DHCP=ipv6$" "$network_file"
+            ;;
+        link6)
+            grep -q "^DHCP=no$" "$network_file"
+            grep -q "^LinkLocalAddressing=ipv6$" "$network_file"
+            grep -q "^IPv6AcceptRA=no$" "$network_file"
+            ;;
+        link-local)
+            grep -q "^DHCP=no$" "$network_file"
+            grep -q "^LinkLocalAddressing=yes$" "$network_file"
+            grep -q "^IPv6AcceptRA=no$" "$network_file"
+            ;;
+        *)
+            echo >&2 "Invalid assignment $cmdline"
+            return 1
+    esac
+
+    return 0
+}
+
+# Check the shortest ip= variant, i.e.:
+#   ip={dhcp|on|any|dhcp6|auto6|either6|link6|link-local}
+#
+# Note:
+#   - dracut also supports single-dhcp
+#   - link-local is supported only by systemd-network-generator
+check_one_dhcp() {
+    local cmdline="${1:?}"
+    local dhcp="${cmdline#ip=}"
+    local network_file
+
+    SYSTEMD_LOG_LEVEL=debug SYSTEMD_PROC_CMDLINE="$cmdline" run_network_generator
+    network_file="${WORK_DIR:?}/run/systemd/network/71-default.network"
+    cat "$network_file"
+
+    check_dhcp "$dhcp" "$network_file"
+
+    return 0
+}
+
+# Similar to the previous one, but with slightly more fields:
+#   ip=<interface>:{dhcp|on|any|dhcp6|auto6|link6|link-local}[:[<mtu>][:<macaddr>]]
+#
+# Same notes apply as well.
+check_one_interface_dhcp() {
+    local cmdline="${1:?}"
+    local ifname dhcp mtu mac network_file
+
+    IFS=":" read -r ifname dhcp mtu mac <<< "${cmdline#ip=}"
+
+    SYSTEMD_LOG_LEVEL=debug SYSTEMD_PROC_CMDLINE="$cmdline" run_network_generator
+    network_file="${WORK_DIR:?}/run/systemd/network/70-$ifname.network"
+    cat "$network_file"
 
-for f in "$src"/test-*.input; do
-    echo "*** Running $f"
+    grep -q "^Name=$ifname$" "$network_file"
+    check_dhcp "$dhcp" "$network_file"
+    [[ -n "$mtu" ]] && grep -q "^MTUBytes=$mtu$" "$network_file"
+    [[ -n "$mac" ]] && grep -q "^MACAddress=$mac$" "$network_file"
 
-    (
-        out=$(mktemp --tmpdir --directory "test-network-generator-conversion.XXXXXXXXXX")
-        # shellcheck disable=SC2064
-        trap "rm -rf '$out'" EXIT INT QUIT PIPE
+    return 0
+}
 
-        # shellcheck disable=SC2046
-        $generator --root "$out" -- $(cat "$f")
+# Check the "long" ip= formats, i.e:
+#   ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft}[:[<mtu>][:<macaddr>]
+#   ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft}[:[<dns1>][:<dns2>]]
+check_one_long() {
+    local cmdline="${1:?}"
+    local ip peer gateway netmask hostname ifname dhcp arg1 arg2 network_file cidr stderr tmp
 
-        if ! diff -u "$out/run/systemd/network" "${f%.input}.expected"; then
-            echo "**** Unexpected output for $f"
-            exit 1
+    # To make parsing a bit easier when IPv6 is involved, replace all colons between [] with #, ...
+    tmp="$(echo "${cmdline#ip=}" | sed -re ':l; s/(\[[^]:]*):/\1#/; tl')"
+    # ... drop the now unnecessary [] and split the string into colon separated fields as usual, ...
+    IFS=":" read -r ip peer gateway netmask hostname ifname dhcp arg1 arg2 <<<"${tmp//[\[\]]}"
+    # ... and then replace # back to colons for fields that might contain an IPv6 address.
+    ip="${ip//#/:}"
+    peer="${peer//#/:}"
+    gateway="${gateway//#/:}"
+    arg1="${arg1//#/:}"
+    arg2="${arg2//#/:}"
+
+    SYSTEMD_LOG_LEVEL=debug SYSTEMD_PROC_CMDLINE="$cmdline" run_network_generator
+
+    if [[ -n "$ifname" ]]; then
+        network_file="${WORK_DIR:?}/run/systemd/network/70-$ifname.network"
+        grep -q "^Name=$ifname$" "$network_file"
+    else
+        network_file="${WORK_DIR:?}/run/systemd/network/71-default.network"
+        grep -q "^Kind=!\*$" "$network_file"
+    fi
+
+    cat "$network_file"
+
+    if [[ -n "$ip" && -n "$netmask" ]]; then
+        # The "ip" and "netmask" fields are merged together into an IP/CIDR value
+        if [[ "$netmask" =~ ^[0-9]+$ ]]; then
+            cidr="$netmask"
+        else
+            cidr="$(netmask_to_cidr "$netmask")"
         fi
-    ) || exit 1
+
+        grep -q "^Address=$ip/$cidr$" "$network_file"
+    else
+        (! grep -q "^Address=" "$network_file")
+    fi
+    # If the "dhcp" field is empty, it defaults to "off"
+    [[ -z "$dhcp" ]] && dhcp="off"
+    [[ -n "$peer" ]] && grep -q "^Peer=$peer$" "$network_file"
+    [[ -n "$gateway" ]] && grep -q "^Gateway=$gateway$" "$network_file"
+    [[ -n "$hostname" ]] && grep -q "^Hostname=$hostname$" "$network_file"
+    check_dhcp "$dhcp" "$network_file"
+
+    # If the first optional argument is empty, assume the first variant
+    # See: https://github.com/dracutdevs/dracut/blob/4d594210d6ef4f04a9dbadacea73e9461ded352d/modules.d/40network/net-lib.sh#L533
+    if [[ -z "$arg1" || "$arg1" =~ ^[0-9]+$ ]]; then
+        # => [:[<mtu>][:<macaddr>]
+        [[ -n "$arg1" ]] && grep -q "^MTUBytes=$arg1$" "$network_file"
+        [[ -n "$arg2" ]] && grep -q "^MACAddress=$arg2$" "$network_file"
+    else
+        # => [:[<dns1>][:<dns2>]]
+        grep -q "^DNS=$arg1$" "$network_file"
+        [[ -n "$arg2" ]] && grep -q "^DNS=$arg2$" "$network_file"
+    fi
+
+    return 0
+}
+
+# Check if the generated .network files match the expected stored ones
+TEST_DATA="$(dirname "$0")/testdata/test-network-generator-conversion"
+for f in "$TEST_DATA"/test-*.input; do
+    fname="${f##*/}"
+    out="$(mktemp --directory "${WORK_DIR:?}/${fname%%.input}.XXX")"
+
+    # shellcheck disable=SC2046
+    "$GENERATOR_BIN" --root "$out" -- $(cat "$f")
+
+    if ! diff -u "$out/run/systemd/network" "${f%.input}.expected"; then
+        echo "**** Unexpected output for $f"
+        exit 1
+    fi
+
+    rm -rf "${out:?}"
+done
+
+# Now generate bunch of .network units on the fly and check if they contain expected
+# directives & values
+
+# ip={dhcp|on|any|dhcp6|auto6|either6|link6|link-local}
+for dhcp in dhcp on any dhcp6 auto6 either6 link6 link-local off none ibft; do
+    check_one_dhcp "ip=$dhcp"
+done
+
+# ip=<interface>:{dhcp|on|any|dhcp6|auto6|link6|link-local}[:[<mtu>][:<macaddr>]]
+COMMAND_LINES=(
+    "ip=foo:dhcp"
+    "ip=bar:dhcp6"
+    "ip=linklocal99:link-local"
+    "ip=baz1:any:666"
+    "ip=baz1:any:128:52:54:00:a7:8f:ac"
+)
+for cmdline in "${COMMAND_LINES[@]}"; do
+    check_one_interface_dhcp "$cmdline"
+done
+
+# ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft}[:[<mtu>][:<macaddr>]
+# ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft}[:[<dns1>][:<dns2>]]
+COMMAND_LINES=(
+    "ip=1.2.3.4:2.3.4.5:1.2.3.1:255.255.255.0:hello-world.local:dummy99:off"
+    "ip=1.2.3.4:2.3.4.5:1.2.3.1:24:hello-world.local:dummy99:off"
+    "ip=1.2.3.4:2.3.4.5:1.2.3.1:255.255.255.0:hello-world.local:dummy99:off:123"
+    "ip=1.2.3.4:2.3.4.5:1.2.3.1:255.255.255.0:hello-world.local:dummy99:off:123:52:54:00:a7:8f:ac"
+    "ip=1.2.3.4:2.3.4.5:1.2.3.1:255.255.255.0:hello-world.local:dummy99:off::52:54:00:a7:8f:ac"
+    "ip=1.2.3.4:2.3.4.5:1.2.3.1:255.255.255.0:hello-world.local:dummy99:off::"
+    "ip=1.2.3.4:2.3.4.5:1.2.3.1:255.255.255.0:hello-world.local:dummy99:off:1.2.3.2"
+    "ip=1.2.3.4:2.3.4.5:1.2.3.1:255.255.255.0:hello-world.local:dummy99:off:1.2.3.2:1.2.3.3"
+    "ip=192.168.0.2::192.168.0.1:255.255.128.0::foo1:off"
+    "ip=192.168.0.2::192.168.0.1:17::foo1:off"
+    "ip=10.0.0.1:::255.255.255.0::foo99:off"
+    "ip=[fdef:c400:bd01:1096::2]::[fdef:c400:bd01:1096::1]:64::ipv6:off"
+    "ip=[fdef:c400:bd01:1096::2]:[fdef:c400:bd01:1096::99]::64::ipv6:off"
+    "ip=[fdef:c400:bd01:1096::2]::[fdef:c400:bd01:1096::1]:64::ipv6:off:666"
+    "ip=[fdef:c400:bd01:1096::2]::[fdef:c400:bd01:1096::1]:64::ipv6:off:666:52:54:00:a7:8f:ac"
+    "ip=[fdef:c400:bd01:1096::2]::[fdef:c400:bd01:1096::1]:64::ipv6:off::52:54:00:a7:8f:ac"
+    "ip=[fdef:c400:bd01:1096::2]::[fdef:c400:bd01:1096::1]:64::ipv6:off::"
+    "ip=[fdef:c400:bd01:1096::2]::[fdef:c400:bd01:1096::1]:64::ipv6:off:[fdef:c400:bd01:1096::aaaa]"
+    "ip=[fdef:c400:bd01:1096::2]::[fdef:c400:bd01:1096::1]:64::ipv6:off:[fdef:c400:bd01:1096::aaaa]:[fdef:c400:bd01:1096::bbbb]"
+    "ip=:::::dhcp99:any"
+    "ip=:::::dhcp99:dhcp6:666"
+    "ip=:::::dhcp99:dhcp6:666:52:54:00:a7:8f:ac"
+    "ip=:::::dhcp99:dhcp6:10.0.0.128"
+    "ip=:::::dhcp99:dhcp6:10.0.0.128:10.0.0.129"
+    "ip=::::::any"
+    "ip=::::::ibft"
+)
+for cmdline in "${COMMAND_LINES[@]}"; do
+    check_one_long "$cmdline"
+done
+
+INVALID_COMMAND_LINES=(
+    "ip=foo"
+    "ip=:::::::"
+    "ip=:::::::foo"
+    "ip=10.0.0:::255.255.255.0::foo99:off"
+    "ip=10.0.0.1:::255.255.255::foo99:off"
+    "ip=10.0.0.1:::255.255.255.0:invalid_hostname:foo99:off"
+    "ip=10.0.0.1:::255.255.255.0::verylonginterfacename:off"
+    "ip=:::::dhcp99:dhcp6:0"
+    "ip=:::::dhcp99:dhcp6:-1"
+    "ip=:::::dhcp99:dhcp6:666:52:54:00"
+    "ip=fdef:c400:bd01:1096::2::[fdef:c400:bd01:1096::1]:64::ipv6:off:[fdef:c400:bd01:1096::aaaa]"
+    "ip=[fdef:c400:bd01:1096::2]::[fdef:c400:bd01:1096::1]:64::ipv6:off:foo"
+    "ip=[fdef:c400:bd01:1096::2]::[fdef:c400:bd01:1096::1]:64::ipv6:off:[fdef:c400:bd01:1096::aaaa]:foo"
+)
+for cmdline in "${INVALID_COMMAND_LINES[@]}"; do
+    (! SYSTEMD_LOG_LEVEL=debug SYSTEMD_PROC_CMDLINE="$cmdline" "$GENERATOR_BIN")
 done