summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-09-09 19:29:06 +0000
committerChristian Breunig <christian@breunig.cc>2026-09-09 19:29:06 +0000
commitfdc0c4f48e6839142db30c2fb9c6d5c84b9eba90 (patch)
tree6cfd430a511fb3f680956fde6973aaf29059202b /src
parentabf7613f8d34d7ed7bc59e7883d584625a4dd65c (diff)
downloadvyos-1x-fdc0c4f48e6839142db30c2fb9c6d5c84b9eba90.tar.gz
vyos-1x-fdc0c4f48e6839142db30c2fb9c6d5c84b9eba90.zip
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.
Diffstat (limited to 'src')
-rwxr-xr-xsrc/system/vyos-net-name-resolve.py9
-rw-r--r--src/tests/test_net_name_resolve.py57
2 files changed, 66 insertions, 0 deletions
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