From 40ee9f0c6b712574c1c17f045aafe5176b3a3ccb Mon Sep 17 00:00:00 2001 From: Thomas Foks Date: Thu, 8 Oct 2026 08:48:28 +0200 Subject: [PATCH] fix: match interfaces by the primary MAC address object on NetBox >= 4.2 From NetBox 4.2 on, sources only maintain the interface's primary MAC address object. The legacy 'mac_address' field keeps the value NetBox returned when the data was read and is never updated during a run. map_object_interfaces_to_current_interfaces() still matched on that field. If one source changed an interface's primary MAC address, the next source still saw the old MAC address. With a VMware and a check_redfish source syncing the same ESXi hosts, a BMC port (i.e. Lenovo XCC) whose MAC address had ended up on vmk0 was merged into vmk0 on every run. The BMC IP moved to vmk0, the host's management IP was removed and the primary IP of the device got unset. Use the MAC address of the primary MAC address object on NetBox >= 4.2. Co-Authored-By: Claude Opus 5.5 --- module/sources/common/source_base.py | 7 ++- tests/test_interface_matching_primary_mac.py | 56 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/test_interface_matching_primary_mac.py diff --git a/module/sources/common/source_base.py b/module/sources/common/source_base.py index df098e7..7537f51 100644 --- a/module/sources/common/source_base.py +++ b/module/sources/common/source_base.py @@ -165,7 +165,12 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac # grab current data for interface in self.inventory.get_all_interfaces(device_vm_object): - int_mac = grab(interface, "data.mac_address") + # from NetBox 4.2 on 'mac_address' is a read-only leftover of the last NetBox read which sources + # don't update. Use the current primary MAC address object instead. + if version.parse(self.inventory.netbox_api_version) >= version.parse("4.2.0"): + int_mac = grab(grab(interface, "data.primary_mac_address"), "data.mac_address") + else: + int_mac = grab(interface, "data.mac_address") int_name = grab(interface, "data.name") if interface_exclude_filter is not None and int_name is not None and \ diff --git a/tests/test_interface_matching_primary_mac.py b/tests/test_interface_matching_primary_mac.py new file mode 100644 index 0000000..fa29309 --- /dev/null +++ b/tests/test_interface_matching_primary_mac.py @@ -0,0 +1,56 @@ +# -*- 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: . + +"""Interface matching by MAC must use the current primary MAC address object (NetBox >= 4.2). + +The legacy interface 'mac_address' field is only what NetBox returned at read time. Once a +previous source moved the primary MAC to another address, matching on the stale value merged +a BMC port (i.e. Lenovo XCC) into the ESXi vmk0 interface on every run. +""" + +from module.netbox.object_classes import NBInterface, NBMACAddress + +BMC_MAC = "38:68:DD:34:33:C5" +VMK_MAC = "00:50:56:67:39:DA" + + +def seed_vmk0(context, legacy_mac, primary_mac): + interface = context.inventory.add_object( + NBInterface, data={"name": "vmk0", "device": context.device, "type": "virtual", "mac_address": legacy_mac}, + source=context.source) + mac_object = context.inventory.add_object( + NBMACAddress, data={"mac_address": primary_mac, "assigned_object_id": interface, + "assigned_object_type": NBInterface}, source=context.source) + interface.update(data={"primary_mac_address": mac_object}, source=context.source) + return interface + + +def bmc_port(): + return {"XCC (1:NIC:abc)": {"name": "XCC (1:NIC:abc)", "mac_address": BMC_MAC, "type": "other", + "mgmt_only": True}} + + +def test_bmc_port_ignores_stale_legacy_mac(check_redfish_source): + + context = check_redfish_source() + seed_vmk0(context, legacy_mac=BMC_MAC, primary_mac=VMK_MAC) + + data = context.source.map_object_interfaces_to_current_interfaces(context.device, bmc_port(), True) + + assert data["XCC (1:NIC:abc)"] is None + + +def test_bmc_port_still_matches_by_primary_mac(check_redfish_source): + + context = check_redfish_source() + vmk0 = seed_vmk0(context, legacy_mac=VMK_MAC, primary_mac=BMC_MAC) + + data = context.source.map_object_interfaces_to_current_interfaces(context.device, bmc_port(), True) + + assert data["XCC (1:NIC:abc)"] is vmk0