From fdc0c4f48e6839142db30c2fb9c6d5c84b9eba90 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Wed, 9 Sep 2026 19:29:06 +0000 Subject: interfaces: T9269: do not run the hw-id naming pass in a container Container interfaces are veth pairs created by the runtime. They have no backing bus device in sysfs, so discover_physical_interfaces() never considers them, and their MAC is assigned by the host and regenerated on every start. A pending node could thus never be resolved. The pass only spent both bounded hardware waits and then asked for an hw-id that would not survive a restart: interface 'eth0' still has no hw-id configured after this boot's naming pass - bind it manually, e.g. ... Returning before write_status() leaves no status file behind, so warn_missing_interface_hardware() in vyos-router stays quiet by itself. setUpModule() pins the detection off for the test module - the build container would otherwise short-circuit every existing main() test. --- src/system/vyos-net-name-resolve.py | 9 ++++++ src/tests/test_net_name_resolve.py | 57 +++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) (limited to 'src') diff --git a/src/system/vyos-net-name-resolve.py b/src/system/vyos-net-name-resolve.py index 5b1f36533..e28f3b3c4 100755 --- a/src/system/vyos-net-name-resolve.py +++ b/src/system/vyos-net-name-resolve.py @@ -41,6 +41,7 @@ from sys import exit from vyos.configtree import ConfigTree from vyos.defaults import directories from vyos.migrate import ConfigMigrate +from vyos.system.image import is_running_as_container from vyos.utils.process import rc_cmd from vyos.utils.process import run @@ -613,6 +614,14 @@ def write_status(configured: dict, found: dict, missing: set, plan: dict, def main(): + if is_running_as_container(): + # A container has no NIC of its own - its interfaces are veth pairs + # created by the container runtime. They have no backing bus device in + # sysfs and their MAC is assigned by the host and regenerated on every + # start, so there is nothing to wait for and no hw-id worth binding. + logger.info('running inside a container - skipping hw-id naming pass') + return + configured = get_configfile_interfaces() pending = get_pending_hwid_nodes() diff --git a/src/tests/test_net_name_resolve.py b/src/tests/test_net_name_resolve.py index 26d2e174e..e1530eba7 100644 --- a/src/tests/test_net_name_resolve.py +++ b/src/tests/test_net_name_resolve.py @@ -38,6 +38,23 @@ vyos_net_name = prepare_module( os.path.join(_here, '../udev/vyos_net_name'), 'vyos_net_name') +_container_patcher = None + + +def setUpModule(): + # main() deliberately short-circuits inside a container, and these tests + # are routinely run from one (the VyOS build container). Pin the detection + # off for the whole module so the naming pass under test actually runs - + # TestMainInContainer patches it back on locally where that is the point. + global _container_patcher + _container_patcher = mock.patch.object( + resolver, 'is_running_as_container', return_value=False) + _container_patcher.start() + + +def tearDownModule(): + _container_patcher.stop() + class TestGetPendingHwidNodes(unittest.TestCase): """A node that exists under interfaces/{ethernet,wireless} but has no @@ -1509,6 +1526,46 @@ class TestMainFirstBootBootstrap(unittest.TestCase): self.assertEqual(status['reclaimed'], {}) +class TestMainInContainer(unittest.TestCase): + """A container owns no NIC: its interfaces are runtime-created veth + pairs with no backing bus device in sysfs and a host-assigned MAC that + changes on every start. The naming pass can therefore never resolve a + pending node there - it only spent both bounded hardware waits and then + told the user to bind an hw-id that would not survive a restart. + """ + + def setUp(self): + self.udev_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.udev_dir, ignore_errors=True) + self._orig_udev_dir = resolver.vyos_udev_dir + resolver.vyos_udev_dir = self.udev_dir + self.addCleanup(setattr, resolver, 'vyos_udev_dir', self._orig_udev_dir) + + status_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, status_dir, ignore_errors=True) + self._orig_status_file = resolver.status_file + resolver.status_file = resolver.Path(status_dir) / 'status.json' + self.addCleanup(setattr, resolver, 'status_file', self._orig_status_file) + + def test_naming_pass_is_skipped_entirely(self): + with mock.patch.object(resolver, 'is_running_as_container', + return_value=True), \ + mock.patch.object(resolver, 'get_configfile_interfaces') as configured, \ + mock.patch.object(resolver, 'get_pending_hwid_nodes') as pending, \ + mock.patch.object(resolver, 'discover_physical_interfaces') as discover, \ + mock.patch.object(resolver, 'run') as run: + resolver.main() + + configured.assert_not_called() + pending.assert_not_called() + discover.assert_not_called() + run.assert_not_called() + # no status file means vyos-router's warn_missing_interface_hardware() + # stays silent instead of warning about an unbindable pending node + self.assertFalse(resolver.status_file.exists()) + self.assertEqual(os.listdir(self.udev_dir), []) + + class TestWriteStatus(unittest.TestCase): """A pending node that couldn't be safely auto-matched must be reported so vyos-router can surface a boot-time warning - it must -- cgit v1.2.3