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