From c436abd059e63fc34327c6c00ebf817f0965e02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Tue, 16 Apr 2024 09:50:51 +0200 Subject: [PATCH 01/11] util: fix potential NULL pointer assert --- src/util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.c b/src/util.c index 659a990c3..cae95cb7c 100644 --- a/src/util.c +++ b/src/util.c @@ -955,7 +955,7 @@ netplan_netdef_match_interface(const NetplanNetDefinition* netdef, const char* n return !g_strcmp0(name, netdef->id); if (netdef->match.mac) { - if (g_ascii_strcasecmp(netdef->match.mac, mac)) + if (g_ascii_strcasecmp(netdef->match.mac ?: "", mac ?: "")) return FALSE; } From a9d53831afbe79f766fcead5a8975b4de2c16530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Tue, 16 Apr 2024 09:30:34 +0200 Subject: [PATCH 02/11] networkd: add wait-online enumeration utils --- src/networkd.c | 65 ++++++++++++++++++++++++ tests/ctests/meson.build | 1 + tests/ctests/test_netplan_networkd.c | 74 ++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 tests/ctests/test_netplan_networkd.c diff --git a/src/networkd.c b/src/networkd.c index 25121c48a..928938851 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,70 @@ #include "util-internal.h" #include "validation.h" +/** + * Query sysfs for the MAC address (up to 20 bytes for infiniband) of @ifname + * The caller owns the returned string and needs to free it. + */ +STATIC char* +_netplan_sysfs_get_mac_by_ifname(const char* ifname, const char* rootdir) +{ + g_autofree gchar* content = NULL; + g_autofree gchar* sysfs_path = NULL; + sysfs_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, + "sys", "class", "net", ifname, "address", NULL); + + if (!g_file_get_contents (sysfs_path, &content, NULL, NULL)) { + g_debug("%s: Cannot read file contents.", __FUNCTION__); + return NULL; + } + + // Trim whitespace & clone value + return g_strdup(g_strstrip(content)); +} + +/** + * Query sysfs for the driver used by @ifname + * The caller owns the returned string and needs to free it. + */ +STATIC char* +_netplan_sysfs_get_driver_by_ifname(const char* ifname, const char* rootdir) +{ + g_autofree gchar* link = NULL; + g_autofree gchar* sysfs_path = NULL; + sysfs_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, + "sys", "class", "net", ifname, "device", "driver", NULL); + + link = g_file_read_link(sysfs_path, NULL); + if (!link) { + g_debug("%s: Cannot read symlink of %s.", __FUNCTION__, sysfs_path); + return NULL; + } + + return g_path_get_basename(link); +} + +/** + * Enumerate all network interfaces (/sys/clas/net/...) and check + * netplan_netdef_match_interface() to see if they match the current NetDef + */ +STATIC void +_netplan_enumerate_interfaces(const NetplanNetDefinition* def, GHashTable* tbl, const char* rootdir) +{ + g_assert(tbl); + struct if_nameindex *if_nidxs, *intf; + if_nidxs = if_nameindex(); + if (if_nidxs != NULL) { + for (intf = if_nidxs; intf->if_index != 0 || intf->if_name != NULL; intf++) { + if (g_hash_table_contains(tbl, intf->if_name)) continue; + g_autofree gchar* mac = _netplan_sysfs_get_mac_by_ifname(intf->if_name, rootdir); + g_autofree gchar* driver = _netplan_sysfs_get_driver_by_ifname(intf->if_name, rootdir); + if (netplan_netdef_match_interface(def, intf->if_name, mac, driver)) + g_hash_table_add(tbl, g_strdup(intf->if_name)); + } + if_freenameindex(if_nidxs); + } +} + /** * Append WiFi frequencies to wpa_supplicant's freq_list= */ diff --git a/tests/ctests/meson.build b/tests/ctests/meson.build index ed3bc5dc4..dcf4b6c45 100644 --- a/tests/ctests/meson.build +++ b/tests/ctests/meson.build @@ -5,6 +5,7 @@ tests = { 'test_netplan_misc': false, 'test_netplan_validation': false, 'test_netplan_keyfile': false, + 'test_netplan_networkd': false, 'test_netplan_nm': false, 'test_netplan_openvswitch': false, } diff --git a/tests/ctests/test_netplan_networkd.c b/tests/ctests/test_netplan_networkd.c new file mode 100644 index 000000000..e9e4b79ec --- /dev/null +++ b/tests/ctests/test_netplan_networkd.c @@ -0,0 +1,74 @@ +#include +#include +#include +#include + +#include + +#include "netplan.h" +#include "util-internal.h" +#include "networkd.c" + +#include "test_utils.h" + +void +test_wait_online_utils(__unused void** state) +{ + char template[] = "/tmp/netplan.XXXXXX"; + const char* rootdir = mkdtemp(template); + + // create mock sysfs + g_autofree gchar* sys = g_strdup_printf("%s/sys", rootdir); + g_autofree gchar* sys_class = g_strdup_printf("%s/sys/class", rootdir); + g_autofree gchar* sys_class_net = g_strdup_printf("%s/sys/class/net", rootdir); + g_autofree gchar* eth99 = g_strdup_printf("%s/sys/class/net/eth99", rootdir); + g_autofree gchar* eth99_device = g_strdup_printf("%s/device", eth99); + g_autofree gchar* driver = g_strdup_printf("%s/device/driver", eth99); + g_autofree gchar* mac = g_strdup_printf("%s/address", eth99); + g_mkdir_with_parents(eth99_device, 0700); + + // assert MAC address file + assert_true(g_file_set_contents(mac, " aa:bb:cc:dd:ee:ff \r\n\n", -1, NULL)); + g_autofree gchar* mac_value = _netplan_sysfs_get_mac_by_ifname("eth99", rootdir); + assert_string_equal(mac_value, "aa:bb:cc:dd:ee:ff"); + + // assert driver link + assert_int_equal(symlink("../somewhere/drivers/mock_drv", driver), 0); + g_autofree gchar* driver_value = _netplan_sysfs_get_driver_by_ifname("eth99", rootdir); + assert_string_equal(driver_value, "mock_drv"); + + // Cleanup + remove(mac); + remove(driver); + rmdir(eth99_device); + rmdir(eth99); + rmdir(sys_class_net); + rmdir(sys_class); + rmdir(sys); + rmdir(rootdir); +} + + +int +setup(__unused void** state) +{ + return 0; +} + +int +tear_down(__unused void** state) +{ + return 0; +} + +int +main() +{ + + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_wait_online_utils), + }; + + return cmocka_run_group_tests(tests, setup, tear_down); + +} From 3fe2f409cc72dc9999a13c5428c912bd33bbfda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Tue, 16 Apr 2024 09:51:40 +0200 Subject: [PATCH 03/11] generate: enable systemd-networkd-wait-online for non-optional interfaces only Skip s-n-wait-online if we don't have any non-optional interfaces, using a "ConditionPathIsSymbolicLink=" checking Netplan's s-n-wait-online.service enablement symlink. This is in favor to RequiredForOnline=yes as the behavior of upstream (pure) systemd-networkd-wait-online.service is not mean to be used in this way. If "RequiredForOnline=no" sd-networkd-wait-online will fully ignore the corresponding interface and it will block/delay network-online.target if no interfaces are "RequiredForOnline=yes" at all. FR-7246 --- src/generate.c | 24 +++++++------ src/networkd.c | 66 ++++++++++++++++++++++++++++------ src/networkd.h | 3 ++ tests/generator/base.py | 2 ++ tests/generator/test_args.py | 56 +++++++++++++++++++++++++++++ tests/generator/test_common.py | 5 --- 6 files changed, 131 insertions(+), 25 deletions(-) diff --git a/src/generate.c b/src/generate.c index 35179a27a..19198ff96 100644 --- a/src/generate.c +++ b/src/generate.c @@ -63,7 +63,7 @@ reload_udevd(void) * Create enablement symlink for systemd-networkd.service. */ static void -enable_networkd(const char* generator_dir) +enable_networkd(const char* generator_dir, gboolean enable_wait_online) { g_autofree char* link = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "multi-user.target.wants", "systemd-networkd.service", NULL); g_debug("We created networkd configuration, adding %s enablement symlink", link); @@ -75,13 +75,15 @@ enable_networkd(const char* generator_dir) // LCOV_EXCL_STOP } - g_autofree char* link2 = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "network-online.target.wants", "systemd-networkd-wait-online.service", NULL); - _netplan_safe_mkdir_p_dir(link2); - if (symlink("/lib/systemd/system/systemd-networkd-wait-online.service", link2) < 0 && errno != EEXIST) { - // LCOV_EXCL_START - g_fprintf(stderr, "failed to create enablement symlink: %m\n"); - exit(1); - // LCOV_EXCL_STOP + if (enable_wait_online) { + g_autofree char* link2 = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "network-online.target.wants", "systemd-networkd-wait-online.service", NULL); + _netplan_safe_mkdir_p_dir(link2); + if (symlink("/lib/systemd/system/systemd-networkd-wait-online.service", link2) < 0 && errno != EEXIST) { + // LCOV_EXCL_START + g_fprintf(stderr, "failed to create enablement symlink: %m\n"); + exit(1); + // LCOV_EXCL_STOP + } } } @@ -300,10 +302,11 @@ int main(int argc, char** argv) if (netplan_state_get_backend(np_state) == NETPLAN_BACKEND_NM || any_nm) _netplan_g_string_free_to_file(g_string_new(NULL), rootdir, "/run/NetworkManager/conf.d/10-globally-managed-devices.conf", NULL); + gboolean enable_wait_online = _netplan_networkd_write_wait_online(np_state, rootdir); if (called_as_generator) { /* Ensure networkd starts if we have any configuration for it */ if (any_networkd) - enable_networkd(files[0]); + enable_networkd(files[0], enable_wait_online); /* Leave a stamp file so that we don't regenerate the configuration * multiple times and userspace can wait for it to finish */ @@ -324,7 +327,8 @@ int main(int argc, char** argv) /* covered via 'cloud-init' integration test */ if (any_networkd) { start_unit_jit("systemd-networkd.socket"); - start_unit_jit("systemd-networkd-wait-online.service"); + if (enable_wait_online) + start_unit_jit("systemd-networkd-wait-online.service"); start_unit_jit("systemd-networkd.service"); } g_autofree char* glob_run = g_build_path(G_DIR_SEPARATOR_S, diff --git a/src/networkd.c b/src/networkd.c index 928938851..2ff70c955 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -803,7 +803,6 @@ _netplan_netdef_write_network_file( g_autoptr(GString) link = NULL; GString* s = NULL; mode_t orig_umask; - gboolean is_optional = def->optional; SET_OPT_OUT_PTR(has_been_written, FALSE); @@ -826,17 +825,9 @@ _netplan_netdef_write_network_file( else /* "off" */ mode = "always-down"; g_string_append_printf(link, "ActivationPolicy=%s\n", mode); - /* When activation-mode is used we default to being optional. - * Otherwise systemd might wait indefinitely for the interface to - * become online. - */ - is_optional = TRUE; } - if (is_optional || def->optional_addresses) { - if (is_optional) { - g_string_append(link, "RequiredForOnline=no\n"); - } + if (def->optional_addresses) { for (unsigned i = 0; NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name != NULL; ++i) { if (def->optional_addresses & NETPLAN_OPTIONAL_ADDRESS_TYPES[i].flag) { g_string_append_printf(link, "OptionalAddresses=%s\n", NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name); @@ -1466,6 +1457,60 @@ _netplan_netdef_write_networkd( return TRUE; } +gboolean +_netplan_networkd_write_wait_online(const NetplanState* np_state, const char* rootdir) +{ + // Hash set of non-optional interfaces to wait for + GHashTable* non_optional_interfaces = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + NetplanStateIterator iter; + netplan_state_iterator_init(np_state, &iter); + while (netplan_state_iterator_has_next(&iter)) { + const NetplanNetDefinition* def = netplan_state_iterator_next(&iter); + if (def->backend != NETPLAN_BACKEND_NETWORKD) + continue; + /* When activation-mode is used we default to being optional. + * Otherwise, systemd might wait indefinitely for the interface to + * come online. + */ + if (!(def->optional || def->activation_mode)) { + if (!netplan_netdef_has_match(def)) { // no matching => single interface + g_hash_table_add(non_optional_interfaces, g_strdup(def->id)); + } else if (def->set_name) { // matching on a single interface + g_hash_table_add(non_optional_interfaces, g_strdup(def->set_name)); + } else { // matching on potentially multiple interfaces + // XXX: we shouldn't run this enumeration for every NetDef... + _netplan_enumerate_interfaces(def, non_optional_interfaces, rootdir); + } + } + } + + // create run/systemd/system/systemd-networkd-wait-online.service.d/ + const char* override = "/run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan.conf"; + // The "ConditionPathIsSymbolicLink" is Netplan's s-n-wait-online enablement symlink, + // as we want to run -wait-online only if enabled by Netplan. + GString* content = g_string_new("[Unit]\n" + "ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service\n"); + if (g_hash_table_size(non_optional_interfaces) == 0) { + _netplan_g_string_free_to_file(content, rootdir, override, NULL); + g_hash_table_destroy(non_optional_interfaces); + return FALSE; + } + + // We have non-optional interface, so let's wait for those explicitly + GHashTableIter idx; + gpointer ifname; + g_string_append(content, "\n[Service]\nExecStart=\n" + "ExecStart=/lib/systemd/systemd-networkd-wait-online"); + g_hash_table_iter_init (&idx, non_optional_interfaces); + while (g_hash_table_iter_next (&idx, &ifname, NULL)) + g_string_append_printf(content, " -i %s", (const char*)ifname); + g_string_append(content, "\n"); + + _netplan_g_string_free_to_file(content, rootdir, override, NULL); + g_hash_table_destroy(non_optional_interfaces); + return TRUE; +} + /** * Clean up all generated configurations in @rootdir from previous runs. */ @@ -1479,6 +1524,7 @@ _netplan_networkd_cleanup(const char* rootdir) _netplan_unlink_glob(rootdir, "/run/udev/rules.d/99-netplan-*"); _netplan_unlink_glob(rootdir, "/run/systemd/system/network.target.wants/netplan-regdom.service"); _netplan_unlink_glob(rootdir, "/run/systemd/system/netplan-regdom.service"); + _netplan_unlink_glob(rootdir, "/run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan*.conf"); /* Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an * upgraded system, we need to make sure to clean those up. */ _netplan_unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa@*.service"); diff --git a/src/networkd.h b/src/networkd.h index 2bd084830..8332b7b2a 100644 --- a/src/networkd.h +++ b/src/networkd.h @@ -37,5 +37,8 @@ _netplan_netdef_write_network_file( gboolean* has_been_written, GError** error); +NETPLAN_INTERNAL gboolean +_netplan_networkd_write_wait_online(const NetplanState* np_state, const char* rootdir); + NETPLAN_INTERNAL void _netplan_networkd_cleanup(const char* rootdir); diff --git a/tests/generator/base.py b/tests/generator/base.py index 17ddbb8c5..e8c12bbdf 100644 --- a/tests/generator/base.py +++ b/tests/generator/base.py @@ -466,6 +466,8 @@ def assert_ovs(self, file_contents_map): self.assertEqual(set(os.listdir(self.workdir.name)) - {'lib'}, {'etc', 'run'}) ovs_systemd_dir = set(os.listdir(systemd_dir)) ovs_systemd_dir.remove('systemd-networkd.service.wants') + if 'systemd-networkd-wait-online.service.d' in ovs_systemd_dir: + ovs_systemd_dir.remove('systemd-networkd-wait-online.service.d') self.assertEqual(ovs_systemd_dir, {'netplan-ovs-' + f for f in file_contents_map}) for fname, contents in file_contents_map.items(): fname = 'netplan-ovs-' + fname diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py index ded0f69a1..496b1b996 100644 --- a/tests/generator/test_args.py +++ b/tests/generator/test_args.py @@ -125,6 +125,11 @@ def test_systemd_generator(self): version: 2 ethernets: eth0: + dhcp4: true + eth1: + dhcp4: true + optional: true + eth2: dhcp4: true''') os.chmod(conf, mode=0o600) outdir = os.path.join(self.workdir.name, 'out') @@ -138,12 +143,28 @@ def test_systemd_generator(self): n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network') self.assertTrue(os.path.exists(n)) os.unlink(n) + n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth1.network') + self.assertTrue(os.path.exists(n)) + os.unlink(n) + n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth2.network') + self.assertTrue(os.path.exists(n)) + os.unlink(n) # should auto-enable networkd and -wait-online + service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system') self.assertTrue(os.path.islink(os.path.join( outdir, 'multi-user.target.wants', 'systemd-networkd.service'))) self.assertTrue(os.path.islink(os.path.join( outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service'))) + override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf') + self.assertTrue(os.path.isfile(override)) + with open(override, 'r') as f: + self.assertEqual(f.read(), '''[Unit] +ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service + +[Service] +ExecStart= +ExecStart=/lib/systemd/systemd-networkd-wait-online -i eth0 -i eth2\n''') # should be a no-op the second time while the stamp exists out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir], @@ -157,6 +178,41 @@ def test_systemd_generator(self): subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) self.assertTrue(os.path.exists(n)) + def test_systemd_generator_all_optional(self): + conf = os.path.join(self.confdir, 'a.yaml') + os.makedirs(os.path.dirname(conf)) + with open(conf, 'w') as f: + f.write('''network: + version: 2 + ethernets: + eth0: + dhcp4: true + optional: true''') + outdir = os.path.join(self.workdir.name, 'out') + os.mkdir(outdir) + + generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') + os.makedirs(os.path.dirname(generator)) + os.symlink(exe_generate, generator) + + subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) + n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network') + self.assertTrue(os.path.exists(n)) + os.unlink(n) + + # should auto-enable networkd but not -wait-online + service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system') + self.assertTrue(os.path.islink(os.path.join( + outdir, 'multi-user.target.wants', 'systemd-networkd.service'))) + self.assertFalse(os.path.islink(os.path.join( + outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service'))) + override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf') + self.assertTrue(os.path.isfile(override)) + with open(override, 'r') as f: + self.assertEqual(f.read(), '''[Unit] +ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service +''') + def test_systemd_generator_noconf(self): outdir = os.path.join(self.workdir.name, 'out') os.mkdir(outdir) diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py index 2128e5875..d6552f3de 100644 --- a/tests/generator/test_common.py +++ b/tests/generator/test_common.py @@ -35,9 +35,6 @@ def test_optional(self): self.assert_networkd({'eth0.network': '''[Match] Name=eth0 -[Link] -RequiredForOnline=no - [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 @@ -85,7 +82,6 @@ def test_activation_mode_off(self): [Link] ActivationPolicy=always-down -RequiredForOnline=no [Network] DHCP=ipv6 @@ -109,7 +105,6 @@ def test_activation_mode_manual(self): [Link] ActivationPolicy=manual -RequiredForOnline=no [Network] DHCP=ipv6 From c567a4bb925e110e1e87613d631159a07a68d35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 17 Apr 2024 10:23:16 +0200 Subject: [PATCH 04/11] CLI:utils: Do not ask for daemon-reload password interactively --- netplan_cli/cli/utils.py | 2 +- tests/cli_legacy.py | 3 +-- tests/test_utils.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/netplan_cli/cli/utils.py b/netplan_cli/cli/utils.py index c89af1a78..c9f9ce201 100644 --- a/netplan_cli/cli/utils.py +++ b/netplan_cli/cli/utils.py @@ -165,7 +165,7 @@ def systemctl_is_installed(unit_pattern): def systemctl_daemon_reload(): '''Reload systemd unit files from disk and re-calculate its dependencies''' - subprocess.check_call(['systemctl', 'daemon-reload']) + subprocess.check_call(['systemctl', 'daemon-reload', '--no-ask-password']) def ip_addr_flush(iface): diff --git a/tests/cli_legacy.py b/tests/cli_legacy.py index 897043ea6..decde8117 100755 --- a/tests/cli_legacy.py +++ b/tests/cli_legacy.py @@ -98,8 +98,7 @@ def test_with_empty_config(self): enlol: {dhcp4: yes}''') os.chmod(path_a, mode=0o600) os.chmod(path_b, mode=0o600) - out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name], stderr=subprocess.STDOUT) - self.assertEqual(out, b'') + subprocess.check_call(exe_cli + ['generate', '--root-dir', self.workdir.name]) self.assertEqual(os.listdir(os.path.join(self.workdir.name, 'run', 'systemd', 'network')), ['10-netplan-enlol.network']) diff --git a/tests/test_utils.py b/tests/test_utils.py index 55c907cf2..c7ab0601e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -380,7 +380,7 @@ def test_systemctl_daemon_reload(self): os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env utils.systemctl_daemon_reload() self.assertEqual(self.mock_cmd.calls(), [ - ['systemctl', 'daemon-reload'] + ['systemctl', 'daemon-reload', '--no-ask-password'] ]) def test_ip_addr_flush(self): From c0c626054b218e1cc4398e9bbde7f5c3818eb8aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 17 Apr 2024 10:23:51 +0200 Subject: [PATCH 05/11] CLI:generate: call daemon-reload after (re-)generating services --- netplan_cli/cli/commands/generate.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/netplan_cli/cli/commands/generate.py b/netplan_cli/cli/commands/generate.py index f84b1710e..dcbbb5349 100644 --- a/netplan_cli/cli/commands/generate.py +++ b/netplan_cli/cli/commands/generate.py @@ -81,5 +81,12 @@ def command_generate(self): if self.mapping: argv += ['--mapping', self.mapping] logging.debug('command generate: running %s', argv) + res = subprocess.call(argv) + # reload systemd, as we might have changed service units, such as + # /run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan.conf + try: + utils.systemctl_daemon_reload() + except subprocess.CalledProcessError as e: + logging.warning(e) # FIXME: os.execv(argv[0], argv) would be better but fails coverage - sys.exit(subprocess.call(argv)) + sys.exit(res) From df8161a3f7081ac9d1875e6b3e79b1e0df49d7af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 17 Apr 2024 10:24:36 +0200 Subject: [PATCH 06/11] wait-online: Do not block on loopback interface --- src/networkd.c | 12 +++++++++--- tests/generator/test_args.py | 8 ++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/networkd.c b/src/networkd.c index 2ff70c955..6aa4252bb 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -1498,12 +1498,18 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro // We have non-optional interface, so let's wait for those explicitly GHashTableIter idx; - gpointer ifname; + gpointer key; g_string_append(content, "\n[Service]\nExecStart=\n" "ExecStart=/lib/systemd/systemd-networkd-wait-online"); g_hash_table_iter_init (&idx, non_optional_interfaces); - while (g_hash_table_iter_next (&idx, &ifname, NULL)) - g_string_append_printf(content, " -i %s", (const char*)ifname); + while (g_hash_table_iter_next (&idx, &key, NULL)) { + const char* ifname = key; + g_string_append_printf(content, " -i %s", ifname); + // XXX: We should be checking IFF_LOOPBACK instead of interface name. + // But don't have access to the flags here. + if (!g_strcmp0(ifname, "lo")) + g_string_append(content, ":carrier"); // "carrier" as min-oper state for loopback + } g_string_append(content, "\n"); _netplan_g_string_free_to_file(content, rootdir, override, NULL); diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py index 496b1b996..765ad431c 100644 --- a/tests/generator/test_args.py +++ b/tests/generator/test_args.py @@ -129,8 +129,8 @@ def test_systemd_generator(self): eth1: dhcp4: true optional: true - eth2: - dhcp4: true''') + lo: + addresses: ["127.0.0.1/8", "::1/128"]''') os.chmod(conf, mode=0o600) outdir = os.path.join(self.workdir.name, 'out') os.mkdir(outdir) @@ -146,7 +146,7 @@ def test_systemd_generator(self): n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth1.network') self.assertTrue(os.path.exists(n)) os.unlink(n) - n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth2.network') + n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-lo.network') self.assertTrue(os.path.exists(n)) os.unlink(n) @@ -164,7 +164,7 @@ def test_systemd_generator(self): [Service] ExecStart= -ExecStart=/lib/systemd/systemd-networkd-wait-online -i eth0 -i eth2\n''') +ExecStart=/lib/systemd/systemd-networkd-wait-online -i lo:carrier -i eth0\n''') # should be a no-op the second time while the stamp exists out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir], From ef055db103c6209b448b731b0348d08140b1ff43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 17 Apr 2024 10:27:27 +0200 Subject: [PATCH 07/11] generate: Do not touch wait-online, if we don't have any networkd NetDefs --- src/generate.c | 5 ++++- tests/generator/test_args.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/generate.c b/src/generate.c index 19198ff96..d6edb0ab7 100644 --- a/src/generate.c +++ b/src/generate.c @@ -302,7 +302,10 @@ int main(int argc, char** argv) if (netplan_state_get_backend(np_state) == NETPLAN_BACKEND_NM || any_nm) _netplan_g_string_free_to_file(g_string_new(NULL), rootdir, "/run/NetworkManager/conf.d/10-globally-managed-devices.conf", NULL); - gboolean enable_wait_online = _netplan_networkd_write_wait_online(np_state, rootdir); + gboolean enable_wait_online = FALSE; + if (any_networkd) + enable_wait_online = _netplan_networkd_write_wait_online(np_state, rootdir); + if (called_as_generator) { /* Ensure networkd starts if we have any configuration for it */ if (any_networkd) diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py index 765ad431c..951868766 100644 --- a/tests/generator/test_args.py +++ b/tests/generator/test_args.py @@ -39,6 +39,10 @@ def test_no_configs(self): self.assert_nm(None) self.assert_nm_udev(None) self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # should not touch -wait-online + service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system') + override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf') + self.assertFalse(os.path.isfile(override)) def test_empty_config(self): self.generate('') From 7d5d50cf31d7f6ae11405f0c71b644075925235d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 17 Apr 2024 12:01:13 +0200 Subject: [PATCH 08/11] wait-online: wait for existing interfaces only and downgrade operational state for interfaces without IP configuration --- src/networkd.c | 71 ++++++++++++++++++++++++---------- tests/generator/test_args.py | 10 ++--- tests/integration/ethernets.py | 37 ++++++++++++++++++ 3 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/networkd.c b/src/networkd.c index 6aa4252bb..14d65ae5f 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -75,28 +75,42 @@ _netplan_sysfs_get_driver_by_ifname(const char* ifname, const char* rootdir) return g_path_get_basename(link); } -/** - * Enumerate all network interfaces (/sys/clas/net/...) and check - * netplan_netdef_match_interface() to see if they match the current NetDef - */ STATIC void -_netplan_enumerate_interfaces(const NetplanNetDefinition* def, GHashTable* tbl, const char* rootdir) +_netplan_query_system_interfaces(GHashTable* tbl) { g_assert(tbl); struct if_nameindex *if_nidxs, *intf; if_nidxs = if_nameindex(); if (if_nidxs != NULL) { - for (intf = if_nidxs; intf->if_index != 0 || intf->if_name != NULL; intf++) { - if (g_hash_table_contains(tbl, intf->if_name)) continue; - g_autofree gchar* mac = _netplan_sysfs_get_mac_by_ifname(intf->if_name, rootdir); - g_autofree gchar* driver = _netplan_sysfs_get_driver_by_ifname(intf->if_name, rootdir); - if (netplan_netdef_match_interface(def, intf->if_name, mac, driver)) - g_hash_table_add(tbl, g_strdup(intf->if_name)); - } + for (intf = if_nidxs; intf->if_index != 0 || intf->if_name != NULL; intf++) + g_hash_table_add(tbl, g_strdup(intf->if_name)); if_freenameindex(if_nidxs); } } +/** + * Enumerate all network interfaces (/sys/clas/net/...) and check + * netplan_netdef_match_interface() to see if they match the current NetDef + */ +STATIC void +_netplan_enumerate_interfaces(const NetplanNetDefinition* def, GHashTable* ifaces, GHashTable* tbl, const char* carrier, const char* set_name, const char* rootdir) +{ + g_assert(ifaces); + g_assert(tbl); + + GHashTableIter iter; + gpointer key; + g_hash_table_iter_init (&iter, ifaces); + while (g_hash_table_iter_next (&iter, &key, NULL)) { + const char* ifname = key; + if (g_hash_table_contains(tbl, ifname)|| (set_name && g_hash_table_contains(tbl, set_name))) continue; + g_autofree gchar* mac = _netplan_sysfs_get_mac_by_ifname(ifname, rootdir); + g_autofree gchar* driver = _netplan_sysfs_get_driver_by_ifname(ifname, rootdir); + if (netplan_netdef_match_interface(def, ifname, mac, driver)) + g_hash_table_replace(tbl, set_name ? g_strdup(set_name) : g_strdup(ifname), g_strdup(carrier)); + } +} + /** * Append WiFi frequencies to wpa_supplicant's freq_list= */ @@ -1460,29 +1474,41 @@ _netplan_netdef_write_networkd( gboolean _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* rootdir) { + // Set of all current network interfaces, potentially non yet renamed + GHashTable* system_interfaces = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + _netplan_query_system_interfaces(system_interfaces); + // Hash set of non-optional interfaces to wait for - GHashTable* non_optional_interfaces = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + GHashTable* non_optional_interfaces = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); NetplanStateIterator iter; netplan_state_iterator_init(np_state, &iter); while (netplan_state_iterator_has_next(&iter)) { - const NetplanNetDefinition* def = netplan_state_iterator_next(&iter); + NetplanNetDefinition* def = netplan_state_iterator_next(&iter); if (def->backend != NETPLAN_BACKEND_NETWORKD) continue; + /* When activation-mode is used we default to being optional. * Otherwise, systemd might wait indefinitely for the interface to * come online. */ if (!(def->optional || def->activation_mode)) { - if (!netplan_netdef_has_match(def)) { // no matching => single interface - g_hash_table_add(non_optional_interfaces, g_strdup(def->id)); - } else if (def->set_name) { // matching on a single interface - g_hash_table_add(non_optional_interfaces, g_strdup(def->set_name)); + // Check if we have any IP configuration + struct address_iter* addr_iter = _netplan_netdef_new_address_iter(def); + gboolean any_ips = _netplan_address_iter_next(addr_iter) != NULL || netplan_netdef_get_link_local_ipv4(def) || netplan_netdef_get_link_local_ipv6(def); + _netplan_address_iter_free(addr_iter); + + // no matching => single interface, ignoring non-existing interfaces + if (!netplan_netdef_has_match(def) && g_hash_table_contains(system_interfaces, def->id)) { + g_hash_table_replace(non_optional_interfaces, g_strdup(def->id), any_ips ? g_strdup("degraded") : g_strdup("carrier")); + } else if (def->set_name) { // matching on a single interface, to be renamed + _netplan_enumerate_interfaces(def, system_interfaces, non_optional_interfaces, any_ips ? "degraded" : "carrier", def->set_name, rootdir); } else { // matching on potentially multiple interfaces // XXX: we shouldn't run this enumeration for every NetDef... - _netplan_enumerate_interfaces(def, non_optional_interfaces, rootdir); + _netplan_enumerate_interfaces(def, system_interfaces, non_optional_interfaces, any_ips ? "degraded" : "carrier", NULL, rootdir); } } } + g_hash_table_destroy(system_interfaces); // create run/systemd/system/systemd-networkd-wait-online.service.d/ const char* override = "/run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan.conf"; @@ -1498,17 +1524,20 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro // We have non-optional interface, so let's wait for those explicitly GHashTableIter idx; - gpointer key; + gpointer key, value; g_string_append(content, "\n[Service]\nExecStart=\n" "ExecStart=/lib/systemd/systemd-networkd-wait-online"); g_hash_table_iter_init (&idx, non_optional_interfaces); - while (g_hash_table_iter_next (&idx, &key, NULL)) { + while (g_hash_table_iter_next (&idx, &key, &value)) { const char* ifname = key; + const char* min_oper_state = value; g_string_append_printf(content, " -i %s", ifname); // XXX: We should be checking IFF_LOOPBACK instead of interface name. // But don't have access to the flags here. if (!g_strcmp0(ifname, "lo")) g_string_append(content, ":carrier"); // "carrier" as min-oper state for loopback + else if (min_oper_state) + g_string_append_printf(content, ":%s", min_oper_state); } g_string_append(content, "\n"); diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py index 951868766..b21534842 100644 --- a/tests/generator/test_args.py +++ b/tests/generator/test_args.py @@ -128,9 +128,9 @@ def test_systemd_generator(self): f.write('''network: version: 2 ethernets: - eth0: + eth99: dhcp4: true - eth1: + eth98: dhcp4: true optional: true lo: @@ -144,10 +144,10 @@ def test_systemd_generator(self): os.symlink(exe_generate, generator) subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) - n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network') + n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth99.network') self.assertTrue(os.path.exists(n)) os.unlink(n) - n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth1.network') + n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth98.network') self.assertTrue(os.path.exists(n)) os.unlink(n) n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-lo.network') @@ -168,7 +168,7 @@ def test_systemd_generator(self): [Service] ExecStart= -ExecStart=/lib/systemd/systemd-networkd-wait-online -i lo:carrier -i eth0\n''') +ExecStart=/lib/systemd/systemd-networkd-wait-online -i lo:carrier\n''') # eth99 does not exist on the system # should be a no-op the second time while the stamp exists out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir], diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index e43f533d9..7028a498c 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -22,6 +22,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import sys import subprocess import unittest @@ -390,6 +391,42 @@ def test_link_local_disabled(self): ['inet6 9876:bbbb::11/70', 'inet 172.16.5.3/20'], ['inet6 fe80:', 'inet 169.254.']) + def test_systemd_networkd_wait_online(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + lo: + addresses: ["127.0.0.1/8", "::1/128"] + optional: true + doesnotexist: + addresses: ["10.0.0.42/24"] + findme: + match: + macaddress: %(ec_mac)s + link-local: [] + set-name: "findme" + %(e2c)s: + addresses: ["10.0.0.1/24"] +''' % {'r': self.backend, 'ec_mac': self.dev_e_client_mac, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client]) + override = os.path.join('/run', 'systemd', 'system', 'systemd-networkd-wait-online.service.d', '10-netplan.conf') + self.assertTrue(os.path.isfile(override)) + + with open(override, 'r') as f: + # lo is optional/ignored and should not be listed + # doesnotexist should not be listed, as it does not exist + # should be listed as "findme", using reduced operational state + # should be listed normally + self.assertEqual(f.read(), '''[Unit] +ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service + +[Service] +ExecStart= +ExecStart=/lib/systemd/systemd-networkd-wait-online -i %s:degraded -i findme:carrier +''' % self.dev_e2_client) + @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") From a4f803ee20d9b7727ea6b4b4bd7cc66fd8248081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 17 Apr 2024 17:42:27 +0200 Subject: [PATCH 09/11] wait-online: account for DHCPv4/v6 addresses --- src/networkd.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/networkd.c b/src/networkd.c index 14d65ae5f..0e6c0afe9 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -1494,7 +1494,11 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro if (!(def->optional || def->activation_mode)) { // Check if we have any IP configuration struct address_iter* addr_iter = _netplan_netdef_new_address_iter(def); - gboolean any_ips = _netplan_address_iter_next(addr_iter) != NULL || netplan_netdef_get_link_local_ipv4(def) || netplan_netdef_get_link_local_ipv6(def); + gboolean any_ips = _netplan_address_iter_next(addr_iter) != NULL + || netplan_netdef_get_dhcp4(def) + || netplan_netdef_get_dhcp6(def) + || netplan_netdef_get_link_local_ipv4(def) + || netplan_netdef_get_link_local_ipv6(def); _netplan_address_iter_free(addr_iter); // no matching => single interface, ignoring non-existing interfaces From a00ec971fff2c61f5d5d038fe3f321c19d3a0ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 17 Apr 2024 18:01:50 +0200 Subject: [PATCH 10/11] wait-online: do not require virtual devices to be created already --- src/networkd.c | 8 ++++++-- tests/generator/test_args.py | 10 ++++++++-- tests/integration/ethernets.py | 8 ++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/networkd.c b/src/networkd.c index 0e6c0afe9..982781e0d 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -1501,8 +1501,12 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro || netplan_netdef_get_link_local_ipv6(def); _netplan_address_iter_free(addr_iter); - // no matching => single interface, ignoring non-existing interfaces - if (!netplan_netdef_has_match(def) && g_hash_table_contains(system_interfaces, def->id)) { + // no matching => single physical interface, ignoring non-existing interfaces + // OR: virtual interfaces, those will be created later on and cannot have a matching condition + gboolean physical_no_match_or_virtual = FALSE + || (!netplan_netdef_has_match(def) && g_hash_table_contains(system_interfaces, def->id)) + || (netplan_netdef_get_type(def) >= NETPLAN_DEF_TYPE_VIRTUAL); + if (physical_no_match_or_virtual) { g_hash_table_replace(non_optional_interfaces, g_strdup(def->id), any_ips ? g_strdup("degraded") : g_strdup("carrier")); } else if (def->set_name) { // matching on a single interface, to be renamed _netplan_enumerate_interfaces(def, system_interfaces, non_optional_interfaces, any_ips ? "degraded" : "carrier", def->set_name, rootdir); diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py index b21534842..9da36a60a 100644 --- a/tests/generator/test_args.py +++ b/tests/generator/test_args.py @@ -134,7 +134,10 @@ def test_systemd_generator(self): dhcp4: true optional: true lo: - addresses: ["127.0.0.1/8", "::1/128"]''') + addresses: ["127.0.0.1/8", "::1/128"] + bridges: + br0: + dhcp4: true''') os.chmod(conf, mode=0o600) outdir = os.path.join(self.workdir.name, 'out') os.mkdir(outdir) @@ -153,6 +156,9 @@ def test_systemd_generator(self): n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-lo.network') self.assertTrue(os.path.exists(n)) os.unlink(n) + n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-br0.network') + self.assertTrue(os.path.exists(n)) + os.unlink(n) # should auto-enable networkd and -wait-online service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system') @@ -168,7 +174,7 @@ def test_systemd_generator(self): [Service] ExecStart= -ExecStart=/lib/systemd/systemd-networkd-wait-online -i lo:carrier\n''') # eth99 does not exist on the system +ExecStart=/lib/systemd/systemd-networkd-wait-online -i br0:degraded -i lo:carrier\n''') # eth99 does not exist on the system # should be a no-op the second time while the stamp exists out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir], diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 7028a498c..1e57816cf 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -409,8 +409,12 @@ def test_systemd_networkd_wait_online(self): set-name: "findme" %(e2c)s: addresses: ["10.0.0.1/24"] + bridges: + br0: + addresses: ["10.0.0.2/24"] + interfaces: [%(e2c)s] ''' % {'r': self.backend, 'ec_mac': self.dev_e_client_mac, 'e2c': self.dev_e2_client}) - self.generate_and_settle([self.dev_e2_client]) + self.generate_and_settle([self.dev_e2_client, 'br0']) override = os.path.join('/run', 'systemd', 'system', 'systemd-networkd-wait-online.service.d', '10-netplan.conf') self.assertTrue(os.path.isfile(override)) @@ -424,7 +428,7 @@ def test_systemd_networkd_wait_online(self): [Service] ExecStart= -ExecStart=/lib/systemd/systemd-networkd-wait-online -i %s:degraded -i findme:carrier +ExecStart=/lib/systemd/systemd-networkd-wait-online -i %s:degraded -i br0:degraded -i findme:carrier ''' % self.dev_e2_client) From 1cb21290292c029369b21ec4fd812c1c36b869ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 18 Apr 2024 12:16:56 +0200 Subject: [PATCH 11/11] wait-online: recognize that bridge/bond members will never gain link-local addresses --- src/networkd.c | 12 ++++++++---- tests/generator/test_args.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/networkd.c b/src/networkd.c index 982781e0d..23798376c 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -1493,12 +1493,16 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro */ if (!(def->optional || def->activation_mode)) { // Check if we have any IP configuration + // bond and bridge members will never ask for link-local addresses (see above) struct address_iter* addr_iter = _netplan_netdef_new_address_iter(def); - gboolean any_ips = _netplan_address_iter_next(addr_iter) != NULL + gboolean routable = _netplan_address_iter_next(addr_iter) != NULL || netplan_netdef_get_dhcp4(def) - || netplan_netdef_get_dhcp6(def) - || netplan_netdef_get_link_local_ipv4(def) - || netplan_netdef_get_link_local_ipv6(def); + || netplan_netdef_get_dhcp6(def); + gboolean degraded = ( netplan_netdef_get_link_local_ipv4(def) + && !(netplan_netdef_get_bond_link(def) || netplan_netdef_get_bridge_link(def))) + || ( netplan_netdef_get_link_local_ipv6(def) + && !(netplan_netdef_get_bond_link(def) || netplan_netdef_get_bridge_link(def))); + gboolean any_ips = routable || degraded; _netplan_address_iter_free(addr_iter); // no matching => single physical interface, ignoring non-existing interfaces diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py index 9da36a60a..a824a8a05 100644 --- a/tests/generator/test_args.py +++ b/tests/generator/test_args.py @@ -135,9 +135,20 @@ def test_systemd_generator(self): optional: true lo: addresses: ["127.0.0.1/8", "::1/128"] + vlans: + eth99.42: + link: eth99 + id: 42 + link-local: [ipv4, ipv6] # this is ignored for bridge-members + eth99.43: + link: eth99 + id: 43 + link-local: [] + addresses: [10.0.0.2/24] bridges: br0: - dhcp4: true''') + dhcp4: true + interfaces: [eth99.42, eth99.43]''') os.chmod(conf, mode=0o600) outdir = os.path.join(self.workdir.name, 'out') os.mkdir(outdir) @@ -169,12 +180,13 @@ def test_systemd_generator(self): override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf') self.assertTrue(os.path.isfile(override)) with open(override, 'r') as f: + # eth99 does not exist on the system, so will not be listed self.assertEqual(f.read(), '''[Unit] ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service [Service] ExecStart= -ExecStart=/lib/systemd/systemd-networkd-wait-online -i br0:degraded -i lo:carrier\n''') # eth99 does not exist on the system +ExecStart=/lib/systemd/systemd-networkd-wait-online -i eth99.43:degraded -i br0:degraded -i lo:carrier -i eth99.42:carrier\n''') # should be a no-op the second time while the stamp exists out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir],