summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-10-26 18:45:56 +0100
committerChristian Breunig <christian@breunig.cc>2026-10-26 18:45:56 +0100
commitd8dcf6bc7c98435c8c01150e6db0d565c88c3849 (patch)
tree2957517af34466cc5c506067c10302d42eb377b6
parent83f45e337a4d9393c0baa4aca2b9ade5c05f9bbe (diff)
downloadvyos-cloud-init-d8dcf6bc7c98435c8c01150e6db0d565c88c3849.tar.gz
vyos-cloud-init-d8dcf6bc7c98435c8c01150e6db0d565c88c3849.zip
cloud-init: T3871: record the names the cloud gave its NICs
Interface names are no longer derived from a MAC held in the configuration, so writing one back was producing a node which no longer exists. A configuration carrying it is rejected on load, and migration does not catch it: the file is brought to the current version before it is modified, so nothing runs over it afterwards. Which NIC the cloud considers the first one is not visible in the hardware and only the datasource knows it, so that order still has to be recorded - now in the mapping store, against the slot each card sits in, before the naming pass reads it. Hardware the datasource describes but which is not present is skipped, as is a name or a slot already spoken for.
-rw-r--r--cloudinit/config/cc_vyos.py78
1 files changed, 63 insertions, 15 deletions
diff --git a/cloudinit/config/cc_vyos.py b/cloudinit/config/cc_vyos.py
index 56014794..1e265c4d 100644
--- a/cloudinit/config/cc_vyos.py
+++ b/cloudinit/config/cc_vyos.py
@@ -36,6 +36,9 @@ from cloudinit.util import load_file, load_json, get_hostname_fqdn, get_cfg_by_p
from cloudinit.sources.DataSourceOVF import get_properties as ovf_get_properties
try:
from vyos.configtree import ConfigTree
+ from vyos.ifconfig.ifname_store import discover_devices
+ from vyos.ifconfig.ifname_store import load_store
+ from vyos.ifconfig.ifname_store import save_store
except ImportError as err:
print(f'The module cannot be imported: {err}')
@@ -466,12 +469,6 @@ def set_config_interfaces_v1(config, iface_config: dict):
if iface_config['type'] == 'physical':
iface_name = iface_config['name']
- # configre MAC
- if 'mac_address' in iface_config:
- logger.debug("Setting MAC for {}: {}".format(iface_name, iface_config['mac_address']))
- config.set(['interfaces', 'ethernet', iface_name, 'hw-id'], value=iface_config['mac_address'], replace=True)
- config.set_tag(['interfaces', 'ethernet'])
-
# configre MTU
if 'mtu' in iface_config and iface_config['mtu'] is not None:
set_ether_mtu(config, iface_name, iface_config['mtu'])
@@ -955,15 +952,6 @@ def config_net_v2_ethernet(config, iface_name: str,
iface_config: dict) -> None:
config_net_v2_common(config, 'ethernet', iface_name, iface_config)
- # configure MAC
- if 'match' in iface_config and 'macaddress' in iface_config['match']:
- logger.debug("Setting MAC for {}: {}".format(
- iface_name, iface_config['match']['macaddress']))
- config.set(['interfaces', 'ethernet', iface_name, 'hw-id'],
- value=iface_config['match']['macaddress'],
- replace=True)
- config.set_tag(['interfaces', 'ethernet'])
-
# configure interface from networking config version 2
def set_config_interfaces_v2(config, netcfg: dict) -> None:
@@ -979,6 +967,64 @@ def set_config_interfaces_v2(config, netcfg: dict) -> None:
config_net_v2_vlan(config, iface_name, iface_config)
+# record the names the cloud gave its NICs
+def record_interface_mapping(netcfg: dict) -> None:
+ """T3871: a name is anchored to the slot a card sits in, recorded in the
+ mapping store rather than in the configuration. Which NIC is the first one
+ is the cloud's to decide and is not visible in the hardware, so the order
+ the datasource gives has to reach the store before the naming pass reads
+ it.
+ """
+ bindings = {}
+ if netcfg.get('version') == 1:
+ for iface_config in netcfg.get('config', []):
+ if iface_config.get('type') != 'physical':
+ continue
+ if iface_config.get('name') and iface_config.get('mac_address'):
+ bindings[iface_config['name']] = \
+ iface_config['mac_address'].lower()
+ elif netcfg.get('version') == 2:
+ for iface_name, iface_config in netcfg.get('ethernets', {}).items():
+ mac = iface_config.get('match', {}).get('macaddress')
+ if mac:
+ bindings[iface_name] = mac.lower()
+
+ if not bindings:
+ return
+
+ try:
+ devices = {device['mac'].lower(): device
+ for device in discover_devices()}
+ store = load_store()
+ except Exception as err:
+ logger.error("Failed to read the interface mapping: {}".format(err))
+ return
+
+ entries = store['interfaces']
+ taken = set(entries.values())
+ added = False
+
+ for iface_name, mac in bindings.items():
+ device = devices.get(mac)
+ # a NIC the datasource describes but which is not here has nothing to
+ # anchor a name to
+ if device is None or not device['key']:
+ logger.debug("No hardware for {} ({})".format(iface_name, mac))
+ continue
+ # an entry already in the store is authoritative, and a slot already
+ # spoken for must not gain a second name
+ if iface_name in entries or device['key'] in taken:
+ continue
+ entries[iface_name] = device['key']
+ store['hardware'][iface_name] = mac
+ taken.add(device['key'])
+ added = True
+ logger.debug("Mapping {} to {}".format(iface_name, device['key']))
+
+ if added and not save_store(store):
+ logger.error("Failed to save the interface mapping")
+
+
# configure SSH server service
def set_config_ssh(config):
logger.debug("Configuring SSH service")
@@ -1131,6 +1177,8 @@ def handle(name, cfg, cloud, log, _args):
# process networking configuration data
if netcfg and network_configured is False and network_config_vyos != 'disabled':
+ record_interface_mapping(netcfg)
+
# check which one version of config we have
# version 1
if netcfg['version'] == 1: