diff --git a/man/udevadm.xml b/man/udevadm.xml index a591ab8c34..396d418f83 100644 --- a/man/udevadm.xml +++ b/man/udevadm.xml @@ -175,6 +175,14 @@ Cleanup the udev database. + + + + + Wait for device to be initialized. If argument SECONDS + is not specified, the default is to wait forever. + + diff --git a/shell-completion/bash/udevadm b/shell-completion/bash/udevadm index 0e9dbb0c4e..c151a41978 100644 --- a/shell-completion/bash/udevadm +++ b/shell-completion/bash/udevadm @@ -48,7 +48,8 @@ _udevadm() { local -A OPTS=( [COMMON]='-h --help -V --version' [DEBUG]='-d --debug' - [INFO_STANDALONE]='-r --root -a --attribute-walk -x --export -e --export-db -c --cleanup-db' + [INFO_STANDALONE]='-r --root -a --attribute-walk -x --export -e --export-db -c --cleanup-db + -w --wait-for-initialization' [INFO_ARG]='-q --query -p --path -n --name -P --export-prefix -d --device-id-of-file' [TRIGGER_STANDALONE]='-v --verbose -n --dry-run -w --settle --wait-daemon' [TRIGGER_ARG]='-t --type -c --action -s --subsystem-match -S --subsystem-nomatch diff --git a/src/rfkill/rfkill.c b/src/rfkill/rfkill.c index fe26566fa3..f9d309c909 100644 --- a/src/rfkill/rfkill.c +++ b/src/rfkill/rfkill.c @@ -109,7 +109,7 @@ static int determine_state_file( if (r < 0) return r; - r = device_wait_for_initialization(d, "rfkill", &device); + r = device_wait_for_initialization(d, "rfkill", USEC_INFINITY, &device); if (r < 0) return r; diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 168fa9cda5..95ee7efa92 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -181,7 +181,7 @@ static int wait_for_partitions_to_appear( continue; if (!FLAGS_SET(flags, DISSECT_IMAGE_NO_UDEV)) { - r = device_wait_for_initialization(q, "block", NULL); + r = device_wait_for_initialization(q, "block", USEC_INFINITY, NULL); if (r < 0) return r; } @@ -251,7 +251,7 @@ static int loop_wait_for_partitions_to_appear( log_debug("Waiting for device (parent + %d partitions) to appear...", num_partitions); if (!FLAGS_SET(flags, DISSECT_IMAGE_NO_UDEV)) { - r = device_wait_for_initialization(d, "block", &device); + r = device_wait_for_initialization(d, "block", USEC_INFINITY, &device); if (r < 0) return r; } else diff --git a/src/shared/udev-util.c b/src/shared/udev-util.c index 054feeda76..482d914d07 100644 --- a/src/shared/udev-util.c +++ b/src/shared/udev-util.c @@ -3,6 +3,7 @@ #include #include "alloc-util.h" +#include "device-util.h" #include "env-file.h" #include "log.h" #include "parse-util.h" @@ -112,14 +113,18 @@ static int device_monitor_handler(sd_device_monitor *monitor, sd_device *device, return 0; } -int device_wait_for_initialization(sd_device *device, const char *subsystem, sd_device **ret) { +static int device_timeout_handler(sd_event_source *s, uint64_t usec, void *userdata) { + return sd_event_exit(sd_event_source_get_event(s), -ETIMEDOUT); +} + +int device_wait_for_initialization(sd_device *device, const char *subsystem, usec_t timeout, sd_device **ret) { _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *timeout_source = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; struct DeviceMonitorData data = {}; int r; assert(device); - assert(subsystem); if (sd_device_get_is_initialized(device) > 0) { if (ret) @@ -139,9 +144,17 @@ int device_wait_for_initialization(sd_device *device, const char *subsystem, sd_ if (r < 0) return log_error_errno(r, "Failed to acquire monitor: %m"); - r = sd_device_monitor_filter_add_match_subsystem_devtype(monitor, subsystem, NULL); - if (r < 0) - return log_error_errno(r, "Failed to add %s subsystem match to monitor: %m", subsystem); + if (!subsystem) { + r = sd_device_get_subsystem(device, &subsystem); + if (r < 0 && r != -ENOENT) + return log_device_error_errno(device, r, "Failed to get subsystem: %m"); + } + + if (subsystem) { + r = sd_device_monitor_filter_add_match_subsystem_devtype(monitor, subsystem, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add %s subsystem match to monitor: %m", subsystem); + } r = sd_device_monitor_attach_event(monitor, event); if (r < 0) @@ -151,6 +164,14 @@ int device_wait_for_initialization(sd_device *device, const char *subsystem, sd_ if (r < 0) return log_error_errno(r, "Failed to start device monitor: %m"); + if (timeout != USEC_INFINITY) { + r = sd_event_add_time(event, &timeout_source, + CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + timeout, 0, + device_timeout_handler, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add timeout event source: %m"); + } + /* Check again, maybe things changed. Udev will re-read the db if the device wasn't initialized * yet. */ if (sd_device_get_is_initialized(device) > 0) { @@ -161,7 +182,7 @@ int device_wait_for_initialization(sd_device *device, const char *subsystem, sd_ r = sd_event_loop(event); if (r < 0) - return log_error_errno(r, "Event loop failed: %m"); + return log_error_errno(r, "Failed to wait for device to be initialized: %m"); if (ret) *ret = TAKE_PTR(data.device); diff --git a/src/shared/udev-util.h b/src/shared/udev-util.h index 3c45447515..f62c4c56e3 100644 --- a/src/shared/udev-util.h +++ b/src/shared/udev-util.h @@ -27,6 +27,6 @@ static inline int udev_parse_config(void) { return udev_parse_config_full(NULL, NULL, NULL, NULL); } -int device_wait_for_initialization(sd_device *device, const char *subsystem, sd_device **ret); +int device_wait_for_initialization(sd_device *device, const char *subsystem, usec_t timeout, sd_device **ret); int device_is_renaming(sd_device *dev); bool device_for_action(sd_device *dev, DeviceAction action); diff --git a/src/udev/udevadm-info.c b/src/udev/udevadm-info.c index 6742cda8f6..9078a3c2ce 100644 --- a/src/udev/udevadm-info.c +++ b/src/udev/udevadm-info.c @@ -20,6 +20,7 @@ #include "fd-util.h" #include "string-table.h" #include "string-util.h" +#include "udev-util.h" #include "udevadm-util.h" #include "udevadm.h" @@ -40,6 +41,7 @@ typedef enum QueryType { static bool arg_root = false; static bool arg_export = false; static const char *arg_export_prefix = NULL; +static usec_t arg_wait_for_initialization_timeout = 0; static bool skip_attribute(const char *name) { static const char* const skip[] = { @@ -329,6 +331,8 @@ static int help(void) { " -P --export-prefix Export the key name with a prefix\n" " -e --export-db Export the content of the udev database\n" " -c --cleanup-db Clean up the udev database\n" + " -w --wait-for-initialization[=SECONDS]\n" + " Wait for device to be initialized\n" , program_invocation_short_name); return 0; @@ -340,25 +344,26 @@ int info_main(int argc, char *argv[], void *userdata) { int c, r; static const struct option options[] = { - { "name", required_argument, NULL, 'n' }, - { "path", required_argument, NULL, 'p' }, - { "query", required_argument, NULL, 'q' }, - { "attribute-walk", no_argument, NULL, 'a' }, - { "cleanup-db", no_argument, NULL, 'c' }, - { "export-db", no_argument, NULL, 'e' }, - { "root", no_argument, NULL, 'r' }, - { "device-id-of-file", required_argument, NULL, 'd' }, - { "export", no_argument, NULL, 'x' }, - { "export-prefix", required_argument, NULL, 'P' }, - { "version", no_argument, NULL, 'V' }, - { "help", no_argument, NULL, 'h' }, + { "name", required_argument, NULL, 'n' }, + { "path", required_argument, NULL, 'p' }, + { "query", required_argument, NULL, 'q' }, + { "attribute-walk", no_argument, NULL, 'a' }, + { "cleanup-db", no_argument, NULL, 'c' }, + { "export-db", no_argument, NULL, 'e' }, + { "root", no_argument, NULL, 'r' }, + { "device-id-of-file", required_argument, NULL, 'd' }, + { "export", no_argument, NULL, 'x' }, + { "export-prefix", required_argument, NULL, 'P' }, + { "wait-for-initialization", optional_argument, NULL, 'w' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, {} }; ActionType action = ACTION_QUERY; QueryType query = QUERY_ALL; - while ((c = getopt_long(argc, argv, "aced:n:p:q:rxP:Vh", options, NULL)) >= 0) + while ((c = getopt_long(argc, argv, "aced:n:p:q:rxP:w::Vh", options, NULL)) >= 0) switch (c) { case 'n': case 'p': { @@ -414,6 +419,14 @@ int info_main(int argc, char *argv[], void *userdata) { arg_export = true; arg_export_prefix = optarg; break; + case 'w': + if (optarg) { + r = parse_sec(optarg, &arg_wait_for_initialization_timeout); + if (r < 0) + return log_error_errno(r, "Failed to parse timeout value: %m"); + } else + arg_wait_for_initialization_timeout = USEC_INFINITY; + break; case 'V': return print_version(); case 'h': @@ -453,6 +466,12 @@ int info_main(int argc, char *argv[], void *userdata) { if (r < 0) return log_error_errno(r, "Unknown device \"%s\": %m", *p); + if (arg_wait_for_initialization_timeout > 0) { + r = device_wait_for_initialization(device, NULL, arg_wait_for_initialization_timeout, NULL); + if (r < 0) + return r; + } + if (action == ACTION_QUERY) r = query_device(query, device); else if (action == ACTION_ATTRIBUTE_WALK) diff --git a/test/test-network/conf/11-dummy-mtu.netdev b/test/test-network/conf/11-dummy-mtu.netdev new file mode 100644 index 0000000000..aeecc1dc01 --- /dev/null +++ b/test/test-network/conf/11-dummy-mtu.netdev @@ -0,0 +1,4 @@ +[NetDev] +Name=test1 +Kind=dummy +MTUBytes=1600 diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 335e3215f2..d9242cb116 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -79,6 +79,23 @@ def expectedFailureIfRoutingPolicyIPProtoIsNotAvailable(): return f +def expectedFailureIfEthtoolDoesNotSupportDriver(): + def f(func): + support = False + rc = subprocess.call(['ip', 'link', 'add', 'name', 'dummy99', 'type', 'dummy']) + if rc == 0: + ret = subprocess.run(['udevadm', 'info', '-w10s', '/sys/class/net/dummy99'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) + if ret.returncode == 0 and 'E: ID_NET_DRIVER=dummy' in ret.stdout.rstrip(): + support = True + subprocess.call(['ip', 'link', 'del', 'dummy99']) + + if support: + return func + else: + return unittest.expectedFailure(func) + + return f + def setUpModule(): os.makedirs(network_unit_file_path, exist_ok=True) os.makedirs(networkd_ci_path, exist_ok=True) @@ -109,6 +126,8 @@ def setUpModule(): drop_in += ['Environment=LSAN_OPTIONS="' + lsan_options + '"'] if ubsan_options: drop_in += ['Environment=UBSAN_OPTIONS="' + ubsan_options + '"'] + if asan_options or lsan_options or ubsan_options: + drop_in += ['SystemCallFilter='] if use_valgrind or asan_options or lsan_options or ubsan_options: drop_in += ['MemoryDenyWriteExecute=no'] @@ -283,6 +302,91 @@ class Utilities(): else: self.assertRegex(output, address_regex) +class NetworkctlTests(unittest.TestCase, Utilities): + + links = [ + 'test1', + 'veth99', + ] + + units = [ + '11-dummy.netdev', + '11-dummy-mtu.netdev', + '11-dummy.network', + '25-veth.netdev', + 'netdev-link-local-addressing-yes.network', + ] + + def setUp(self): + self.remove_links(self.links) + + def tearDown(self): + self.remove_links(self.links) + self.remove_unit_from_networkd_path(self.units) + + def test_glob(self): + self.copy_unit_to_networkd_unit_path('11-dummy.netdev', '11-dummy.network') + self.start_networkd(0) + + self.wait_online(['test1:degraded']) + + output = subprocess.check_output(networkctl_cmd + ['list'], universal_newlines=True, env=env).rstrip() + self.assertRegex(output, '1 lo ') + self.assertRegex(output, 'test1') + + output = subprocess.check_output(networkctl_cmd + ['list', 'test1'], universal_newlines=True, env=env).rstrip() + self.assertNotRegex(output, '1 lo ') + self.assertRegex(output, 'test1') + + output = subprocess.check_output(networkctl_cmd + ['list', 'te*'], universal_newlines=True, env=env).rstrip() + self.assertNotRegex(output, '1 lo ') + self.assertRegex(output, 'test1') + + output = subprocess.check_output(networkctl_cmd + ['status', 'te*'], universal_newlines=True, env=env).rstrip() + self.assertNotRegex(output, '1: lo ') + self.assertRegex(output, 'test1') + + output = subprocess.check_output(networkctl_cmd + ['status', 'tes[a-z][0-9]'], universal_newlines=True, env=env).rstrip() + self.assertNotRegex(output, '1: lo ') + self.assertRegex(output, 'test1') + + def test_mtu(self): + self.copy_unit_to_networkd_unit_path('11-dummy-mtu.netdev', '11-dummy.network') + self.start_networkd(0) + + self.wait_online(['test1:degraded']) + + output = subprocess.check_output(networkctl_cmd + ['status', 'test1'], universal_newlines=True, env=env).rstrip() + self.assertRegex(output, 'MTU: 1600') + + @expectedFailureIfEthtoolDoesNotSupportDriver() + def test_udev_driver(self): + self.copy_unit_to_networkd_unit_path('11-dummy.netdev', '11-dummy.network', + '25-veth.netdev', 'netdev-link-local-addressing-yes.network') + self.start_networkd(0) + + self.wait_online(['test1:degraded', 'veth99:degraded', 'veth-peer:degraded']) + + output = subprocess.check_output(networkctl_cmd + ['status', 'test1'], universal_newlines=True, env=env).rstrip() + self.assertRegex(output, 'Driver: dummy') + + output = subprocess.check_output(networkctl_cmd + ['status', 'veth99'], universal_newlines=True, env=env).rstrip() + self.assertRegex(output, 'Driver: veth') + + output = subprocess.check_output(networkctl_cmd + ['status', 'veth-peer'], universal_newlines=True, env=env).rstrip() + self.assertRegex(output, 'Driver: veth') + + def test_delete_links(self): + self.copy_unit_to_networkd_unit_path('11-dummy.netdev', '11-dummy.network', + '25-veth.netdev', 'netdev-link-local-addressing-yes.network') + self.start_networkd(0) + + self.wait_online(['test1:degraded', 'veth99:degraded', 'veth-peer:degraded']) + + subprocess.check_call(networkctl_cmd + ['delete', 'test1', 'veth99']) + self.assertFalse(self.link_exists('test1')) + self.assertFalse(self.link_exists('veth99')) + self.assertFalse(self.link_exists('veth-peer')) class NetworkdNetDevTests(unittest.TestCase, Utilities): @@ -451,41 +555,16 @@ class NetworkdNetDevTests(unittest.TestCase, Utilities): self.remove_links(self.links) self.remove_unit_from_networkd_path(self.units) - def test_dropin_and_networkctl_glob(self): + def test_dropin_and_name_conflict(self): self.copy_unit_to_networkd_unit_path('10-dropin-test.netdev', '15-name-conflict-test.netdev') self.start_networkd(0) self.wait_online(['dropin-test:off']) - # This also tests NetDev.Name= conflict and basic networkctl functionalities - output = subprocess.check_output(['ip', 'link', 'show', 'dropin-test'], universal_newlines=True).rstrip() print(output) self.assertRegex(output, '00:50:56:c0:00:28') - output = subprocess.check_output(networkctl_cmd + ['list'], universal_newlines=True, env=env).rstrip() - self.assertRegex(output, '1 lo ') - self.assertRegex(output, 'dropin-test') - - output = subprocess.check_output(networkctl_cmd + ['list', 'dropin-test'], universal_newlines=True, env=env).rstrip() - self.assertNotRegex(output, '1 lo ') - self.assertRegex(output, 'dropin-test') - - output = subprocess.check_output(networkctl_cmd + ['list', 'dropin-*'], universal_newlines=True, env=env).rstrip() - self.assertNotRegex(output, '1 lo ') - self.assertRegex(output, 'dropin-test') - - output = subprocess.check_output(networkctl_cmd + ['status', 'dropin-*'], universal_newlines=True, env=env).rstrip() - self.assertNotRegex(output, '1: lo ') - self.assertRegex(output, 'dropin-test') - - ret = subprocess.run(['ethtool', '--driver', 'dropin-test'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) - print(ret.stdout.rstrip()) - if ret.returncode == 0 and re.search('driver: dummy', ret.stdout.rstrip()) != None: - self.assertRegex(output, 'Driver: dummy') - else: - print('ethtool does not support driver field at least for dummy interfaces, skipping test for Driver field of networkctl.') - def test_wait_online_any(self): self.copy_unit_to_networkd_unit_path('25-bridge.netdev', '25-bridge.network', '11-dummy.netdev', '11-dummy.network') self.start_networkd(0)