diff --git a/module/sources/common/source_base.py b/module/sources/common/source_base.py index 70f459f..7538e5d 100644 --- a/module/sources/common/source_base.py +++ b/module/sources/common/source_base.py @@ -188,6 +188,12 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac current_interfaces_by_wwn = dict() current_fc_interface_names = set() + # netbox-sync cuts names to the data model length when it creates an interface, + # compare discovered names the same way + interface_class = NBVMInterface if isinstance(device_vm_object, NBVM) else NBInterface + max_name_length = interface_class().data_model.get("name") + discovered_names_by_cut_name = dict() + return_data = dict() # grab current data @@ -244,11 +250,24 @@ def map_object_interfaces_to_current_interfaces(self, device_vm_object, interfac if "virtual" not in str(int_type_string): int_type = "physical" + stored_int_name = int_name + if isinstance(max_name_length, int) and isinstance(int_name, str) and len(int_name) > max_name_length: + stored_int_name = int_name[0:max_name_length] + log.debug(f"Name of interface '{int_name}' exceeds {max_name_length} characters, " + f"comparing it as '{stored_int_name}'") + + colliding_name = discovered_names_by_cut_name.get(stored_int_name) + if colliding_name is not None: + log.warning(f"Interfaces '{colliding_name}' and '{int_name}' of " + f"'{device_vm_object.get_display_name()}' share the same first {max_name_length} " + f"characters. Only one of them can exist in NetBox with that name") + discovered_names_by_cut_name[stored_int_name] = int_name + # match simply by name matching_int = None - if int_name in current_object_interface_names: + if stored_int_name in current_object_interface_names: log.debug2(f"Found 1:1 name match for NIC '{int_name}'") - matching_int = current_object_interfaces.get(int_name) + matching_int = current_object_interfaces.get(stored_int_name) # match by WWN when no interface of that name exists yet if matching_int is None and int_wwn is not None: diff --git a/tests/test_interface_matching_truncated_name.py b/tests/test_interface_matching_truncated_name.py new file mode 100644 index 0000000..fe30130 --- /dev/null +++ b/tests/test_interface_matching_truncated_name.py @@ -0,0 +1,79 @@ +# -*- 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 names longer than the NetBox limit have to match the name NetBox stored. + +netbox-sync cuts interface names to 64 characters when it creates them. A Lenovo XCC port is named after the +manager model and its Redfish id, which together are longer than that. Comparing the full +discovered name never matched the stored one, so the port was created again on every run +and NetBox rejected it with 'Interface with this Device and Name already exists.' +""" + +import logging + +from module.netbox.object_classes import NBInterface, NBVM, NBVMInterface + +XCC_PORT_NAME = "Lenovo XClarity Controller 2 (1:NIC:672cab626d845d56d397509738c8b03900c7d706)" + + +def xcc_port(): + return {XCC_PORT_NAME: {"name": XCC_PORT_NAME, "type": "other", "mgmt_only": True}} + + +def test_long_name_matches_the_stored_interface(check_redfish_source): + + context = check_redfish_source() + stored = context.inventory.add_object( + NBInterface, data={"name": XCC_PORT_NAME, "device": context.device, "type": "other"}, + source=context.source) + assert stored.data.get("name") != XCC_PORT_NAME, "name was not cut to the NetBox limit" + + data = context.source.map_object_interfaces_to_current_interfaces(context.device, xcc_port(), True) + + assert data[XCC_PORT_NAME] is stored + + +def test_long_name_without_stored_interface_stays_unmatched(check_redfish_source): + + context = check_redfish_source() + + data = context.source.map_object_interfaces_to_current_interfaces(context.device, xcc_port(), True) + + assert data[XCC_PORT_NAME] is None + + +def test_long_vm_interface_name_matches_the_stored_interface(check_redfish_source): + + context = check_redfish_source() + # only the VMware source syncs VMs, the matcher reads its VM exclude filter + context.source.settings.vm_interface_exclude_filter = None + vm = context.inventory.add_object(NBVM, data={"name": "vm01"}, source=context.source) + name = "Network adapter 1 (" + "a" * 60 + ")" + stored = context.inventory.add_object( + NBVMInterface, data={"name": name, "virtual_machine": vm}, source=context.source) + assert stored.data.get("name") != name, "name was not cut to the NetBox limit" + + data = context.source.map_object_interfaces_to_current_interfaces( + vm, {name: {"name": name, "type": "virtual"}}, True) + + assert data[name] is stored + + +def test_names_colliding_after_the_cut_are_reported(check_redfish_source, caplog): + + context = check_redfish_source() + first = XCC_PORT_NAME + second = XCC_PORT_NAME.replace("b03900c7d706", "000000000000") + ports = {name: {"name": name, "type": "other", "mgmt_only": True} for name in [first, second]} + + caplog.set_level(logging.WARNING, logger="NetBox-Sync") + context.source.map_object_interfaces_to_current_interfaces(context.device, ports, True) + + warnings = [r.getMessage() for r in caplog.records if r.levelno == logging.WARNING] + assert any(first in message and second in message for message in warnings), warnings