Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions module/sources/common/source_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
79 changes: 79 additions & 0 deletions tests/test_interface_matching_truncated_name.py
Original file line number Diff line number Diff line change
@@ -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: <https://opensource.org/licenses/MIT>.

"""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
Loading