From 2da9e509afdcbb365646bb8ce12bed61f17ea125 Mon Sep 17 00:00:00 2001 From: Thomas Foks Date: Mon, 12 Oct 2026 09:50:37 +0200 Subject: [PATCH 1/2] fix(VMware): keep a faster physical host NIC type already in NetBox The VMware source derives the type of a host pNIC from its current link speed (or the first valid link speed without link). With a check_redfish source on the same hosts, which sets the type from the NIC hardware, both sources overwrote each other on every run, i.e. 10gbase-x-sfpp <> 1000base-t for an SFP+ port running at 1G. Keep the current type of an existing host interface if it is a physical type which supports the speed vCenter reports. A slower type is still updated. Co-Authored-By: Claude Opus 5.5 --- module/sources/vmware/connection.py | 15 ++++++ tests/test_vmware_host_nic_type.py | 82 +++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 tests/test_vmware_host_nic_type.py diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index 6c5d255..e7dc12a 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -1894,6 +1894,21 @@ def add_device_vm_to_inventory(self, object_type, object_data, pnic_data=None, v if object_type == NBVM and self.settings.overwrite_vm_interface_name is False: del int_data["name"] + # the pNIC type is only derived from the current link speed. Keep a physical type + # which supports this speed (i.e. an SFP+ port set by redfish running at 1G or + # without link), otherwise both sources overwrite each other on every run. + if object_type == NBDevice: + valid_types = NetBoxInterfaceType.valid_types + current_type = grab(nic_object_dict.get(int_name), "data.type") + # NetBox returns choice fields as {"value": ..., "label": ...} + if isinstance(current_type, dict): + current_type = current_type.get("value") + current_speed = valid_types.get(current_type) + new_speed = valid_types.get(int_data.get("type")) + if isinstance(current_speed, int) and isinstance(new_speed, int) and \ + current_speed >= new_speed: + int_data.pop("type", None) + # add/update interface with retrieved data nic_object, ip_address_objects = self.add_update_interface(nic_object_dict.get(int_name), device_vm_object, int_data, nic_ips.get(int_name, list()), diff --git a/tests/test_vmware_host_nic_type.py b/tests/test_vmware_host_nic_type.py new file mode 100644 index 0000000..8293c65 --- /dev/null +++ b/tests/test_vmware_host_nic_type.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 - 2026 netbox-sync team. All rights reserved. +# +# netbox-sync.py +# +# This work is licensed under the terms of the MIT license. +# For a copy, see file LICENSE.txt included in this +# repository or visit: . + +""" +vCenter only knows the current link speed of a host pNIC, so the interface type it derives +is a guess. A physical type already in NetBox which supports that speed (i.e. an SFP+ port +set by the check_redfish source, running at 1G or without link) must be kept. Otherwise both +sources overwrite each other's type on every run. +""" +import pytest + +from module.netbox.object_classes import NBDevice, NBInterface, NetBoxInterfaceType +from module.sources import instantiate_sources + + +def first_sync(vcsim, inventory, load_config, vmware_settings): + load_config(vmware_settings) + first_run = instantiate_sources()[0] + inventory.resolve_relations() + first_run.apply() + + interfaces = [i for i in inventory.get_all_items(NBInterface) + if isinstance(NetBoxInterfaceType.valid_types.get(i.data.get("type")), int)] + if not interfaces: + pytest.skip(f"vcsim dump '{vcsim.name}' has no physical host interfaces") + + # the second run finds the objects of the first one in NetBox + for nb_id, this_object in enumerate(inventory.get_all_items(NBInterface) + inventory.get_all_items(NBDevice), + start=1): + this_object.nb_id = nb_id + this_object.is_new = False + this_object.updated_items = list() + this_object.unset_items = list() + + return interfaces + + +def second_sync(inventory, load_config, vmware_settings): + inventory.source_list = [] + load_config(vmware_settings) + second_run = instantiate_sources()[0] + inventory.resolve_relations() + second_run.apply() + + +def type_value(interface): + nic_type = interface.data.get("type") + return nic_type.get("value") if isinstance(nic_type, dict) else nic_type + + +def test_faster_physical_type_is_kept(vcsim, inventory, load_config, vmware_settings): + interfaces = first_sync(vcsim, inventory, load_config, vmware_settings) + for interface in interfaces: + # the shape NetBox hands back for a choice field + interface.data["type"] = {"value": "400gbase-x-qsfpdd", "label": "QSFP-DD (400GE)"} + + second_sync(inventory, load_config, vmware_settings) + + for interface in interfaces: + assert type_value(interface) == "400gbase-x-qsfpdd", \ + f"type of '{interface.get_display_name()}' was overwritten from the link speed" + + +def test_slower_physical_type_is_updated(vcsim, inventory, load_config, vmware_settings): + interfaces = first_sync(vcsim, inventory, load_config, vmware_settings) + discovered = {interface: interface.data.get("type") for interface in interfaces} + for interface in interfaces: + interface.data["type"] = {"value": "100base-tx", "label": "100BASE-TX (10/100ME)"} + + second_sync(inventory, load_config, vmware_settings) + + for interface, discovered_type in discovered.items(): + if discovered_type == "100base-tx": + continue + assert type_value(interface) == discovered_type, \ + f"type of '{interface.get_display_name()}' was not updated to the link speed" From a205f2c3814345ffd59006ac74cfb009aaf297f1 Mon Sep 17 00:00:00 2001 From: Thomas Foks Date: Mon, 12 Oct 2026 12:49:38 +0200 Subject: [PATCH 2/2] feat(VMware): add option overwrite_host_interface_type Replace the link speed heuristic of the previous commit with an explicit option, as suggested in the review. The heuristic changed the behaviour for vCenter-only setups (a too fast type was no longer corrected), still overwrote the type of a port without link (derived as 'other') and compared FC types in Gbit with Ethernet types in Mbit. overwrite_host_interface_type defaults to True, the current behaviour. With False the type of an existing host interface is kept if it is a physical type, so a check_redfish source can own the type of the host interfaces. Co-Authored-By: Claude Opus 5.5 --- module/sources/vmware/config.py | 6 +++ module/sources/vmware/connection.py | 13 ++---- settings-example.ini | 5 +++ tests/test_vmware_host_nic_type.py | 64 +++++++++++++++++------------ 4 files changed, 53 insertions(+), 35 deletions(-) diff --git a/module/sources/vmware/config.py b/module/sources/vmware/config.py index 771eef4..8da9c4c 100644 --- a/module/sources/vmware/config.py +++ b/module/sources/vmware/config.py @@ -553,6 +553,12 @@ def __init__(self): description="""define if the name of the device interface discovered overwrites the interface name in NetBox. The interface will only be matched by identical MAC address""", default_value=True), + ConfigOption("overwrite_host_interface_type", + bool, + description="""define if the type of a host interface derived from its link speed + overwrites a physical interface type in NetBox. Set to False if a source which knows the + hardware (i.e. check_redfish) sets the type of the host interfaces""", + default_value=True), ConfigOption("overwrite_vm_interface_name", bool, description="""define if the name of the VM interface discovered overwrites the diff --git a/module/sources/vmware/connection.py b/module/sources/vmware/connection.py index e7dc12a..85e41c2 100644 --- a/module/sources/vmware/connection.py +++ b/module/sources/vmware/connection.py @@ -1894,19 +1894,14 @@ def add_device_vm_to_inventory(self, object_type, object_data, pnic_data=None, v if object_type == NBVM and self.settings.overwrite_vm_interface_name is False: del int_data["name"] - # the pNIC type is only derived from the current link speed. Keep a physical type - # which supports this speed (i.e. an SFP+ port set by redfish running at 1G or - # without link), otherwise both sources overwrite each other on every run. - if object_type == NBDevice: - valid_types = NetBoxInterfaceType.valid_types + # vCenter derives the pNIC type from the current link speed only. Keep a physical type + # set by a source which knows the hardware (i.e. check_redfish) if configured. + if object_type == NBDevice and self.settings.overwrite_host_interface_type is False: current_type = grab(nic_object_dict.get(int_name), "data.type") # NetBox returns choice fields as {"value": ..., "label": ...} if isinstance(current_type, dict): current_type = current_type.get("value") - current_speed = valid_types.get(current_type) - new_speed = valid_types.get(int_data.get("type")) - if isinstance(current_speed, int) and isinstance(new_speed, int) and \ - current_speed >= new_speed: + if current_type not in [None, "virtual", "bridge", "lag", "other"]: int_data.pop("type", None) # add/update interface with retrieved data diff --git a/settings-example.ini b/settings-example.ini index d8640c2..c63a034 100644 --- a/settings-example.ini +++ b/settings-example.ini @@ -518,6 +518,11 @@ password = super-secret ; NetBox. The interface will only be matched by identical MAC address ;overwrite_device_interface_name = True +; define if the type of a host interface derived from its link speed overwrites a physical +; interface type in NetBox. Set to False if a source which knows the hardware (i.e. +; check_redfish) sets the type of the host interfaces +;overwrite_host_interface_type = True + ; define if the name of the VM interface discovered overwrites the interface name in ; NetBox. The interface will only be matched by identical MAC address ;overwrite_vm_interface_name = True diff --git a/tests/test_vmware_host_nic_type.py b/tests/test_vmware_host_nic_type.py index 8293c65..7a8e06b 100644 --- a/tests/test_vmware_host_nic_type.py +++ b/tests/test_vmware_host_nic_type.py @@ -9,24 +9,26 @@ """ vCenter only knows the current link speed of a host pNIC, so the interface type it derives -is a guess. A physical type already in NetBox which supports that speed (i.e. an SFP+ port -set by the check_redfish source, running at 1G or without link) must be kept. Otherwise both -sources overwrite each other's type on every run. +is a guess. With 'overwrite_host_interface_type = False' a physical type already in NetBox +(i.e. an SFP+ port set by the check_redfish source, running at 1G or without link) is kept. +Otherwise both sources overwrite each other's type. By default vCenter still sets the type. """ import pytest -from module.netbox.object_classes import NBDevice, NBInterface, NetBoxInterfaceType +from module.netbox.object_classes import NBDevice, NBInterface from module.sources import instantiate_sources +KEEP_TYPE = "\noverwrite_host_interface_type = False\n" -def first_sync(vcsim, inventory, load_config, vmware_settings): - load_config(vmware_settings) + +def first_sync(vcsim, inventory, load_config, settings): + load_config(settings) first_run = instantiate_sources()[0] inventory.resolve_relations() first_run.apply() interfaces = [i for i in inventory.get_all_items(NBInterface) - if isinstance(NetBoxInterfaceType.valid_types.get(i.data.get("type")), int)] + if i.data.get("type") not in [None, "virtual", "other"]] if not interfaces: pytest.skip(f"vcsim dump '{vcsim.name}' has no physical host interfaces") @@ -38,12 +40,17 @@ def first_sync(vcsim, inventory, load_config, vmware_settings): this_object.updated_items = list() this_object.unset_items = list() - return interfaces + return {interface: interface.data.get("type") for interface in interfaces} + +def second_sync(inventory, load_config, settings, nic_type): + for interface in inventory.get_all_items(NBInterface): + if interface.data.get("type") not in [None, "virtual"]: + # the shape NetBox hands back for a choice field + interface.data["type"] = {"value": nic_type, "label": nic_type} -def second_sync(inventory, load_config, vmware_settings): inventory.source_list = [] - load_config(vmware_settings) + load_config(settings) second_run = instantiate_sources()[0] inventory.resolve_relations() second_run.apply() @@ -54,29 +61,34 @@ def type_value(interface): return nic_type.get("value") if isinstance(nic_type, dict) else nic_type -def test_faster_physical_type_is_kept(vcsim, inventory, load_config, vmware_settings): - interfaces = first_sync(vcsim, inventory, load_config, vmware_settings) - for interface in interfaces: - # the shape NetBox hands back for a choice field - interface.data["type"] = {"value": "400gbase-x-qsfpdd", "label": "QSFP-DD (400GE)"} +def test_physical_type_is_kept(vcsim, inventory, load_config, vmware_settings): + discovered = first_sync(vcsim, inventory, load_config, vmware_settings + KEEP_TYPE) - second_sync(inventory, load_config, vmware_settings) + second_sync(inventory, load_config, vmware_settings + KEEP_TYPE, "10gbase-x-sfpp") - for interface in interfaces: - assert type_value(interface) == "400gbase-x-qsfpdd", \ + for interface in discovered: + assert type_value(interface) == "10gbase-x-sfpp", \ f"type of '{interface.get_display_name()}' was overwritten from the link speed" -def test_slower_physical_type_is_updated(vcsim, inventory, load_config, vmware_settings): - interfaces = first_sync(vcsim, inventory, load_config, vmware_settings) - discovered = {interface: interface.data.get("type") for interface in interfaces} - for interface in interfaces: - interface.data["type"] = {"value": "100base-tx", "label": "100BASE-TX (10/100ME)"} +def test_type_other_is_still_updated(vcsim, inventory, load_config, vmware_settings): + discovered = first_sync(vcsim, inventory, load_config, vmware_settings + KEEP_TYPE) + + second_sync(inventory, load_config, vmware_settings + KEEP_TYPE, "other") + + for interface, discovered_type in discovered.items(): + assert type_value(interface) == discovered_type, \ + f"type 'other' of '{interface.get_display_name()}' was not updated" + + +def test_type_is_overwritten_by_default(vcsim, inventory, load_config, vmware_settings): + """vCenter-only setups still get a wrong type corrected""" + discovered = first_sync(vcsim, inventory, load_config, vmware_settings) - second_sync(inventory, load_config, vmware_settings) + second_sync(inventory, load_config, vmware_settings, "400gbase-x-qsfpdd") for interface, discovered_type in discovered.items(): - if discovered_type == "100base-tx": + if discovered_type == "400gbase-x-qsfpdd": continue assert type_value(interface) == discovered_type, \ - f"type of '{interface.get_display_name()}' was not updated to the link speed" + f"type of '{interface.get_display_name()}' was not set from the link speed"