diff options
| author | Christian Breunig <christian@breunig.cc> | 2026-10-07 19:57:17 +0200 |
|---|---|---|
| committer | Christian Breunig <christian@breunig.cc> | 2026-10-31 09:58:29 +0000 |
| commit | afd4574df20a1cf70607b5e7644773ce336ea095 (patch) | |
| tree | b48bfc4d8a158e90a49281b5d80746da18e54bb8 /python | |
| parent | 5a70decaaccd0c72871543c8c31fc9a501486b69 (diff) | |
| download | vyos-1x-afd4574df20a1cf70607b5e7644773ce336ea095.tar.gz vyos-1x-afd4574df20a1cf70607b5e7644773ce336ea095.zip | |
interfaces: T3871: keep the hypervisor's adapter order on ESXi
ESXi does not lay its adapters out in PCI address order. With four or more it
puts the fourth on a root port sorting ahead of the first three, so naming by
address made Network adapter 4 eth0 and shifted the rest. On the vmware-vsphere
OVA that moved the WAN, because cloud-init takes eth0 from the OVF properties.
The firmware port label carries the adapter order, and is what the VMware udev
rule this series removed matched on. Order by it, ahead of the address, for
hardware the hypervisor presented - recognised by its PCI vendor, or by DMI for
a model whose IDs are not known here. A physical server labels its ports too
and must not be reordered.
The label orders, it never identifies: a relabelled adapter is still the same
card.
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/ifconfig/ifname_store.py | 60 |
1 files changed, 57 insertions, 3 deletions
diff --git a/python/vyos/ifconfig/ifname_store.py b/python/vyos/ifconfig/ifname_store.py index b0ee3df35..e0a6b4cbe 100644 --- a/python/vyos/ifconfig/ifname_store.py +++ b/python/vyos/ifconfig/ifname_store.py @@ -54,6 +54,13 @@ _BUS_RANK = {'pci': 0, 'vmbus': 1, 'xen': 2, 'usb': 3} _BUS_RANK_UNKNOWN = 90 _PCI_BDF = re.compile(r'([0-9a-f]{4}):([0-9a-f]{2}):([0-9a-f]{2})\.([0-9a-f]+)', re.I) +# ESXi labels its adapters Ethernet0..EthernetN, in "Network adapter N" order +_PORT_LABEL = re.compile(r'^Ethernet(\d+)$') +# sorts after every real label, and must stay an int - canonical_sort_key() +# compares tuples element-wise, so a None here would raise against an int +_LABEL_UNKNOWN = 1 << 30 +_VMWARE_PCI_VENDOR = '0x15ad' +_DMI_SYS_VENDOR = Path('/sys/class/dmi/id/sys_vendor') _MAC_RE = re.compile(r'([0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5})') @@ -91,18 +98,62 @@ def device_matches(properties: dict, key: str) -> bool: def canonical_sort_key(device: dict) -> tuple: - """Total order by bus class, physical position then MAC, so it never - depends on probe order. Only used for hardware not yet in the store. + """Total order by bus class, the hypervisor's adapter order where it says + so, then physical position and MAC, so it never depends on probe order. + Only used for hardware not yet in the store. """ properties = device.get('properties') or {} path = properties.get('ID_PATH', '') rank = _BUS_RANK.get(properties.get('ID_BUS', ''), _BUS_RANK_UNKNOWN) + # a labelled adapter outranks the PCI address, which on ESXi does not + # follow the order the adapters were added in. Unlabelled hardware sorts + # after it: a passthrough NIC is not part of the hypervisor's own set and + # must not take the first name from it. + label = device.get('label_index', _LABEL_UNKNOWN) + # numeric, so 0000:09:00.0 sorts before 0000:10:00.0 segments = [tuple(int(part, 16) for part in m.groups()) for m in _PCI_BDF.finditer(path)] - return (rank, segments, path, device.get('mac') or '') + return (rank, label, segments, path, device.get('mac') or '') + + +def _sysfs(path: Path) -> str: + try: + return path.read_text().strip() + except OSError: + return '' + + +def is_vmware_nic(ifname: str, sys_class_net: str = '/sys/class/net') -> bool: + """Whether a NIC is one ESXi presents - vmxnet3 carries VMware's PCI + vendor, an emulated e1000 carries it as the subsystem vendor. + + The DMI fallback is not redundant: smbios.reflectHost makes a guest report + the physical host's vendor, and a NIC model whose IDs are not listed here + would otherwise go unrecognised. + """ + device = Path(sys_class_net) / ifname / 'device' + if _VMWARE_PCI_VENDOR in (_sysfs(device / 'vendor'), + _sysfs(device / 'subsystem_vendor')): + return True + return _sysfs(_DMI_SYS_VENDOR) == 'VMware, Inc.' + + +def port_label_index(ifname: str, sys_class_net: str = '/sys/class/net') -> int: + """The hypervisor's adapter number, from the firmware port label. + + ESXi does not lay its adapters out in PCI address order - with four or more + it places the fourth on a root port which sorts ahead of the first three - + so the label is the only thing that gives their intended order. + + Returns _LABEL_UNKNOWN when there is no such label. Physical servers label + ports too, e.g. "NIC1" or "Embedded LOM 1 Port 1", and must not be reordered + by this. + """ + m = _PORT_LABEL.match(_sysfs(Path(sys_class_net) / ifname / 'device' / 'label')) + return int(m.group(1)) if m else _LABEL_UNKNOWN def permanent_mac(ifname: str, sys_class_net: str = '/sys/class/net') -> str: @@ -200,6 +251,9 @@ def discover_devices(sys_class_net: str = '/sys/class/net') -> list: 'mac': mac, 'wireless': is_wireless(entry.name, sys_class_net), 'wwan': is_wwan(entry.name, sys_class_net), + 'label_index': (port_label_index(entry.name, sys_class_net) + if is_vmware_nic(entry.name, sys_class_net) + else _LABEL_UNKNOWN), 'properties': read_properties(entry.name), }) |
