From 5e7205025590ccbe5b00b0f2fd497d916b715838 Mon Sep 17 00:00:00 2001 From: Thomas Foks Date: Tue, 3 Nov 2026 11:11:33 +0100 Subject: [PATCH] feat(check_redfish): add option to sync the asset tag check_redfish reports the asset tag of the system and of the chassis, but the source did not sync it, so servers which are only known via their BMC never got an asset tag in NetBox. The new option 'collect_hardware_asset_tag' (default: False) sets the system asset tag, falling back to the chassis asset tag, as the device asset tag. Placeholder values like 'Unknown' are ignored. An asset tag which another device in NetBox already has is not set and a warning is logged, since NetBox would reject the whole device update. Co-Authored-By: Claude Opus 5.5 --- module/sources/check_redfish/config.py | 8 ++ .../sources/check_redfish/import_inventory.py | 34 ++++++ settings-example.ini | 6 + tests/test_check_redfish_asset_tag.py | 106 ++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 tests/test_check_redfish_asset_tag.py diff --git a/module/sources/check_redfish/config.py b/module/sources/check_redfish/config.py index 00db8503..863b2dbf 100644 --- a/module/sources/check_redfish/config.py +++ b/module/sources/check_redfish/config.py @@ -54,6 +54,14 @@ def __init__(self): is then stored in the 'system_serial' custom field""", default_value=False), + ConfigOption("collect_hardware_asset_tag", + bool, + description="""define if the asset tag reported by the BMC (system asset tag, + falling back to the chassis asset tag) is set as the NetBox device asset tag. + Placeholder values are ignored, as is an asset tag which another device in + NetBox already has, since asset tags are unique in NetBox""", + default_value=False), + ConfigOption("model_components_as_modules", bool, description="""model discovered hardware components (CPUs, memory, drives, diff --git a/module/sources/check_redfish/import_inventory.py b/module/sources/check_redfish/import_inventory.py index 91add4e1..4eec7650 100644 --- a/module/sources/check_redfish/import_inventory.py +++ b/module/sources/check_redfish/import_inventory.py @@ -38,6 +38,10 @@ class CheckRedfish(SourceBase): # minimum check_redfish inventory version minimum_check_redfish_version = "2.0.0" + # values a BMC reports in place of an asset tag which was never set + unset_asset_tags = ["Default string", "NA", "N/A", "None", "Null", "Unknown", "Not Specified", + "To be filled by O.E.M."] + dependent_netbox_objects = [ NBTag, NBManufacturer, @@ -282,6 +286,25 @@ def get_service_tag(self): return get_string_or_none(grab(self.inventory_file_content, "inventory.chassis.0.sku")) + def get_asset_tag(self): + """ + Return the asset tag of the system, falling back to the one of the chassis. Placeholder + values which a BMC reports for an asset tag that was never set are ignored. + + Returns + ------- + (str, None): the asset tag + """ + + unset_asset_tags = [x.lower() for x in self.unset_asset_tags] + + for path in ["inventory.system.0.asset_tag", "inventory.chassis.0.asset_tag"]: + asset_tag = get_string_or_none(grab(self.inventory_file_content, path)) + if asset_tag is not None and asset_tag.lower() not in unset_asset_tags: + return asset_tag + + return None + def update_device(self): system = grab(self.inventory_file_content, "inventory.system.0") @@ -351,6 +374,17 @@ def update_device(self): if serial is not None: device_data["serial"] = serial + if self.settings.collect_hardware_asset_tag is True: + asset_tag = self.get_asset_tag() + if asset_tag is not None: + # NetBox rejects an asset tag which another device already has, and with it the whole update + other_device = self.inventory.get_by_data(NBDevice, data={"asset_tag": asset_tag}) + if other_device is not None and other_device is not self.device_object: + log.warning(f"Asset tag '{asset_tag}' of '{self.device_object.get_display_name()}' is already " + f"used by device '{other_device.get_display_name()}'. Not setting the asset tag.") + else: + device_data["asset_tag"] = asset_tag + self.device_object.update(data=device_data, source=self) def update_power_supply(self): diff --git a/settings-example.ini b/settings-example.ini index bf8ab5af..51b00115 100644 --- a/settings-example.ini +++ b/settings-example.ini @@ -638,6 +638,12 @@ inventory_file_path = /full/path/to/inventory/files ; serial number (the Dell PPID) is then stored in the 'system_serial' custom field ;dell_serial_from_service_tag = False +; define if the asset tag reported by the BMC (system asset tag, falling back to the +; chassis asset tag) is set as the NetBox device asset tag. Placeholder values are +; ignored, as is an asset tag which another device in NetBox already has, since asset tags +; are unique in NetBox +;collect_hardware_asset_tag = False + ; model discovered hardware components (CPUs, memory, drives, controllers, NICs, ...) as ; NetBox modules instead of the deprecated inventory items. Requires NetBox >= 4.3, on ; older versions inventory items are used diff --git a/tests/test_check_redfish_asset_tag.py b/tests/test_check_redfish_asset_tag.py new file mode 100644 index 00000000..be754508 --- /dev/null +++ b/tests/test_check_redfish_asset_tag.py @@ -0,0 +1,106 @@ +# -*- 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: . + +"""The asset tag a BMC reports can be set as the NetBox device asset tag. + +Drives the real update_device() against real NBDevice objects. +""" + +from module.netbox.object_classes import NBDevice + +ASSET_TAG = "AS00174157" + + +def system(system_asset_tag=ASSET_TAG, chassis_asset_tag=None): + """A system as check_redfish reports it, with the asset tag on system and chassis.""" + + return {"inventory": { + "system": [{"id": "1", "name": "System", "manufacturer": "HPE", "model": "ProLiant DL380 Gen10", + "serial": "CZJ0000001", "host_name": "server01", "asset_tag": system_asset_tag, + "health_status": "OK", "power_state": "On"}], + "chassis": [{"id": "1", "asset_tag": chassis_asset_tag}], + }} + + +def run_update_device(context, content): + context.source.inventory_file_content = content + context.source.update_device() + return context.device + + +def test_asset_tag_is_not_synced_by_default(check_redfish_source): + """Existing behaviour with the option off, which must not change.""" + + device = run_update_device(check_redfish_source(), system()) + + assert device.data.get("asset_tag") is None + + +def test_option_sets_the_system_asset_tag(check_redfish_source): + device = run_update_device(check_redfish_source(collect_hardware_asset_tag=True), + system(chassis_asset_tag="CHASSIS-TAG")) + + assert device.data["asset_tag"] == ASSET_TAG + + +def test_chassis_asset_tag_is_the_fallback(check_redfish_source): + """Some BMCs report the asset tag only on the chassis.""" + + device = run_update_device(check_redfish_source(collect_hardware_asset_tag=True), + system(system_asset_tag=None, chassis_asset_tag=ASSET_TAG)) + + assert device.data["asset_tag"] == ASSET_TAG + + +def test_padded_asset_tag_is_stripped(check_redfish_source): + device = run_update_device(check_redfish_source(collect_hardware_asset_tag=True), + system(system_asset_tag=f" {ASSET_TAG} ")) + + assert device.data["asset_tag"] == ASSET_TAG + + +def test_placeholder_asset_tags_are_ignored(check_redfish_source): + """A placeholder on the system must not hide a real chassis asset tag, nor be synced itself.""" + + context = check_redfish_source(collect_hardware_asset_tag=True) + + device = run_update_device(context, system(system_asset_tag="Unknown", chassis_asset_tag=ASSET_TAG)) + assert device.data["asset_tag"] == ASSET_TAG + + other = context.inventory.add_object(NBDevice, data={"name": "server02"}, source=context.source) + context.source.device_object = other + context.source.inventory_file_content = system(system_asset_tag="To Be Filled By O.E.M.", + chassis_asset_tag=" ") + context.source.update_device() + + assert other.data.get("asset_tag") is None + + +def test_asset_tag_of_another_device_is_not_set(check_redfish_source): + """NetBox rejects a duplicate asset tag and with it every other change of the update.""" + + context = check_redfish_source(collect_hardware_asset_tag=True) + context.inventory.add_object(NBDevice, data={"name": "other", "asset_tag": ASSET_TAG}, source=context.source) + + device = run_update_device(context, system()) + + assert device.data.get("asset_tag") is None + assert device.data["serial"] == "CZJ0000001" + + +def test_own_asset_tag_is_kept(check_redfish_source): + """The device which already has the asset tag is not a conflict with itself.""" + + context = check_redfish_source(collect_hardware_asset_tag=True) + context.device.data["asset_tag"] = ASSET_TAG + + device = run_update_device(context, system()) + + assert device.data["asset_tag"] == ASSET_TAG + assert "asset_tag" not in device.updated_items