From 30b0bfc98446893853cea26158be42d851824bc7 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Thu, 26 Aug 2021 14:38:54 -0400 Subject: [PATCH] Added workaround flag to the inventory script --- README.md | 7 + redfish_utilities/inventory.py | 288 +++++++++++++++++++++------------ scripts/rf_sys_inventory.py | 9 +- setup.py | 2 +- 4 files changed, 193 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index c9f8012..a90f4ff 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ pip install dist/redfish_utilities-x.x.x.tar.gz External modules: * redfish: https://pypi.python.org/pypi/redfish +* XlsxWriter: https://pypi.org/project/XlsxWriter You may install the external modules by running: @@ -99,6 +100,9 @@ optional arguments: --write [WRITE], -w [WRITE] Indicates if the inventory should be written to a spreadsheet and what the file name should be if given + --workaround, -workaround + Indicates if workarounds should be attempted for non- + conformant services ``` Example: `rf_sys_inventory.py -u root -p root -r https://192.168.1.100 -details` @@ -392,6 +396,9 @@ optional arguments: --attribute name value, -a name value Sets a BIOS attribute to a new value; can be supplied multiple times to set multiple attributes + --workaround, -workaround + Indicates if workarounds should be attempted for non- + conformant services ``` Example: `rf_bios_settings.py -u root -p root -r https://192.168.1.100 -a BiosMode Legacy` diff --git a/redfish_utilities/inventory.py b/redfish_utilities/inventory.py index 5432a42..6ae2031 100644 --- a/redfish_utilities/inventory.py +++ b/redfish_utilities/inventory.py @@ -12,57 +12,72 @@ Redfish service for an inventory of components """ +import warnings import xlsxwriter +from .messages import verify_response -def get_system_inventory( context ): +class RedfishChassisNotFoundError( Exception ): + """ + Raised when a matching chassis cannot be found + """ + pass + +def get_system_inventory( context, workaround = False ): """ Walks a Redfish service for system component information, such as drives, processors, and memory Args: context: The Redfish client object with an open session + workaround: Indicates if workarounds should be attempted for non-conformant services Returns: A list containing all system component information """ + chassis_uri_pattern = "/redfish/v1/Chassis/{}" inventory_list = [] - # Get the Service Root to find the Chassis Collection - service_root = context.get( "/redfish/v1/" ) - if "Chassis" not in service_root.dict: - # No Chassis Collection + # Get the set of chassis instances to initialize the structure + try: + chassis_ids = get_chassis_ids( context ) + except: + # No chassis instances return inventory_list - # Get the Chassis Collection and iterate through its collection - chassis_col = context.get( service_root.dict["Chassis"]["@odata.id"] ) - for chassis_member in chassis_col.dict["Members"]: - chassis = context.get( chassis_member["@odata.id"] ) - - # Catalog Chassis itself + # Set up the inventory list based on the chassis instances found + # This is done prior to cataloging anything since depending on how links are used, some devices might point back to a chassis instance not yet cataloged + for chassis_id in chassis_ids: chassis_instance = { - "ChassisName": chassis.dict["Id"], + "ChassisName": chassis_id, "Chassis": [], "Processors": [], "Memory": [], "Drives": [], "PCIeDevices": [], "StorageControllers": [], - "NetworkAdapters": [] + "NetworkAdapters": [], + "Switches": [] } inventory_list.append( chassis_instance ) - catalog_resource( chassis.dict, chassis_instance["Chassis"] ) - # Catalog all Drives, PCIeDevices, NetworkAdapters, Systems, and ResourceBlocks in the Chassis - if "Links" in chassis.dict: - catalog_array( context, chassis.dict["Links"], "Drives", chassis_instance["Drives"] ) - catalog_array( context, chassis.dict["Links"], "PCIeDevices", chassis_instance["PCIeDevices"] ) - catalog_systems( context, chassis.dict["Links"], "ComputerSystems", chassis_instance ) - catalog_collection( context, chassis.dict, "NetworkAdapters", chassis_instance["NetworkAdapters"] ) + # Go through each chassis and catalog the results + for chassis_id in chassis_ids: + chassis_uri = chassis_uri_pattern.format( chassis_id ) + chassis = context.get( chassis_uri ) + try: + verify_response( chassis ) + except: + if workaround: + warnings.warn( "Could not access '{}'. Contact your vendor. Skipping...".format( chassis_uri ) ) + continue + else: + raise + catalog_resource( context, chassis.dict, inventory_list, chassis_id, workaround ) return inventory_list -def catalog_array( context, resource, name, inventory ): +def catalog_array( context, resource, name, inventory, chassis_id, workaround ): """ Catalogs an array of resources for the inventory list @@ -70,117 +85,121 @@ def catalog_array( context, resource, name, inventory ): context: The Redfish client object with an open session resource: The resource with the array name: The name of the property of the array - inventory: The inventory list to update + inventory: The inventory to update + chassis_id: The identifier for the chassis being scanned + workaround: Indicates if workarounds should be attempted for non-conformant services """ if name in resource: for member in resource[name]: member_res = context.get( member["@odata.id"] ) - catalog_resource( member_res.dict, inventory ) + try: + verify_response( member_res ) + except: + if workaround: + warnings.warn( "Could not access '{}'. Contact your vendor. Skipping...".format( member["@odata.id"] ) ) + continue + else: + raise + catalog_resource( context, member_res.dict, inventory, chassis_id, workaround ) -def catalog_collection( context, resource, name, inventory ): +def catalog_collection( context, resource, name, inventory, chassis_id, workaround ): """ Catalogs a collection of resources for the inventory list Args: context: The Redfish client object with an open session - resource: The resource with the collection - name: The name of the property of the collection - inventory: The inventory list to update - """ - - if name in resource: - collection = context.get( resource[name]["@odata.id"] ) - for member in collection.dict["Members"]: - member_res = context.get( member["@odata.id"] ) - catalog_resource( member_res.dict, inventory ) - -def catalog_systems( context, resource, name, inventory ): - """ - Catalogs an array of systems for the inventory list - - Args: - context: The Redfish client object with an open session - resource: The resource with the array of computer systems - name: The name of the property of the array - inventory: The inventory list to update - """ - - if name in resource: - for system in resource[name]: - system_res = context.get( system["@odata.id"] ) - - # Catalog all Processors, Memory, and PCIeDevices in the System - catalog_collection( context, system_res.dict, "Processors", inventory["Processors"] ) - catalog_collection( context, system_res.dict, "Memory", inventory["Memory"] ) - catalog_array( context, system_res.dict, "PCIeDevices", inventory["PCIeDevices"] ) - catalog_simple_storage( context, system_res.dict, "SimpleStorage", inventory["Drives"] ) - catalog_storage( context, system_res.dict, "Storage", inventory ) - -def catalog_simple_storage( context, resource, name, inventory ): - """ - Catalogs a collection of Simple Storage resources for the inventory list - - Args: - context: The Redfish client object with an open session - resource: The resource with the collection + resource: The resource with the array name: The name of the property of the collection - inventory: The inventory list to update + inventory: The inventory to update + chassis_id: The identifier for the chassis being scanned + workaround: Indicates if workarounds should be attempted for non-conformant services """ if name in resource: collection = context.get( resource[name]["@odata.id"] ) - for member in collection.dict["Members"]: - member_res = context.get( member["@odata.id"] ) - if "Devices" in member_res.dict: - for index, drive in enumerate( member_res.dict["Devices"] ): - drive["@odata.id"] = "{}#/Devices/{}".format( member_res.dict["@odata.id"], index ) - drive["@odata.type"] = "#Drive.Drive" - drive["Id"] = drive["Name"] - catalog_resource( drive, inventory ) - -def catalog_storage( context, resource, name, inventory ): - """ - Catalogs a collection of Storage resources for the inventory list - - Args: - context: The Redfish client object with an open session - resource: The resource with the collection - name: The name of the property of the collection - inventory: The inventory list to update - """ + try: + verify_response( collection ) + except: + if workaround: + warnings.warn( "Could not access '{}'. Contact your vendor. Skipping...".format( resource[name]["@odata.id"] ) ) + return + else: + raise + catalog_array( context, collection.dict, "Members", inventory, chassis_id, workaround ) - if name in resource: - collection = context.get( resource[name]["@odata.id"] ) - for member in collection.dict["Members"]: - member_res = context.get( member["@odata.id"] ) - catalog_array( context, member_res.dict, "Drives", inventory["Drives"] ) - if "StorageControllers" in member_res.dict: - for index, controller in enumerate( member_res.dict["StorageControllers"] ): - controller["@odata.type"] = "#StorageController.StorageController" - controller["Id"] = controller["MemberId"] - catalog_resource( controller, inventory["StorageControllers"] ) - -def catalog_resource( resource, inventory ): +def catalog_resource( context, resource, inventory, chassis_id, workaround ): """ Catalogs a resource for the inventory list Args: + context: The Redfish client object with an open session resource: The resource to catalog - inventory: The inventory list to update + inventory: The inventory to update + chassis_id: The identifier for the chassis being scanned + workaround: Indicates if workarounds should be attempted for non-conformant services """ - # Scan the inventory to ensure this is a new entry - for item in inventory: - if item["Uri"] == resource["@odata.id"]: - return - - resource_type = resource["@odata.type"].rsplit( "." )[-1] + resource_type = resource["@odata.type"].rsplit(".")[-1] + # Based on the resource type, see if anything needs to be cataloged within it + if resource_type == "Chassis": + # Catalog all of the components within the chassis + catalog_collection( context, resource, "NetworkAdapters", inventory, chassis_id, workaround ) + catalog_collection( context, resource, "Drives", inventory, chassis_id, workaround ) + catalog_collection( context, resource, "PCIeDevices", inventory, chassis_id, workaround ) + catalog_collection( context, resource, "Memory", inventory, chassis_id, workaround ) + if "Links" in resource: + catalog_array( context, resource["Links"], "Drives", inventory, chassis_id, workaround ) + catalog_array( context, resource["Links"], "PCIeDevices", inventory, chassis_id, workaround ) + catalog_array( context, resource["Links"], "Switches", inventory, chassis_id, workaround ) + catalog_array( context, resource["Links"], "ComputerSystems", inventory, chassis_id, workaround ) + elif resource_type == "ComputerSystem": + # Catalog all of the components within the system + catalog_collection( context, resource, "Processors", inventory, chassis_id, workaround ) + catalog_collection( context, resource, "Memory", inventory, chassis_id, workaround ) + catalog_collection( context, resource, "SimpleStorage", inventory, chassis_id, workaround ) + catalog_collection( context, resource, "Storage", inventory, chassis_id, workaround ) + # The system itself does not get cataloged (the chassis representation should cover this) + return + elif resource_type == "Storage": + # Catalog the drives and storage controllers in the storage subsystem + catalog_array( context, resource, "Drives", inventory, chassis_id, workaround ) + if "StorageControllers" in resource: + for index, controller in enumerate( resource["StorageControllers"] ): + controller["@odata.type"] = "#StorageController.StorageController" + controller["Id"] = controller["MemberId"] + catalog_resource( context, controller, inventory, chassis_id, workaround ) + # The storage subsystem itself does not get cataloged + return + elif resource_type == "SimpleStorage": + # If there is a full storage representation of this resource, skip it + if "Links" in resource: + if "Storage" in resource["Links"]: + return + + # Catalog the devices (as drives) + if "Devices" in resource: + for index, drive in enumerate( resource["Devices"] ): + drive["@odata.id"] = "{}#/Devices/{}".format( resource["@odata.id"], index ) + drive["@odata.type"] = "#Drive.Drive" + drive["Id"] = drive["Name"] + catalog_resource( context, drive, inventory, chassis_id, workaround ) + # The simple storage subsystem itself does not get cataloged + return + + # If the resource has a pointer back to chassis, use the identifier in the link for cataloging + if "Links" in resource: + if "Chassis" in resource["Links"]: + if isinstance( resource["Links"]["Chassis"], dict ): + chassis_id = resource["Links"]["Chassis"]["@odata.id"].strip( "/" ).split( "/" )[-1] + + # Determine the location property location_prop = "Location" if resource_type == "Drive": location_prop = "PhysicalLocation" + # Pull out all relevant properties for the catalog catalog = { "Uri": resource["@odata.id"], "PartNumber": resource.get( "PartNumber", None ), @@ -199,24 +218,39 @@ def catalog_resource( resource, inventory ): catalog["Label"] = resource_type + ": " + resource["Id"] # Build a string description of the component based on other properties + entry_tag = None prop_list = [] if resource_type == "Chassis": + entry_tag = "Chassis" prop_list = [ "Model" ] elif resource_type == "Processor": + entry_tag = "Processors" if catalog["Model"] is not None: prop_list = [ "Model" ] else: prop_list = [ "Manufacturer", "ProcessorArchitecture", "ProcessorType", "TotalCores", "MaxSpeedMHz" ] elif resource_type == "Memory": + entry_tag = "Memory" prop_list = [ "Manufacturer", "CapacityMiB", "MemoryDeviceType", "MemoryType" ] elif resource_type == "Drive": + entry_tag = "Drives" prop_list = [ "Manufacturer", "CapacityBytes", "Protocol", "MediaType" ] elif resource_type == "PCIeDevice": + entry_tag = "PCIeDevices" prop_list = [ "Manufacturer", "Model", "DeviceType", "PCIeInterface" ] elif resource_type == "StorageController": + entry_tag = "StorageControllers" prop_list = [ "Manufacturer", "SpeedGbps", "SupportedDeviceProtocols" ] elif resource_type == "NetworkAdapter": + entry_tag = "NetworkAdapters" prop_list = [ "Manufacturer", "Model" ] + elif resource_type == "Switch": + entry_tag = "Switches" + prop_list = [ "Manufacturer", "Model" ] + if entry_tag is None: + # No handling set up for this resource type + # Should not happen; check the types against the possible lists + return # Based on the listed properties for the resource type, build the description string if catalog["State"] != "Absent": @@ -253,7 +287,20 @@ def catalog_resource( resource, inventory ): description_str = description_str + " Storage Controller" catalog["Description"] = description_str.strip() - inventory.append( catalog ) + # Find the inventory instance to update based on the chassis identifier + inventory_instance = None + for chassis_inventory in inventory: + if chassis_inventory["ChassisName"] == chassis_id: + inventory_instance = chassis_inventory + if inventory_instance is None: + # No matching spot to put this entry in the inventory + return + # Check if this is a new entry + for item in inventory_instance[entry_tag]: + if item["Uri"] == resource["@odata.id"]: + return + + inventory_instance[entry_tag].append( catalog ) def print_system_inventory( inventory_list, details = False, skip_absent = False ): """ @@ -275,7 +322,7 @@ def print_system_inventory( inventory_list, details = False, skip_absent = False print( inventory_line_format.format( "Name", "Description" ) ) # Go through each component type in the chassis - type_list = [ "Chassis", "Processors", "Memory", "Drives", "PCIeDevices", "StorageControllers", "NetworkAdapters" ] + type_list = [ "Chassis", "Processors", "Memory", "Drives", "PCIeDevices", "StorageControllers", "NetworkAdapters", "Switches" ] for inv_type in type_list: # Go through each component and prints its info for item in chassis[inv_type]: @@ -292,7 +339,6 @@ def print_system_inventory( inventory_list, details = False, skip_absent = False print( inventory_line_format_detail.format( "", detail, item[detail] ) ) print( "" ) - def write_system_inventory( inventory_list, file_name ): """ Write the system inventory list into a spreadsheet @@ -321,7 +367,7 @@ def write_system_inventory( inventory_list, file_name ): for chassis in inventory_list: # Go through each component type in the chassis - type_list = [ "Chassis", "Processors", "Memory", "Drives", "PCIeDevices", "StorageControllers", "NetworkAdapters" ] + type_list = [ "Chassis", "Processors", "Memory", "Drives", "PCIeDevices", "StorageControllers", "NetworkAdapters", "Switches" ] for inv_type in type_list: # Go through each component and prints its info for item in chassis[inv_type]: @@ -335,3 +381,31 @@ def write_system_inventory( inventory_list, file_name ): row += 1 workbook.close() + +def get_chassis_ids( context ): + """ + Finds the chassis collection and returns all of the member's identifiers + + Args: + context: The Redfish client object with an open session + + Returns: + A list of identifiers of the members of the chassis collection + """ + + # Get the service root to find the chassis collection + service_root = context.get( "/redfish/v1/" ) + if "Chassis" not in service_root.dict: + # No system collection + raise RedfishChassisNotFoundError( "Service does not contain a chassis collection" ) + + # Get the chassis collection and iterate through its collection + avail_chassis = [] + chassis_col = context.get( service_root.dict["Chassis"]["@odata.id"] ) + while True: + for chassis_member in chassis_col.dict["Members"]: + avail_chassis.append( chassis_member["@odata.id"].strip( "/" ).split( "/" )[-1] ) + if "Members@odata.nextLink" not in chassis_col.dict: + break + chassis_col = context.get( chassis_col.dict["Members@odata.nextLink"] ) + return avail_chassis diff --git a/scripts/rf_sys_inventory.py b/scripts/rf_sys_inventory.py index 7f7fa17..9d1de5e 100644 --- a/scripts/rf_sys_inventory.py +++ b/scripts/rf_sys_inventory.py @@ -16,8 +16,6 @@ import redfish import redfish_utilities -import json - # Get the input arguments argget = argparse.ArgumentParser( description = "A tool to walk a Redfish service and list component information" ) argget.add_argument( "--user", "-u", type = str, required = True, help = "The user name for authentication" ) @@ -26,6 +24,7 @@ argget.add_argument( "--details", "-details", action = "store_true", help = "Indicates if the full details of each component should be shown" ) argget.add_argument( "--noabsent", "-noabsent", action = "store_true", help = "Indicates if absent devices should be skipped" ) argget.add_argument( "--write", "-w", nargs = "?", const = "Device_Inventory", type = str, help = "Indicates if the inventory should be written to a spreadsheet and what the file name should be if given" ) +argget.add_argument( "--workaround", "-workaround", action = "store_true", help = "Indicates if workarounds should be attempted for non-conformant services", default = False ) args = argget.parse_args() # Set up the Redfish object @@ -34,10 +33,10 @@ try: # Get and print the system inventory - inventory = redfish_utilities.get_system_inventory( redfish_obj ) - redfish_utilities.print_system_inventory( inventory, details = args.details, skip_absent = args.noabsent ) + inventory = redfish_utilities.get_system_inventory( redfish_obj, args.workaround ) + redfish_utilities.print_system_inventory( inventory, args.details, args.noabsent ) - if( args.write ): + if args.write: redfish_utilities.write_system_inventory( inventory, args.write ) finally: diff --git a/setup.py b/setup.py index 4ca21da..06b418c 100644 --- a/setup.py +++ b/setup.py @@ -40,5 +40,5 @@ "scripts/rf_update.py", "scripts/rf_virtual_media.py" ], - install_requires = [ "redfish" ] + install_requires = [ "redfish", "XlsxWriter" ] )