From 153dd688b327c922ec4b371db1fdd11b8ee8cf9e Mon Sep 17 00:00:00 2001 From: Alex Kudentsov <43482574+alexk37@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:46:08 +0700 Subject: interfaces: T3871: give a re-created Hyper-V NIC the name of the NIC it replaced A Hyper-V NIC is known by its MAC alone. One deleted and created again on the same VM - an Azure NIC re-created, or a Hyper-V adapter removed and added - comes back with a new MAC, so nothing on it matched the store: its name stayed reserved, the new NIC got a new name, and its configuration was left on a name with no NIC. cloud-init does not run again on the same instance, so nothing else repairs it. At boot, a vacant name now goes to a new NIC only when the pairing is not in doubt: - exactly one recorded name has no NIC, and exactly one new Hyper-V NIC appeared. With more of either, the order the NICs come up in does not say which replaced which, so nothing is handed over and the names stay reserved, as before; - the name went missing at this pass. The store records which names were missing at the last pass, so a NIC removed for good does not hand its name to a NIC added at a later boot; - the name's own NIC was a Hyper-V one. The store records which names sat on a Hyper-V NIC; a MAC-keyed name can belong to other hardware, and a store written before this was recorded hands nothing over (the boot pass logs that when it holds a name back); - only at boot. On a running system the configuration under a name is not applied to the NIC that takes it, so the hotplug unit passes --hotplug, which names the new NIC like any other and records the vacant name as missing, so the next boot does not hand it over either. An adoption is logged as a warning, reported under 'adopted' in the status file (kept there across hotplug passes while it holds), and repeated by vyos-router, as a slot match is: verify the port before relying on its configuration. --- src/system/vyos-net-name-resolve.py | 56 ++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 7 deletions(-) (limited to 'src/system') diff --git a/src/system/vyos-net-name-resolve.py b/src/system/vyos-net-name-resolve.py index d74663707..f3e4e62fb 100755 --- a/src/system/vyos-net-name-resolve.py +++ b/src/system/vyos-net-name-resolve.py @@ -26,6 +26,7 @@ import json import logging import logging.handlers +import sys import time from pathlib import Path @@ -182,16 +183,21 @@ def write_status(store: dict, devices: list, applied: dict, report: dict, # in use, so left alone until the next boot 'rename_refused': refused or {}, # everything recognised from the store, however it was recognised - 'matched': {**report.get('matched', {}), - **report.get('reassigned', {}), - **report.get('moved', {}), - **report.get('replaced', {})}, + 'matched': { + **report.get('matched', {}), + **report.get('reassigned', {}), + **report.get('moved', {}), + **report.get('replaced', {}), + **report.get('adopted', {}), + }, # the name changed hands between cards the store already knows 'reassigned': report.get('reassigned', {}), # the card was found in a different slot - certain, it is the same card 'moved': report.get('moved', {}), # a different card took the slot - the one case that needs verifying 'matched_by_path': report.get('replaced', {}), + # a recorded Hyper-V NIC was gone and its one replacement took its name + 'adopted': report.get('adopted', {}), 'new_hardware': report.get('bootstrapped', {}), 'persisted': persisted, } @@ -201,7 +207,25 @@ def write_status(store: dict, devices: list, applied: dict, report: dict, logger.error(f'could not write status file: {e}') -def main(): +def boot_adoptions(store: dict) -> dict: + """The adoptions the boot pass reported that still hold. A pass for + hot-plugged hardware adopts nothing, and must not wipe the record of + what this boot did.""" + try: + adopted = json.loads(status_file.read_text()).get('adopted') or {} + except (OSError, ValueError, AttributeError): + return {} + hardware = store.get('hardware', {}) + return {name: mac for name, mac in adopted.items() + if hardware.get(name) == mac} + + +def hotplug_requested(argv: list) -> bool: + """--hotplug: started for hardware added to a running system""" + return '--hotplug' in argv + + +def main(hotplug: bool = False): if is_running_as_container(): # a container owns no NIC - its veth pairs have no backing bus device logger.info('running inside a container - skipping interface naming') @@ -229,7 +253,16 @@ def main(): 'reserved.' ) - plan, store, report = resolve(devices, store) + if not hotplug and 'hyperv' not in store and any( + key.startswith('ID_NET_NAME_MAC=') and key in absent + for key in store['interfaces'].values()): + # a name is vacant, and the map was written by code that did not + # record which NICs were Hyper-V ones: the name is not handed over. + # Each pass records the Hyper-V NICs it finds, but never a vacant name + logger.info('the interface map does not say which interfaces were ' + 'Hyper-V NICs - no vacant name is given to a new NIC ' + 'this boot') + plan, store, report = resolve(devices, store, adopt=not hotplug) for name, mac in sorted(report.get('bootstrapped', {}).items()): logger.info(f"new hardware {mac} named '{name}'") @@ -242,6 +275,12 @@ def main(): f"'{name}' was matched to new hardware {mac} by slot - verify it " 'is the port you expect before relying on its configuration' ) + for name, mac in sorted(report.get('adopted', {}).items()): + logger.warning( + f"'{name}' had no NIC this boot and was given to the new Hyper-V " + f'NIC {mac}, which took its place - verify it is the port you ' + 'expect before relying on its configuration' + ) plan, refused = refuse_renames_in_use(plan) for old, target in sorted(refused.items()): logger.warning( @@ -263,6 +302,9 @@ def main(): 'resolved for this boot but could not be persisted' ) + if hotplug: + # reported once, by the boot pass; kept on record until the next boot + report = {**report, 'adopted': boot_adoptions(store)} write_status(store, devices, applied, report, persisted, refused) @@ -274,4 +316,4 @@ if __name__ == '__main__': logger.addHandler(syslog_handler) logger.setLevel(logging.DEBUG) - main() + main(hotplug=hotplug_requested(sys.argv[1:])) -- cgit v1.2.3