From 6dfff5f8fbcf0056863f2968d6bad86449d4d9af Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Fri, 21 Jun 2019 09:41:52 -0400 Subject: [PATCH 1/9] Added system inventory collection --- redfish_utilities/__init__.py | 1 + redfish_utilities/inventory.py | 190 +++++++++++++++++++++++++++++++++ scripts/rf_sys_inventory | 36 +++++++ setup.py | 1 + 4 files changed, 228 insertions(+) create mode 100644 redfish_utilities/inventory.py create mode 100644 scripts/rf_sys_inventory diff --git a/redfish_utilities/__init__.py b/redfish_utilities/__init__.py index c29a5e6..b357ce3 100644 --- a/redfish_utilities/__init__.py +++ b/redfish_utilities/__init__.py @@ -3,6 +3,7 @@ # Copyright 2019 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/master/LICENSE.md +from .inventory import get_system_inventory from .messages import print_error_payload from .messages import verify_response from .sensors import get_sensors diff --git a/redfish_utilities/inventory.py b/redfish_utilities/inventory.py new file mode 100644 index 0000000..f1575b3 --- /dev/null +++ b/redfish_utilities/inventory.py @@ -0,0 +1,190 @@ +""" +Sensors Module + +File : inventory.py + +Brief : This file contains the definitions and functionalities for scanning a + Redfish service for an inventory of components +""" + +def get_system_inventory( context ): + """ + Walks a Redfish service for system component information, such as drives, + processors, and memory + + Args: + context: The Redfish client object with an open session + + Returns: + A list containing all system component information + """ + + inventory_list = [] + + # Get the Service Root to find the Chassis Collection + service_root = context.get( "/redfish/v1/", None ) + if "Chassis" not in service_root.dict: + # No Chassis Collection + return inventory_list + + # Get the Chassis Collection and iterate through its collection + chassis_col = context.get( service_root.dict["Chassis"]["@odata.id"], None ) + for chassis_member in chassis_col.dict["Members"]: + chassis = context.get( chassis_member["@odata.id"], None ) + + # Catalog Chassis itself + chassis_instance = { + "ChassisName": chassis.dict["Id"], + "Inventory": [] + } + inventory_list.append( chassis_instance ) + catalog_resource( chassis.dict, chassis_instance["Inventory"] ) + + # 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["Inventory"] ) + catalog_array( context, chassis.dict["Links"], "PCIeDevices", chassis_instance["Inventory"] ) + catalog_systems( context, chassis.dict["Links"], "ComputerSystems", chassis_instance["Inventory"] ) + catalog_collection( context, chassis.dict, "NetworkAdapters", chassis_instance["Inventory"] ) + + return inventory_list + +def catalog_array( context, resource, name, inventory ): + """ + Catalogs an array of resources for the inventory list + + Args: + 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 + """ + + if name in resource: + for member in resource[name]: + member_res = context.get( member["@odata.id"], None ) + catalog_resource( member_res.dict, inventory ) + +def catalog_collection( context, resource, name, inventory ): + """ + 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"], None ) + for member in collection.dict["Members"]: + member_res = context.get( member["@odata.id"], None ) + 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"], None ) + + # Catalog the System itself + catalog_resource( system_res.dict, inventory ) + + # Catalog all Processors, Memory, and PCIeDevices in the System + catalog_collection( context, system_res.dict, "Processors", inventory ) + catalog_collection( context, system_res.dict, "Memory", inventory ) + catalog_array( context, system_res.dict, "PCIeDevices", inventory ) + catalog_simple_storage( context, system_res.dict, "SimpleStorage", inventory ) + 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 + 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"], None ) + for member in collection.dict["Members"]: + member_res = context.get( member["@odata.id"], None ) + 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"] = "#SimpleStorage.SimpleStorage" + 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 + """ + + if name in resource: + collection = context.get( resource[name]["@odata.id"], None ) + for member in collection.dict["Members"]: + member_res = context.get( member["@odata.id"], None ) + catalog_array( context, member_res.dict, "Drives", inventory ) + if "StorageControllers" in member_res.dict: + for index, controller in enumerate( member_res.dict["StorageControllers"] ): + controller["@odata.type"] = "#StorageController.StorageController" + catalog_resource( controller, inventory ) + +def catalog_resource( resource, inventory ): + """ + Catalogs a resource for the inventory list + + Args: + resource: The resource to catalog + inventory: The inventory list to update + """ + + # 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] + + location_prop = "Location" + if resource_type == "Drive": + location_prop = "PhysicalLocation" + + catalog = { + "Uri": resource["@odata.id"], + "Type": resource_type, + "PartNumber": resource.get( "PartNumber", None ), + "SerialNumber": resource.get( "SerialNumber", None ), + "Manufacturer": resource.get( "Manufacturer", None ), + "Model": resource.get( "Model", None ), + "SKU": resource.get( "SKU", None ), + "AssetTag": resource.get( "AssetTag", None ), + "Label": resource.get( location_prop, {} ).get( "PartLocation", {} ).get( "ServiceLabel", None ), + "State": resource.get( "Status", {} ).get( "State", None ) + } + + # If no label was found, build a default name + if catalog["Label"] is None: + catalog["Label"] = "{} {}".format( resource_type, resource["Id"] ) + + inventory.append( catalog ) diff --git a/scripts/rf_sys_inventory b/scripts/rf_sys_inventory new file mode 100644 index 0000000..5d61970 --- /dev/null +++ b/scripts/rf_sys_inventory @@ -0,0 +1,36 @@ +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/master/LICENSE.md + +""" +Redfish System Inventory + +File : rf_sys_inventory + +Brief : This script uses the redfish_utilities module to dump system inventory + information +""" + +import argparse +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" ) +argget.add_argument( "--password", "-p", type = str, required = True, help = "The password for authentication" ) +argget.add_argument( "--rhost", "-r", type = str, required = True, help = "The address of the Redfish service (with scheme)" ) +args = argget.parse_args() + +# Set up the Redfish object +redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password, default_prefix = "/redfish/v1" ) +redfish_obj.login( auth = "session" ) + +try: + print( json.dumps( redfish_utilities.get_system_inventory( redfish_obj ), sort_keys = True, indent = 4, separators = ( ",", ": " ) ) ) +finally: + # Log out + redfish_obj.logout() diff --git a/setup.py b/setup.py index 99f7cd9..d0dff03 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ "scripts/rf_boot_override", "scripts/rf_power_reset", "scripts/rf_sensor_list", + "scripts/rf_sys_inventory", "scripts/rf_update" ], install_requires = [ "redfish" ] From f7a36ab11dcbe41c3372790f41bb0ada00c08e9c Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Fri, 21 Jun 2019 11:14:53 -0400 Subject: [PATCH 2/9] Added API for printing inventory as a table --- redfish_utilities/__init__.py | 1 + redfish_utilities/inventory.py | 73 ++++++++++++++++++++++++++-------- scripts/rf_sys_inventory | 4 +- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/redfish_utilities/__init__.py b/redfish_utilities/__init__.py index b357ce3..1890b14 100644 --- a/redfish_utilities/__init__.py +++ b/redfish_utilities/__init__.py @@ -4,6 +4,7 @@ # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/master/LICENSE.md from .inventory import get_system_inventory +from .inventory import print_system_inventory from .messages import print_error_payload from .messages import verify_response from .sensors import get_sensors diff --git a/redfish_utilities/inventory.py b/redfish_utilities/inventory.py index f1575b3..6006d90 100644 --- a/redfish_utilities/inventory.py +++ b/redfish_utilities/inventory.py @@ -35,17 +35,23 @@ def get_system_inventory( context ): # Catalog Chassis itself chassis_instance = { "ChassisName": chassis.dict["Id"], - "Inventory": [] + "Chassis": [], + "Processors": [], + "Memory": [], + "Drives": [], + "PCIeDevices": [], + "StorageControllers": [], + "NetworkAdapters": [] } inventory_list.append( chassis_instance ) - catalog_resource( chassis.dict, chassis_instance["Inventory"] ) + 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["Inventory"] ) - catalog_array( context, chassis.dict["Links"], "PCIeDevices", chassis_instance["Inventory"] ) - catalog_systems( context, chassis.dict["Links"], "ComputerSystems", chassis_instance["Inventory"] ) - catalog_collection( context, chassis.dict, "NetworkAdapters", chassis_instance["Inventory"] ) + 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"] ) return inventory_list @@ -97,14 +103,11 @@ def catalog_systems( context, resource, name, inventory ): for system in resource[name]: system_res = context.get( system["@odata.id"], None ) - # Catalog the System itself - catalog_resource( system_res.dict, inventory ) - # Catalog all Processors, Memory, and PCIeDevices in the System - catalog_collection( context, system_res.dict, "Processors", inventory ) - catalog_collection( context, system_res.dict, "Memory", inventory ) - catalog_array( context, system_res.dict, "PCIeDevices", inventory ) - catalog_simple_storage( context, system_res.dict, "SimpleStorage", inventory ) + 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 ): @@ -144,11 +147,11 @@ def catalog_storage( context, resource, name, inventory ): collection = context.get( resource[name]["@odata.id"], None ) for member in collection.dict["Members"]: member_res = context.get( member["@odata.id"], None ) - catalog_array( context, member_res.dict, "Drives", inventory ) + 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" - catalog_resource( controller, inventory ) + catalog_resource( controller, inventory["StorageControllers"] ) def catalog_resource( resource, inventory ): """ @@ -172,7 +175,6 @@ def catalog_resource( resource, inventory ): catalog = { "Uri": resource["@odata.id"], - "Type": resource_type, "PartNumber": resource.get( "PartNumber", None ), "SerialNumber": resource.get( "SerialNumber", None ), "Manufacturer": resource.get( "Manufacturer", None ), @@ -185,6 +187,43 @@ def catalog_resource( resource, inventory ): # If no label was found, build a default name if catalog["Label"] is None: - catalog["Label"] = "{} {}".format( resource_type, resource["Id"] ) + catalog["Label"] = resource["Id"] inventory.append( catalog ) + +def print_system_inventory( inventory_list ): + """ + Prints the system inventory list into a table + + Args: + inventory_list: The inventory list to print + """ + + inventory_line_format = " {:30s} | {:40s} | {:20} | {:20s}" + inventory_line_format_empty = " {:30s} | Not Present" + + # Go through each chassis instance + for chassis in inventory_list: + print( "'" + chassis["ChassisName"] + "' Inventory" ) + print( inventory_line_format.format( "Name", "Model", "Part Number", "Serial Number" ) ) + + # Go through each component type in the chassis + type_list = [ "Chassis", "Processors", "Memory", "Drives", "PCIeDevices", "StorageControllers", "NetworkAdapters" ] + for inv_type in type_list: + # Go through each component and prints its info + for item in chassis[inv_type]: + model = item["Model"] + if model is None: + model = "N/A" + part_num = item["PartNumber"] + if part_num is None: + part_num = "N/A" + serial_num = item["SerialNumber"] + if serial_num is None: + serial_num = "N/A" + + if item["State"] == "Absent": + print( inventory_line_format_empty.format( item["Label"][:30] ) ) + else: + print( inventory_line_format.format( item["Label"][:30], model[:40], part_num[:20], serial_num[:20] ) ) + print( "" ) diff --git a/scripts/rf_sys_inventory b/scripts/rf_sys_inventory index 5d61970..d34bcda 100644 --- a/scripts/rf_sys_inventory +++ b/scripts/rf_sys_inventory @@ -30,7 +30,9 @@ redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.use redfish_obj.login( auth = "session" ) try: - print( json.dumps( redfish_utilities.get_system_inventory( redfish_obj ), sort_keys = True, indent = 4, separators = ( ",", ": " ) ) ) + # Get and print the system inventory + inventory = redfish_utilities.get_system_inventory( redfish_obj ) + redfish_utilities.print_system_inventory( inventory ) finally: # Log out redfish_obj.logout() From 63507728b067895f868a337caa146299d5ac0c5d Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Fri, 21 Jun 2019 11:19:37 -0400 Subject: [PATCH 3/9] Readme update for the new tool --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 28516e4..14b6788 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,31 @@ It then traverses the Chassis Collection for the Service, and reads their respec Using the information from those resources, it will build a sensor table and print the information collected. +### System Inventory + +``` +usage: rf_sys_inventory [-h] --user USER --password PASSWORD --rhost RHOST + +A tool to walk a Redfish service and list component information + +required arguments: + --user USER, -u USER The user name for authentication + --password PASSWORD, -p PASSWORD + The password for authentication + --rhost RHOST, -r RHOST + The address of the Redfish service (with scheme) + +optional arguments: + -h, --help show this help message and exit +``` + +Example: `rf_sys_inventory -u root -p root -r https://192.168.1.100` + +The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. +It then traverses the Chassis Collection for the Service, and collects component information for Processors, Memory, Drives, PCIeDevices, NetworkAdapters, and StorageControllers. +Using the information collected, it will build an inventory table and print the information. + + ### Power/Reset ``` From 9785bec13f95e24fbb44633861fa57ee68e81845 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Mon, 24 Jun 2019 11:07:15 -0400 Subject: [PATCH 4/9] Added building of component descriptions --- redfish_utilities/inventory.py | 65 +++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/redfish_utilities/inventory.py b/redfish_utilities/inventory.py index 6006d90..77d27d0 100644 --- a/redfish_utilities/inventory.py +++ b/redfish_utilities/inventory.py @@ -7,6 +7,8 @@ Redfish service for an inventory of components """ +import re + def get_system_inventory( context ): """ Walks a Redfish service for system component information, such as drives, @@ -128,7 +130,7 @@ def catalog_simple_storage( context, resource, name, inventory ): 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"] = "#SimpleStorage.SimpleStorage" + drive["@odata.type"] = "#Drive.Drive" drive["Id"] = drive["Name"] catalog_resource( drive, inventory ) @@ -187,7 +189,46 @@ def catalog_resource( resource, inventory ): # If no label was found, build a default name if catalog["Label"] is None: - catalog["Label"] = resource["Id"] + catalog["Label"] = resource_type + ": " + resource["Id"] + + # Build a string description of the component based on other properties + description_str = "" + if resource_type == "Chassis": + description_str = resource.get( "Model", "" ) + elif resource_type == "Processor": + if catalog["Model"] is not None: + description_str = catalog["Model"] + else: + core_str = "" + if resource.get( "TotalCores", None ) is not None: + core_str = str( resource["TotalCores"] ) + " Cores" + speed_str = "" + if resource.get( "MaxSpeedMHz", None ) is not None: + speed_str = "@ " + str( resource["MaxSpeedMHz"] ) + "MHz" + description_str = "{} {} {} {} {}".format( resource.get( "Manufacturer", "" ), resource.get( "ProcessorArchitecture", "" ), resource.get( "ProcessorType", "" ), core_str, speed_str ) + elif resource_type == "Memory": + size_str = "" + if resource.get( "CapacityMiB", None ) is not None: + size_str = str( resource["CapacityMiB"] ) + "MB" + description_str = "{} {} {} {}".format( resource.get( "Manufacturer", "" ), size_str, resource.get( "MemoryDeviceType", "" ), resource.get( "MemoryType", "" ) ) + elif resource_type == "Drive": + size_str = "" + if resource.get( "CapacityBytes", None ) is not None: + size_str = str( resource["CapacityBytes"] / ( 2 ** 30 ) ) + "GB" + description_str = "{} {} {} {}".format( resource.get( "Manufacturer", "" ), size_str, resource.get( "Protocol", "" ), resource.get( "MediaType", "Drive" ) ) + elif resource_type == "PCIeDevice": + description_str = "{} {}".format( resource.get( "Manufacturer", "" ), resource.get( "Model", "" ) ) + elif resource_type == "StorageController": + speed_str = "" + if resource.get( "SpeedGbps", None ) is not None: + speed_str = str( resource["SpeedGbps"] ) + "Gbps" + protocol_str = "Storage Controller" + if resource.get( "SupportedDeviceProtocols", None ) is not None : + protocol_str = resource["SupportedDeviceProtocols"].join( "/" ) + " Controller" + description_str = "{} {} {}".format( resource.get( "Manufacturer", "" ), speed_str, protocol_str ) + elif resource_type == "NetworkAdapter": + description_str = "{} {}".format( resource.get( "Manufacturer", "" ), resource.get( "Model", "" ) ) + catalog["Description"] = re.sub( " +", " ", description_str ).strip() inventory.append( catalog ) @@ -199,31 +240,21 @@ def print_system_inventory( inventory_list ): inventory_list: The inventory list to print """ - inventory_line_format = " {:30s} | {:40s} | {:20} | {:20s}" - inventory_line_format_empty = " {:30s} | Not Present" + inventory_line_format = " {:35s} | {}" + inventory_line_format_empty = " {:35s} | Not Present" # Go through each chassis instance for chassis in inventory_list: print( "'" + chassis["ChassisName"] + "' Inventory" ) - print( inventory_line_format.format( "Name", "Model", "Part Number", "Serial Number" ) ) + print( inventory_line_format.format( "Name", "Description" ) ) # Go through each component type in the chassis type_list = [ "Chassis", "Processors", "Memory", "Drives", "PCIeDevices", "StorageControllers", "NetworkAdapters" ] for inv_type in type_list: # Go through each component and prints its info for item in chassis[inv_type]: - model = item["Model"] - if model is None: - model = "N/A" - part_num = item["PartNumber"] - if part_num is None: - part_num = "N/A" - serial_num = item["SerialNumber"] - if serial_num is None: - serial_num = "N/A" - if item["State"] == "Absent": - print( inventory_line_format_empty.format( item["Label"][:30] ) ) + print( inventory_line_format_empty.format( item["Label"][:35] ) ) else: - print( inventory_line_format.format( item["Label"][:30], model[:40], part_num[:20], serial_num[:20] ) ) + print( inventory_line_format.format( item["Label"][:35], item["Description"] ) ) print( "" ) From ef95f247ac36f2f285f817cacbd4aa624f301b62 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Mon, 24 Jun 2019 13:01:52 -0400 Subject: [PATCH 5/9] Added 'details' flag to show full device details --- README.md | 5 ++++- redfish_utilities/inventory.py | 10 +++++++++- scripts/rf_sys_inventory | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 14b6788..d349c27 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Using the information from those resources, it will build a sensor table and pri ``` usage: rf_sys_inventory [-h] --user USER --password PASSWORD --rhost RHOST + [--details] A tool to walk a Redfish service and list component information @@ -75,9 +76,11 @@ required arguments: optional arguments: -h, --help show this help message and exit + --details, -details Indicates if the full details of each component should + be shown ``` -Example: `rf_sys_inventory -u root -p root -r https://192.168.1.100` +Example: `rf_sys_inventory -u root -p root -r https://192.168.1.100 -details` The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. It then traverses the Chassis Collection for the Service, and collects component information for Processors, Memory, Drives, PCIeDevices, NetworkAdapters, and StorageControllers. diff --git a/redfish_utilities/inventory.py b/redfish_utilities/inventory.py index 77d27d0..f3a45d3 100644 --- a/redfish_utilities/inventory.py +++ b/redfish_utilities/inventory.py @@ -232,15 +232,17 @@ def catalog_resource( resource, inventory ): inventory.append( catalog ) -def print_system_inventory( inventory_list ): +def print_system_inventory( inventory_list, details = False ): """ Prints the system inventory list into a table Args: inventory_list: The inventory list to print + details: True to print all of the detailed info """ inventory_line_format = " {:35s} | {}" + inventory_line_format_detail = " {:35s} | {}: {}" inventory_line_format_empty = " {:35s} | Not Present" # Go through each chassis instance @@ -257,4 +259,10 @@ def print_system_inventory( inventory_list ): print( inventory_line_format_empty.format( item["Label"][:35] ) ) else: print( inventory_line_format.format( item["Label"][:35], item["Description"] ) ) + + if details: + detail_list = [ "Manufacturer", "Model", "SKU", "PartNumber", "SerialNumber", "AssetTag" ] + for detail in detail_list: + if item[detail] is not None: + print( inventory_line_format_detail.format( "", detail, item[detail] ) ) print( "" ) diff --git a/scripts/rf_sys_inventory b/scripts/rf_sys_inventory index d34bcda..9c184f6 100644 --- a/scripts/rf_sys_inventory +++ b/scripts/rf_sys_inventory @@ -23,6 +23,7 @@ argget = argparse.ArgumentParser( description = "A tool to walk a Redfish servic argget.add_argument( "--user", "-u", type = str, required = True, help = "The user name for authentication" ) argget.add_argument( "--password", "-p", type = str, required = True, help = "The password for authentication" ) argget.add_argument( "--rhost", "-r", type = str, required = True, help = "The address of the Redfish service (with scheme)" ) +argget.add_argument( "--details", "-details", action = "store_true", help = "Indicates if the full details of each component should be shown" ) args = argget.parse_args() # Set up the Redfish object @@ -32,7 +33,7 @@ redfish_obj.login( auth = "session" ) try: # Get and print the system inventory inventory = redfish_utilities.get_system_inventory( redfish_obj ) - redfish_utilities.print_system_inventory( inventory ) + redfish_utilities.print_system_inventory( inventory, details = args.details ) finally: # Log out redfish_obj.logout() From 8a593fa2dcde0e9842a61dcd1ea8b946fdd1f7f4 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Mon, 24 Jun 2019 16:28:49 -0400 Subject: [PATCH 6/9] Added noabsent option; added some robustness around null properties in description building --- README.md | 4 +- redfish_utilities/inventory.py | 73 ++++++++++++++++++++-------------- scripts/rf_sys_inventory | 3 +- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index d349c27..2de66af 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Using the information from those resources, it will build a sensor table and pri ``` usage: rf_sys_inventory [-h] --user USER --password PASSWORD --rhost RHOST - [--details] + [--details] [--noabsent] A tool to walk a Redfish service and list component information @@ -78,6 +78,8 @@ optional arguments: -h, --help show this help message and exit --details, -details Indicates if the full details of each component should be shown + --noabsent, -noabsent + Indicates if absent devices should be skipped ``` Example: `rf_sys_inventory -u root -p root -r https://192.168.1.100 -details` diff --git a/redfish_utilities/inventory.py b/redfish_utilities/inventory.py index f3a45d3..b77f664 100644 --- a/redfish_utilities/inventory.py +++ b/redfish_utilities/inventory.py @@ -192,53 +192,65 @@ def catalog_resource( resource, inventory ): catalog["Label"] = resource_type + ": " + resource["Id"] # Build a string description of the component based on other properties - description_str = "" + prop_list = [] if resource_type == "Chassis": - description_str = resource.get( "Model", "" ) + prop_list = [ "Model" ] elif resource_type == "Processor": if catalog["Model"] is not None: - description_str = catalog["Model"] + prop_list = [ "Model" ] else: - core_str = "" - if resource.get( "TotalCores", None ) is not None: - core_str = str( resource["TotalCores"] ) + " Cores" - speed_str = "" - if resource.get( "MaxSpeedMHz", None ) is not None: - speed_str = "@ " + str( resource["MaxSpeedMHz"] ) + "MHz" - description_str = "{} {} {} {} {}".format( resource.get( "Manufacturer", "" ), resource.get( "ProcessorArchitecture", "" ), resource.get( "ProcessorType", "" ), core_str, speed_str ) + prop_list = [ "Manufacturer", "ProcessorArchitecture", "ProcessorType", "TotalCores", "MaxSpeedMHz" ] elif resource_type == "Memory": - size_str = "" - if resource.get( "CapacityMiB", None ) is not None: - size_str = str( resource["CapacityMiB"] ) + "MB" - description_str = "{} {} {} {}".format( resource.get( "Manufacturer", "" ), size_str, resource.get( "MemoryDeviceType", "" ), resource.get( "MemoryType", "" ) ) + prop_list = [ "Manufacturer", "CapacityMiB", "MemoryDeviceType", "MemoryType" ] elif resource_type == "Drive": - size_str = "" - if resource.get( "CapacityBytes", None ) is not None: - size_str = str( resource["CapacityBytes"] / ( 2 ** 30 ) ) + "GB" - description_str = "{} {} {} {}".format( resource.get( "Manufacturer", "" ), size_str, resource.get( "Protocol", "" ), resource.get( "MediaType", "Drive" ) ) + prop_list = [ "Manufacturer", "CapacityBytes", "Protocol", "MediaType" ] elif resource_type == "PCIeDevice": - description_str = "{} {}".format( resource.get( "Manufacturer", "" ), resource.get( "Model", "" ) ) + prop_list = [ "Manufacturer", "Model" ] elif resource_type == "StorageController": - speed_str = "" - if resource.get( "SpeedGbps", None ) is not None: - speed_str = str( resource["SpeedGbps"] ) + "Gbps" - protocol_str = "Storage Controller" - if resource.get( "SupportedDeviceProtocols", None ) is not None : - protocol_str = resource["SupportedDeviceProtocols"].join( "/" ) + " Controller" - description_str = "{} {} {}".format( resource.get( "Manufacturer", "" ), speed_str, protocol_str ) + prop_list = [ "Manufacturer", "SpeedGbps", "SupportedDeviceProtocols" ] elif resource_type == "NetworkAdapter": - description_str = "{} {}".format( resource.get( "Manufacturer", "" ), resource.get( "Model", "" ) ) - catalog["Description"] = re.sub( " +", " ", description_str ).strip() + prop_list = [ "Manufacturer", "Model" ] + + # Based on the listed properties for the resource type, build the description string + if catalog["State"] != "Absent": + description_str = "" + for prop in prop_list: + if resource.get( prop, None ) is not None: + prop_val = resource[prop] + # Some properties require refinement + if prop == "TotalCores": + prop_val = str( prop_val ) + " Cores" + elif prop == "MaxSpeedMHz": + prop_val = "@ " + str( prop_val ) + "MHz" + elif prop == "CapacityMiB": + prop_val = str( prop_val ) + "MB" + elif prop == "CapacityBytes": + prop_val = str( int( prop_val / ( 2 ** 30 ) ) ) + "GB" + elif prop == "SpeedGbps": + prop_val = str( prop_val ) + "Gbps" + elif prop == "SupportedDeviceProtocols": + prop_val = prop_val.join( "/" ) + " Controller" + description_str = description_str + " " + prop_val + else: + # Some properties will have a default if not found + if prop == "MediaType": + description_str = description_str + " Drive" + elif prop == "SupportedDeviceProtocols": + description_str = description_str + " Storage Controller" + catalog["Description"] = re.sub( " +", " ", description_str ).strip() + else: + catalog["Description"] = None inventory.append( catalog ) -def print_system_inventory( inventory_list, details = False ): +def print_system_inventory( inventory_list, details = False, skip_absent = False ): """ Prints the system inventory list into a table Args: inventory_list: The inventory list to print details: True to print all of the detailed info + skip_absent: True to skip printing absent components """ inventory_line_format = " {:35s} | {}" @@ -256,7 +268,8 @@ def print_system_inventory( inventory_list, details = False ): # Go through each component and prints its info for item in chassis[inv_type]: if item["State"] == "Absent": - print( inventory_line_format_empty.format( item["Label"][:35] ) ) + if not skip_absent: + print( inventory_line_format_empty.format( item["Label"][:35] ) ) else: print( inventory_line_format.format( item["Label"][:35], item["Description"] ) ) diff --git a/scripts/rf_sys_inventory b/scripts/rf_sys_inventory index 9c184f6..1e33958 100644 --- a/scripts/rf_sys_inventory +++ b/scripts/rf_sys_inventory @@ -24,6 +24,7 @@ argget.add_argument( "--user", "-u", type = str, required = True, help = "The us argget.add_argument( "--password", "-p", type = str, required = True, help = "The password for authentication" ) argget.add_argument( "--rhost", "-r", type = str, required = True, help = "The address of the Redfish service (with scheme)" ) 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" ) args = argget.parse_args() # Set up the Redfish object @@ -33,7 +34,7 @@ redfish_obj.login( auth = "session" ) try: # Get and print the system inventory inventory = redfish_utilities.get_system_inventory( redfish_obj ) - redfish_utilities.print_system_inventory( inventory, details = args.details ) + redfish_utilities.print_system_inventory( inventory, details = args.details, skip_absent = args.noabsent ) finally: # Log out redfish_obj.logout() From d86f77a68972752607fde7530b58e63c2e1c3694 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Tue, 25 Jun 2019 08:52:59 -0400 Subject: [PATCH 7/9] Removed unnecessary setting of default prefix --- scripts/rf_boot_override | 2 +- scripts/rf_power_reset | 2 +- scripts/rf_sensor_list | 2 +- scripts/rf_sys_inventory | 2 +- scripts/rf_update | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/rf_boot_override b/scripts/rf_boot_override index d397b70..4943329 100644 --- a/scripts/rf_boot_override +++ b/scripts/rf_boot_override @@ -28,7 +28,7 @@ argget.add_argument( "--reset", "-reset", action = "store_true", help = "Signifi args = argget.parse_args() # Set up the Redfish object -redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password, default_prefix = "/redfish/v1" ) +redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password ) redfish_obj.login( auth = "session" ) try: diff --git a/scripts/rf_power_reset b/scripts/rf_power_reset index cf35a96..917ddbd 100644 --- a/scripts/rf_power_reset +++ b/scripts/rf_power_reset @@ -25,7 +25,7 @@ argget.add_argument( "--type", "-t", type = str, help = "The type of power/reset args = argget.parse_args() # Set up the Redfish object -redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password, default_prefix = "/redfish/v1" ) +redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password ) redfish_obj.login( auth = "session" ) try: diff --git a/scripts/rf_sensor_list b/scripts/rf_sensor_list index 1a1544b..dec881c 100644 --- a/scripts/rf_sensor_list +++ b/scripts/rf_sensor_list @@ -23,7 +23,7 @@ argget.add_argument( "--rhost", "-r", type = str, required = True, help = "The a args = argget.parse_args() # Set up the Redfish object -redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password, default_prefix = "/redfish/v1" ) +redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password ) redfish_obj.login( auth = "session" ) try: diff --git a/scripts/rf_sys_inventory b/scripts/rf_sys_inventory index 1e33958..a4493c0 100644 --- a/scripts/rf_sys_inventory +++ b/scripts/rf_sys_inventory @@ -28,7 +28,7 @@ argget.add_argument( "--noabsent", "-noabsent", action = "store_true", help = "I args = argget.parse_args() # Set up the Redfish object -redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password, default_prefix = "/redfish/v1" ) +redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password ) redfish_obj.login( auth = "session" ) try: diff --git a/scripts/rf_update b/scripts/rf_update index adcd9a6..71a3ee8 100644 --- a/scripts/rf_update +++ b/scripts/rf_update @@ -70,7 +70,7 @@ argget.add_argument( "--target", "-t", type = str, help = "The target resource t args = argget.parse_args() # Set up the Redfish object -redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password, default_prefix = "/redfish/v1" ) +redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password ) redfish_obj.login( auth = "session" ) # If the file is local, spin up a web server to host the image From b2d2b6f2b596c1d3f95294241e1b183b4dd47425 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Tue, 25 Jun 2019 08:53:09 -0400 Subject: [PATCH 8/9] Typo in comment block --- redfish_utilities/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redfish_utilities/inventory.py b/redfish_utilities/inventory.py index b77f664..b5afb67 100644 --- a/redfish_utilities/inventory.py +++ b/redfish_utilities/inventory.py @@ -1,5 +1,5 @@ """ -Sensors Module +Inventory Module File : inventory.py From 9676b2196e4bf5f85886a3fb4192831fda8b4849 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Tue, 25 Jun 2019 16:27:59 -0400 Subject: [PATCH 9/9] Simplifications to description building --- redfish_utilities/inventory.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/redfish_utilities/inventory.py b/redfish_utilities/inventory.py index b5afb67..47f2017 100644 --- a/redfish_utilities/inventory.py +++ b/redfish_utilities/inventory.py @@ -7,8 +7,6 @@ Redfish service for an inventory of components """ -import re - def get_system_inventory( context ): """ Walks a Redfish service for system component information, such as drives, @@ -184,7 +182,8 @@ def catalog_resource( resource, inventory ): "SKU": resource.get( "SKU", None ), "AssetTag": resource.get( "AssetTag", None ), "Label": resource.get( location_prop, {} ).get( "PartLocation", {} ).get( "ServiceLabel", None ), - "State": resource.get( "Status", {} ).get( "State", None ) + "State": resource.get( "Status", {} ).get( "State", None ), + "Description": None } # If no label was found, build a default name @@ -237,9 +236,7 @@ def catalog_resource( resource, inventory ): description_str = description_str + " Drive" elif prop == "SupportedDeviceProtocols": description_str = description_str + " Storage Controller" - catalog["Description"] = re.sub( " +", " ", description_str ).strip() - else: - catalog["Description"] = None + catalog["Description"] = description_str.strip() inventory.append( catalog )