From e0d48720a5c9cc330dfb84754df5f365f182dadd Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Wed, 22 Jan 2025 14:27:04 -0500 Subject: [PATCH 1/8] Initial conversion of the use case checkers to be a standalone Signed-off-by: Mike Raineri --- .gitignore | 1 + account_management/account_management.py | 191 --------- account_management/test_conf.json | 5 - account_management/toolspath.py | 11 - one_time_boot/one_time_boot_check.py | 148 ------- one_time_boot/test_conf.json | 5 - one_time_boot/toolspath.py | 11 - power_control/power_control.py | 134 ------ power_control/test_conf.json | 5 - power_control/toolspath.py | 11 - power_thermal_info/power_thermal_test.py | 104 ----- power_thermal_info/test_conf.json | 5 - power_thermal_info/toolspath.py | 11 - redfish_use_case_checkers/__init__.py | 0 .../account_management.py | 319 ++++++++++++++ redfish_use_case_checkers/boot_override.py | 404 ++++++++++++++++++ redfish_use_case_checkers/console_scripts.py | 114 +++++ redfish_use_case_checkers/logger.py | 70 +++ redfish_use_case_checkers/power_control.py | 233 ++++++++++ redfish_use_case_checkers/redfish_logo.py | 321 ++++++++++++++ redfish_use_case_checkers/report.py | 125 ++++++ .../system_under_test.py | 216 ++++++++++ requirements.txt | 1 + rf_use_case_checkers.py | 8 + 24 files changed, 1812 insertions(+), 641 deletions(-) delete mode 100644 account_management/account_management.py delete mode 100644 account_management/test_conf.json delete mode 100644 account_management/toolspath.py delete mode 100644 one_time_boot/one_time_boot_check.py delete mode 100644 one_time_boot/test_conf.json delete mode 100644 one_time_boot/toolspath.py delete mode 100644 power_control/power_control.py delete mode 100644 power_control/test_conf.json delete mode 100644 power_control/toolspath.py delete mode 100644 power_thermal_info/power_thermal_test.py delete mode 100644 power_thermal_info/test_conf.json delete mode 100644 power_thermal_info/toolspath.py create mode 100644 redfish_use_case_checkers/__init__.py create mode 100644 redfish_use_case_checkers/account_management.py create mode 100644 redfish_use_case_checkers/boot_override.py create mode 100644 redfish_use_case_checkers/console_scripts.py create mode 100644 redfish_use_case_checkers/logger.py create mode 100644 redfish_use_case_checkers/power_control.py create mode 100644 redfish_use_case_checkers/redfish_logo.py create mode 100644 redfish_use_case_checkers/report.py create mode 100644 redfish_use_case_checkers/system_under_test.py create mode 100644 rf_use_case_checkers.py diff --git a/.gitignore b/.gitignore index d02c70e..41df51c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ results.json +reports diff --git a/account_management/account_management.py b/account_management/account_management.py deleted file mode 100644 index 7454979..0000000 --- a/account_management/account_management.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright Notice: -# Copyright 2017-2019 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -""" -Account Management Usecase Test - -File : account_management.py - -Brief : This file contains the definitions and functionalities for performing - the usecase test for account management -""" - -import argparse -import datetime -import logging -import sys - -import redfish -import redfish_utilities - -import toolspath -from usecase.results import Results - -def verify_user( context, user_name, role = None, enabled = None ): - """ - Checks that a given user is in the user list with a certain role - - Args: - context: The Redfish client object with an open session - user_name: The name of the user to check - role: The role for the user - enabled: The enabled state for the user - - Returns: - True if a match is found, false otherwise - """ - user_list = redfish_utilities.get_users( context ) - for user in user_list: - if user["UserName"] == user_name: - if role is not None and user["RoleId"] != role: - return False - if enabled is not None and user["Enabled"] != enabled: - return False - return True - - return False - -if __name__ == "__main__": - - # Get the input arguments - argget = argparse.ArgumentParser( description = "Usecase checker for account management" ) - 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" ) - argget.add_argument( "--Secure", "-S", type = str, default = "Always", help = "When to use HTTPS (Always, IfSendingCredentials, IfLoginOrAuthenticatedApi, Never)" ) - argget.add_argument( "--directory", "-d", type = str, default = None, help = "Output directory for results.json" ) - argget.add_argument( "--debug", action = "store_true", help = "Creates debug file showing HTTP traces and exceptions" ) - args = argget.parse_args() - - if args.debug: - log_file = "account_management-{}.log".format( datetime.datetime.now().strftime( "%Y-%m-%d-%H%M%S" ) ) - log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - logger = redfish.redfish_logger( log_file, log_format, logging.DEBUG ) - logger.info( "account_management Trace" ) - - # Set up the Redfish object - base_url = "https://" + args.rhost - if args.Secure == "Never": - base_url = "http://" + args.rhost - with redfish.redfish_client( base_url = base_url, username = args.user, password = args.password ) as redfish_obj: - # Create the results object - service_root = redfish_obj.get( "/redfish/v1/" ) - results = Results( "Account Management", service_root.dict ) - if args.directory is not None: - results.set_output_dir( args.directory ) - - # Get the list of current users - try: - usernames = [] - user_list = redfish_utilities.get_users( redfish_obj ) - for user in user_list: - usernames.append( user["UserName"] ) - user_count = len( user_list ) - if user_count == 0: - results.update_test_results( "User Count", 1, "No users were found." ) - else: - results.update_test_results( "User Count", 0, None ) - except Exception as err: - results.update_test_results( "User Count", 1, "Failed to get user list ({}).".format( err ) ) - - # Determine a user name for testing - for x in range( 1000 ): - test_username = "testuser" + str( x ) - if test_username not in usernames: - break - - # Create a new user - user_added = False - last_error = "" - test_passwords = [ "hUPgd9Z4", "7jIl3dn!kd0Fql", "m5Ljed3!n0olvdS*m0kmWER15!" ] - print( "Creating new user '{}'".format( test_username ) ) - for x in range( 3 ): - # Try different passwords in case there are password requirements that we cannot detect - try: - test_password = test_passwords[x] - redfish_utilities.add_user( redfish_obj, test_username, test_password, "Administrator" ) - user_added = True - break - except Exception as err: - last_error = err - if user_added: - results.update_test_results( "Add User", 0, None ) - else: - results.update_test_results( "Add User", 1, "Failed to add user '{}' ({}).".format( test_username, last_error ) ) - - # Only run the remaining tests if the user was added successfully - if user_added: - # Get the list of current users to verify the new user was added - if verify_user( redfish_obj, test_username, role = "Administrator" ): - results.update_test_results( "Add User", 0, None ) - else: - results.update_test_results( "Add User", 1, "Failed to find user '{}' with the role 'Administrator'.".format( test_username ) ) - - # Check if the user needs to be enabled - try: - if verify_user( redfish_obj, test_username, enabled = False ): - redfish_utilities.modify_user( redfish_obj, test_username, new_enabled = True ) - if verify_user( redfish_obj, test_username, enabled = True ): - results.update_test_results( "Enable User", 0, None ) - else: - results.update_test_results( "Enable User", 1, "User '{}' not enabled after successful PATCH.".format( test_username ) ) - else: - results.update_test_results( "Enable User", 0, "User '{}' already enabled by the service.".format( test_username ), skipped = True ) - except Exception as err: - results.update_test_results( "Enable User", 1, "Failed to enable user '{}' ({}).".format( test_username, err ) ) - - # Log in with the new user - print( "Logging in as '{}'".format( test_username ) ) - test_obj = redfish.redfish_client( base_url = base_url, username = test_username, password = test_password ) - try: - test_obj.login( auth = "session" ) - test_list = redfish_utilities.get_users( test_obj ) - results.update_test_results( "Credential Check", 0, None ) - except: - results.update_test_results( "Credential Check", 1, "Failed to login with user '{}'.".format( test_username ) ) - finally: - test_obj.logout() - - # Log in with the new user, but with bad credentials - print( "Logging in as '{}', but with the wrong password".format( test_username ) ) - test_obj = redfish.redfish_client( base_url = base_url, username = test_username, password = test_password + "ExtraStuff" ) - try: - test_obj.login( auth = "session" ) - test_list = redfish_utilities.get_users( test_obj ) - results.update_test_results( "Credential Check", 1, "Login with user '{}' when using invalid credentials.".format( test_username ) ) - except: - results.update_test_results( "Credential Check", 0, None ) - finally: - test_obj.logout() - - # Change the role of the user - test_roles = [ "ReadOnly", "Operator", "Administrator" ] - for role in test_roles: - try: - print( "Setting user '{}' to role '{}'".format( test_username, role ) ) - redfish_utilities.modify_user( redfish_obj, test_username, new_role = role ) - results.update_test_results( "Change Role", 0, None ) - if verify_user( redfish_obj, test_username, role = role ): - results.update_test_results( "Change Role", 0, None ) - else: - results.update_test_results( "Change Role", 1, "Failed to find user '{}' with the role '{}'.".format( test_username, role ) ) - except Exception as err: - results.update_test_results( "Change Role", 1, "Failed to set user '{}' to '{}' ({}).".format( test_username, role, err ) ) - - # Delete the user - try: - print( "Deleting user '{}'".format( test_username ) ) - redfish_utilities.delete_user( redfish_obj, test_username ) - results.update_test_results( "Delete User", 0, None ) - if verify_user( redfish_obj, test_username ): - results.update_test_results( "Delete User", 1, "User '{}' is still in the user list.".format( test_username ) ) - else: - results.update_test_results( "Delete User", 0, None ) - except Exception as err: - results.update_test_results( "Delete User", 1, "Failed to delete user '{}' ({}).".format( test_username, err ) ) - - # Save the results - results.write_results() - - sys.exit( results.get_return_code() ) diff --git a/account_management/test_conf.json b/account_management/test_conf.json deleted file mode 100644 index ea51041..0000000 --- a/account_management/test_conf.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "test": { - "command": "$interpreter account_management.py -r $target_system -u $username -p $password -S $https -d $output_subdir" - } -} diff --git a/account_management/toolspath.py b/account_management/toolspath.py deleted file mode 100644 index 28466cd..0000000 --- a/account_management/toolspath.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright Notice: -# Copyright 2017-2019 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -import os -import sys - -cur_dir = os.path.dirname( __file__ ) -path_dir = os.path.abspath( os.path.join( cur_dir, os.path.pardir ) ) -if path_dir not in sys.path: - sys.path.insert( 0, path_dir ) diff --git a/one_time_boot/one_time_boot_check.py b/one_time_boot/one_time_boot_check.py deleted file mode 100644 index faded29..0000000 --- a/one_time_boot/one_time_boot_check.py +++ /dev/null @@ -1,148 +0,0 @@ -#! /usr/bin/python3 -# Copyright Notice: -# Copyright 2019 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -""" -One Time Boot Usecase Test - -File : one_time_boot_check.py - -Brief : This file contains the definitions and functionalities for performing - the usecase test for performing a one time boot -""" - -import argparse -import datetime -import logging -import sys -import time - -import redfish -import redfish_utilities - -import toolspath -from usecase.results import Results - -if __name__ == '__main__': - - # Get the input arguments - argget = argparse.ArgumentParser( description = "Usecase checker for one time boot" ) - 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" ) - argget.add_argument( "--Secure", "-S", type = str, default = "Always", help = "When to use HTTPS (Always, IfSendingCredentials, IfLoginOrAuthenticatedApi, Never)" ) - argget.add_argument( "--directory", "-d", type = str, default = None, help = "Output directory for results.json" ) - argget.add_argument( "--debug", action = "store_true", help = "Creates debug file showing HTTP traces and exceptions" ) - args = argget.parse_args() - - if args.debug: - log_file = "one_time_boot_check-{}.log".format( datetime.datetime.now().strftime( "%Y-%m-%d-%H%M%S" ) ) - log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - logger = redfish.redfish_logger( log_file, log_format, logging.DEBUG ) - logger.info( "one_time_boot_check Trace" ) - - # Set up the Redfish object - base_url = "https://" + args.rhost - if args.Secure == "Never": - base_url = "http://" + args.rhost - with redfish.redfish_client( base_url = base_url, username = args.user, password = args.password ) as redfish_obj: - # Create the results object - service_root = redfish_obj.get( "/redfish/v1/" ) - results = Results( "One Time Boot", service_root.dict ) - if args.directory is not None: - results.set_output_dir( args.directory ) - - # Get the available systems - test_systems = [] - system_col = redfish_obj.get( service_root.dict["Systems"]["@odata.id"] ) - for member in system_col.dict["Members"]: - system = redfish_obj.get( member["@odata.id"] ) - test_systems.append( system.dict["Id"] ) - - # Check that the system list is not empty - system_count = len( test_systems ) - print( "Found {} system instances".format( system_count ) ) - if system_count == 0: - results.update_test_results( "System Count", 1, "No system instances were found" ) - else: - results.update_test_results( "System Count", 0, None ) - - # Perform a test on each system found - for system in test_systems: - # See if PXE or USB are allowable - test_path = None - boot_obj = redfish_utilities.get_system_boot( redfish_obj, system ) - if "BootSourceOverrideTarget@Redfish.AllowableValues" in boot_obj: - if "Pxe" in boot_obj["BootSourceOverrideTarget@Redfish.AllowableValues"]: - test_path = "Pxe" - elif "Usb" in boot_obj["BootSourceOverrideTarget@Redfish.AllowableValues"]: - test_path = "Usb" - else: - test_path = "Pxe" - if test_path is None: - print( "{} does not support PXE or USB boot override".format( system ) ) - results.update_test_results( "Boot Check", 0, "{} does not allow for PXE or USB boot override.".format( system ), skipped = True ) - results.update_test_results( "Continuous Boot Set", 0, "{} does not allow for PXE or USB boot override.".format( system ), skipped = True ) - results.update_test_results( "Boot Set", 0, "{} does not allow for PXE or USB boot override.".format( system ), skipped = True ) - results.update_test_results( "Boot Verify", 0, "{} does not allow for PXE or USB boot override.".format( system ), skipped = True ) - continue - results.update_test_results( "Boot Check", 0, None ) - - # Check that Continuous is allowed to be applied to the boot override settings - print( "Setting {} to boot continuously from {}".format( system, test_path ) ) - try: - redfish_utilities.set_system_boot( redfish_obj, system_id = system, ov_target = test_path, ov_enabled = "Continuous" ) - boot_obj = redfish_utilities.get_system_boot( redfish_obj, system ) - if boot_obj["BootSourceOverrideTarget"] != test_path and boot_obj["BootSourceOverrideEnabled"] != "Continuous": - raise ValueError( "Boot object was not modified after PATCH" ) - else: - results.update_test_results( "Continuous Boot Set", 0, None ) - except Exception as err: - results.update_test_results( "Continuous Boot Set", 1, "Failed to set {} to continuously boot from {} ({}).".format( system, test_path, err ) ) - - # Set the boot object and verify the setting was applied - print( "Setting {} to boot from {}".format( system, test_path ) ) - try: - redfish_utilities.set_system_boot( redfish_obj, system_id = system, ov_target = test_path, ov_enabled = "Once" ) - boot_obj = redfish_utilities.get_system_boot( redfish_obj, system ) - if boot_obj["BootSourceOverrideTarget"] != test_path and boot_obj["BootSourceOverrideEnabled"] != "Once": - raise ValueError( "Boot object was not modified after PATCH" ) - else: - results.update_test_results( "Boot Set", 0, None ) - - # Reset the system - print( "Resetting {}".format( system ) ) - try: - response = redfish_utilities.system_reset( redfish_obj, system ) - response = redfish_utilities.poll_task_monitor( redfish_obj, response ) - redfish_utilities.verify_response( response ) - - # Monitor the system to go back to None - print( "Monitoring boot progress for {}...".format( system ) ) - for i in range( 0, 300 ): - time.sleep( 1 ) - boot_obj = redfish_utilities.get_system_boot( redfish_obj, system ) - if boot_obj["BootSourceOverrideEnabled"] == "Disabled": - break - if boot_obj["BootSourceOverrideEnabled"] == "Disabled": - print( "{} booted from {}!".format( system, test_path ) ) - results.update_test_results( "Boot Verify", 0, None ) - else: - raise ValueError( "{} did not reset back to 'Disabled'".format( system ) ) - except Exception as err: - results.update_test_results( "Boot Verify", 1, "{} failed to boot from {}.".format( system, test_path ) ) - except Exception as err: - results.update_test_results( "Boot Set", 1, "Failed to set {} to boot from {} ({}).".format( system, test_path, err ) ) - results.update_test_results( "Boot Verify", 0, "Boot setting not applied.", skipped = True ) - - # Cleanup (should be clean already if everything passed) - try: - redfish_utilities.set_system_boot( redfish_obj, system_id = system, ov_target = "None", ov_enabled = "Disabled" ) - except: - pass - - # Save the results - results.write_results() - - sys.exit( results.get_return_code() ) diff --git a/one_time_boot/test_conf.json b/one_time_boot/test_conf.json deleted file mode 100644 index 20854a9..0000000 --- a/one_time_boot/test_conf.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "test": { - "command": "$interpreter one_time_boot_check.py -r $target_system -u $username -p $password -S $https -d $output_subdir" - } -} diff --git a/one_time_boot/toolspath.py b/one_time_boot/toolspath.py deleted file mode 100644 index 1fb13da..0000000 --- a/one_time_boot/toolspath.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright Notice: -# Copyright 2019 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -import os -import sys - -cur_dir = os.path.dirname( __file__ ) -path_dir = os.path.abspath( os.path.join( cur_dir, os.path.pardir ) ) -if path_dir not in sys.path: - sys.path.insert( 0, path_dir ) diff --git a/power_control/power_control.py b/power_control/power_control.py deleted file mode 100644 index 888ad33..0000000 --- a/power_control/power_control.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright Notice: -# Copyright 2017-2019 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -""" -Power Control Usecase Test - -File : power_control.py - -Brief : This file contains the definitions and functionalities for performing - the usecase test for performing reset and power operations -""" - -import argparse -import datetime -import logging -import sys -import time - -import redfish -import redfish_utilities - -import toolspath -from usecase.results import Results - -if __name__ == '__main__': - - # Get the input arguments - argget = argparse.ArgumentParser( description = "Usecase checker for power and reset operations" ) - 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" ) - argget.add_argument( "--Secure", "-S", type = str, default = "Always", help = "When to use HTTPS (Always, IfSendingCredentials, IfLoginOrAuthenticatedApi, Never)" ) - argget.add_argument( "--directory", "-d", type = str, default = None, help = "Output directory for results.json" ) - argget.add_argument( "--timeout", "-t", type = int, default = 10, help = "Length of each timeout after reset" ) - argget.add_argument( "--debug", action = "store_true", help = "Creates debug file showing HTTP traces and exceptions" ) - args = argget.parse_args() - - if args.debug: - log_file = "power_control-{}.log".format( datetime.datetime.now().strftime( "%Y-%m-%d-%H%M%S" ) ) - log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - logger = redfish.redfish_logger( log_file, log_format, logging.DEBUG ) - logger.info( "power_control Trace" ) - - # Set up the Redfish object - base_url = "https://" + args.rhost - if args.Secure == "Never": - base_url = "http://" + args.rhost - with redfish.redfish_client( base_url = base_url, username = args.user, password = args.password ) as redfish_obj: - # Create the results object - service_root = redfish_obj.get( "/redfish/v1/" ) - results = Results( "Power Control", service_root.dict ) - if args.directory is not None: - results.set_output_dir( args.directory ) - - # Get the available systems - test_systems = [] - system_col = redfish_obj.get( service_root.dict["Systems"]["@odata.id"] ) - for member in system_col.dict["Members"]: - system = redfish_obj.get( member["@odata.id"] ) - test_systems.append( { "Id": system.dict["Id"], "URI": member["@odata.id"] } ) - - # Check that the system list is not empty - system_count = len( test_systems ) - print( "Found {} system instances".format( system_count ) ) - if system_count == 0: - results.update_test_results( "System Count", 1, "No system instances were found." ) - else: - results.update_test_results( "System Count", 0, None ) - - # Perform a test on each system found - for system in test_systems: - # Check what types of resets are supported - try: - reset_types = None - reset_uri, reset_params = redfish_utilities.get_system_reset_info( redfish_obj, system["Id"] ) - except Exception as err: - results.update_test_results( "Reset Type Check", 1, "Could not get reset info for {} ({}).".format( system["Id"], err ) ) - continue - - for param in reset_params: - if param["Name"] == "ResetType": - reset_types = param["AllowableValues"] - if reset_types is None: - results.update_test_results( "Reset Type Check", 1, "{} is not advertising any allowable resets.".format( system["Id"] ) ) - continue - results.update_test_results( "Reset Type Check", 0, None ) - - # Reset the system - for reset_type in reset_types: - if reset_type == "Nmi": - # NMI could fail depending on the state of the system; no real reason to test this at this time - continue - print( "Resetting {} using {}".format( system["Id"], reset_type ) ) - try: - response = redfish_utilities.system_reset( redfish_obj, system["Id"], reset_type ) - response = redfish_utilities.poll_task_monitor( redfish_obj, response ) - redfish_utilities.verify_response( response ) - results.update_test_results( "Reset Performed", 0, None ) - except Exception as err: - results.update_test_results( "Reset Performed", 1, "Failed to reset {} using {} ({})".format( system["Id"], reset_type, err ) ) - continue - - # Allow some time before checking the power state - # We also might skip the PowerState check and want to allow for the system to settle before performing another reset - time.sleep( args.timeout ) - - # Check the power state to ensure it's in the proper state - exp_power_state = "On" - if reset_type == "ForceOff" or reset_type == "GracefulShutdown": - exp_power_state = "Off" - if reset_type == "PushPowerButton": - # Depending on the system design, pushing the button can have different outcomes with regards to the power state - continue - print( "Monitoring power state for {}...".format( system["Id"] ) ) - power_state = None - for i in range( 0, 10 ): - system_info = redfish_obj.get( system["URI"] ) - power_state = system_info.dict.get( "PowerState" ) - if power_state is None or power_state == exp_power_state: - break - time.sleep( 5 ) - if power_state is not None: - if power_state != exp_power_state: - results.update_test_results( "Power State Check", 1, "{} was not in the {} state after using {} as the reset type.".format( system["Id"], exp_power_state, reset_type ) ) - else: - results.update_test_results( "Power State Check", 0, None ) - else: - results.update_test_results( "Power State Check", 0, "{} does not contain the PowerState property.".format( system["Id"] ), skipped = True ) - - # Save the results - results.write_results() - - sys.exit( results.get_return_code() ) diff --git a/power_control/test_conf.json b/power_control/test_conf.json deleted file mode 100644 index 7ec1f7b..0000000 --- a/power_control/test_conf.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "test": { - "command": "$interpreter power_control.py -r $target_system -u $username -p $password -S $https -d $output_subdir" - } -} diff --git a/power_control/toolspath.py b/power_control/toolspath.py deleted file mode 100644 index 28466cd..0000000 --- a/power_control/toolspath.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright Notice: -# Copyright 2017-2019 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -import os -import sys - -cur_dir = os.path.dirname( __file__ ) -path_dir = os.path.abspath( os.path.join( cur_dir, os.path.pardir ) ) -if path_dir not in sys.path: - sys.path.insert( 0, path_dir ) diff --git a/power_thermal_info/power_thermal_test.py b/power_thermal_info/power_thermal_test.py deleted file mode 100644 index 448b223..0000000 --- a/power_thermal_info/power_thermal_test.py +++ /dev/null @@ -1,104 +0,0 @@ -#! /usr/bin/python3 -# Copyright Notice: -# Copyright 2019 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -""" -Power-Thermal Usecase Test - -File : power_thermal_test.py - -Brief : This file contains the definitions and functionalities for performing - the usecase test for retrieving power and thermal info -""" - -import argparse -import datetime -import logging -import sys - -import redfish -import redfish_utilities - -import toolspath -from usecase.results import Results - -if __name__ == '__main__': - - # Get the input arguments - argget = argparse.ArgumentParser( description = "Usecase checker for power/thermal info" ) - 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" ) - argget.add_argument( "--Secure", "-S", type = str, default = "Always", help = "When to use HTTPS (Always, IfSendingCredentials, IfLoginOrAuthenticatedApi, Never)" ) - argget.add_argument( "--directory", "-d", type = str, default = None, help = "Output directory for results.json" ) - argget.add_argument( "--debug", action = "store_true", help = "Creates debug file showing HTTP traces and exceptions" ) - args = argget.parse_args() - - if args.debug: - log_file = "power_thermal_test-{}.log".format( datetime.datetime.now().strftime( "%Y-%m-%d-%H%M%S" ) ) - log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - logger = redfish.redfish_logger( log_file, log_format, logging.DEBUG ) - logger.info( "power_thermal_test Trace" ) - - # Set up the Redfish object - base_url = "https://" + args.rhost - if args.Secure == "Never": - base_url = "http://" + args.rhost - with redfish.redfish_client( base_url = base_url, username = args.user, password = args.password ) as redfish_obj: - # Create the results object - service_root = redfish_obj.get( "/redfish/v1/" ) - results = Results( "Power/Thermal Info", service_root.dict ) - if args.directory is not None: - results.set_output_dir( args.directory ) - - # Fetch the sensors - sensors = None - try: - sensors = redfish_utilities.get_sensors( redfish_obj ) - except Exception as err: - results.update_test_results( "Chassis Count", 1, "Failed to collect sensor information ({}).".format( err ) ) - - # Exit early if nothing could be returned - if sensors is None: - results.write_results() - sys.exit( results.get_return_code() ) - - # Print the data received - redfish_utilities.print_sensors( sensors ) - - # Test 1: Check that the chassis list is not empty - chassis_count = len( sensors ) - print( "Found {} chassis instances".format( chassis_count ) ) - if chassis_count == 0: - results.update_test_results( "Chassis Count", 1, "No chassis instances were found." ) - else: - results.update_test_results( "Chassis Count", 0, None ) - - # Test 2: Check that each chassis has at least one sensor - for chassis in sensors: - sensor_count = len( chassis["Readings"] ) - print( "Found {} sensors in Chassis '{}'".format( sensor_count, chassis["ChassisName"] ) ) - if sensor_count == 0: - results.update_test_results( "Sensor Count", 1, "No sensors were found in Chassis '{}'.".format( chassis["ChassisName"] ) ) - else: - results.update_test_results( "Sensor Count", 0, None ) - - # Test 3: Check that all sensors not "Enabled" don't have a bogus reading - print( "Testing sensor readings..." ) - for chassis in sensors: - for reading in chassis["Readings"]: - if reading["State"] is not None and reading["Reading"] is not None: - # Both State and Reading are populated; perform the test - if reading["State"] != "Enabled" and reading["Reading"] != reading["State"]: - # When State is not Enabled, Reading is supposed to be a copy of State - # The only time this is not true is if there is a bogus reading, such as reporting "0V" when a device is absent - results.update_test_results( "Sensor State", 1, "Sensor '{}' in chassis '{}' contains reading '{}', but is in state '{}'.".format( - reading["Name"], chassis["ChassisName"], reading["Reading"], reading["State"] ) ) - else: - results.update_test_results( "Sensor State", 0, None ) - - # Save the results - results.write_results() - - sys.exit( results.get_return_code() ) diff --git a/power_thermal_info/test_conf.json b/power_thermal_info/test_conf.json deleted file mode 100644 index b18269a..0000000 --- a/power_thermal_info/test_conf.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "test": { - "command": "$interpreter power_thermal_test.py -r $target_system -u $username -p $password -S $https -d $output_subdir" - } -} diff --git a/power_thermal_info/toolspath.py b/power_thermal_info/toolspath.py deleted file mode 100644 index 1fb13da..0000000 --- a/power_thermal_info/toolspath.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright Notice: -# Copyright 2019 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -import os -import sys - -cur_dir = os.path.dirname( __file__ ) -path_dir = os.path.abspath( os.path.join( cur_dir, os.path.pardir ) ) -if path_dir not in sys.path: - sys.path.insert( 0, path_dir ) diff --git a/redfish_use_case_checkers/__init__.py b/redfish_use_case_checkers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/redfish_use_case_checkers/account_management.py b/redfish_use_case_checkers/account_management.py new file mode 100644 index 0000000..a5152ad --- /dev/null +++ b/redfish_use_case_checkers/account_management.py @@ -0,0 +1,319 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +""" +Account Management Use Cases + +File : account_management.py + +Brief : This file contains the definitions and functionalities for testing + use cases for account management +""" + +import logging +import redfish +import redfish_utilities + +from redfish_use_case_checkers.system_under_test import SystemUnderTest +from redfish_use_case_checkers import logger + +CAT_NAME = "Account Management" +TEST_USER_COUNT = ("User Count", "Verifies the user list is not empty", "Locates the ManagerAccountCollection resource and performs GET on all members.") +TEST_ADD_USER = ("Add User", "Verifies that a user can be added", "Performs a POST operation on the ManagerAccountCollection resource. Performs a GET on the new ManagerAccount resource and verifies the new user matches the specified criteria.") +TEST_ENABLE_USER = ("Enable User", "Verifies that a user can be enabled", "Performs a PATCH operation on the ManagerAccount resource to enable the new user. Performs a GET on the ManagerAccount resource and verifies the user account was enabled.") +TEST_CREDENTIAL_CHECK = ("Credential Check", "Verifies the credentials of the new user are correctly enforced", "Creates a new Redfish session with the new user account. Attempts to read the members of the ManagerAccountCollection resource with the new session.") +TEST_CHANGE_ROLE = ("Change Role", "Verifies that user roles can be modified", "Performs PATCH operations on the ManagerAccount resource of the new account to change the role. Performs a GET on the ManagerAccount resource and verifies the role was changed as requested.") +TEST_DELETE_USER = ("Delete User", "Verifies that a user can be deleted", "Performs a DELETE operation on the ManagerAccount resource of the new account. Reads the members of the ManagerAccountCollection resource and verifies the user was deleted.") + +def use_cases(sut: SystemUnderTest): + """ + Performs the use cases for account management + + Args: + sut: The system under test + """ + + logger.log_use_case_category_header(CAT_NAME) + + # Set initial results + sut.add_results_category(CAT_NAME, [TEST_USER_COUNT, TEST_ADD_USER, TEST_ENABLE_USER, TEST_CREDENTIAL_CHECK, TEST_CHANGE_ROLE, TEST_DELETE_USER]) + + # Check that there is an account service + if "AccountService" not in sut.service_root: + msg = "Service does not contain an account service." + sut.add_test_result(CAT_NAME, TEST_USER_COUNT[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_ADD_USER[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_ENABLE_USER[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_CREDENTIAL_CHECK[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_CHANGE_ROLE[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_DELETE_USER[0], "", "SKIP", msg) + logger.log_use_case_category_footer(CAT_NAME) + return + + # Go through the test cases + test_username = acc_test_user_count(sut) + user_added, test_password = acc_test_add_user(sut, test_username) + acc_test_enable_user(sut, user_added, test_username) + acc_test_credential_check(sut, user_added, test_username, test_password) + acc_test_change_role(sut, user_added, test_username) + acc_test_delete_user(sut, user_added, test_username) + logger.log_use_case_category_footer(CAT_NAME) + +def verify_user(context, username, role=None, enabled=None): + """ + Checks that a given user is in the user list with a certain role + + Args: + context: The Redfish client object with an open session + username: The name of the user to check + role: The role for the user + enabled: The enabled state for the user + + Returns: + True if a match is found, false otherwise + """ + user_list = redfish_utilities.get_users(context) + for user in user_list: + if user["UserName"] == username: + if role is not None and user["RoleId"] != role: + return False + if enabled is not None and user["Enabled"] != enabled: + return False + return True + + return False + +def acc_test_user_count(sut: SystemUnderTest): + """ + Performs the user count test + + Args: + sut: The system under test + + Returns: + The username for testing + """ + + test_name = TEST_USER_COUNT[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + usernames = [] + operation = "Counting the members of the account collection" + logger.logger.info(operation) + + # Get the list of current users + try: + user_list = redfish_utilities.get_users(sut.session) + for user in user_list: + usernames.append(user["UserName"]) + user_count = len(user_list) + if user_count == 0: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "No users were found.") + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get the user list ({}).".format(err)) + + # Determine a username for testing + for x in range(1000): + test_username = "testuser" + str(x) + if test_username not in usernames: + break + + logger.log_use_case_test_footer(CAT_NAME, test_name) + return test_username + +def acc_test_add_user(sut: SystemUnderTest, username: str): + """ + Performs the add user test + + Args: + sut: The system under test + username: The username for testing + + Returns: + An indication if the test user was added + The password for testing + """ + + test_name = TEST_ADD_USER[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + user_added = False + test_passwords = ["hUPgd9Z4", "7jIl3dn!kd0Fql", "m5Ljed3!n0olvdS*m0kmWER15!"] + + # Create a new user + last_error = "" + operation = "Creating new user '{}' as 'Administrator'".format(username) + logger.logger.info(operation) + for x in range(3): + # Try different passwords in case there are password requirements that we cannot detect + try: + test_password = test_passwords[x] + redfish_utilities.add_user(sut.session, username, test_password, "Administrator") + user_added = True + break + except Exception as err: + last_error = err + if not user_added: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to create user '{}' ({}).".format(username, last_error)) + + # Get the list of current users to verify the new user was added + if verify_user(sut.session, username, role="Administrator"): + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + else: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to find user '{}' with the role 'Administrator' after successful POST.".format(username)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + return user_added, test_password + +def acc_test_enable_user(sut: SystemUnderTest, user_added: bool, username: str): + """ + Performs the enable user test + + Args: + sut: The system under test + user_added: Indicates if the test user was added + username: The username for testing + """ + + test_name = TEST_ENABLE_USER[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + + # Skip the test if the test user was not added + if not user_added: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "Failure of the '{}' test prevents performing this test.".format(TEST_ADD_USER[0])) + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Check if the user needs to be enabled + operation = "Enabling user '{}'".format(username) + logger.logger.info(operation) + try: + if verify_user(sut.session, username, enabled=False): + redfish_utilities.modify_user(sut.session, username, new_enabled=True) + if verify_user(sut.session, username, enabled=True): + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + else: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "User '{}' not enabled after successful PATCH.".format(username)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "User '{}' already enabled by the service.".format(username)) + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to enable user '{}' ({}).".format(username, err)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + +def acc_test_credential_check(sut: SystemUnderTest, user_added: bool, username: str, password: str): + """ + Performs the credential check test + + Args: + sut: The system under test + user_added: Indicates if the test user was added + username: The username for testing + password: The password for testing + """ + + test_name = TEST_CREDENTIAL_CHECK[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + + # Skip the test if the test user was not added + if not user_added: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "Failure of the '{}' test prevents performing this test.".format(TEST_ADD_USER[0])) + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Log in with the new user + operation = "Logging in as '{}' with the correct password".format(username) + logger.logger.info(operation) + test_obj = redfish.redfish_client(base_url=sut.rhost, username=username, password=password) + try: + test_obj.login(auth="session") + test_list = redfish_utilities.get_users(test_obj) + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to login with user '{}'.".format(username)) + finally: + test_obj.logout() + + # Log in with the new user, but with bad credentials + operation = "Logging in as '{}', but with the incorrect password".format(username) + logger.logger.info(operation) + test_obj = redfish.redfish_client(base_url=sut.rhost, username=username, password=password + "ExtraStuff") + try: + test_obj.login(auth="session") + test_list = redfish_utilities.get_users(test_obj) + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Successful login with user '{}' when using invalid credentials.".format(username)) + except: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + finally: + test_obj.logout() + + logger.log_use_case_test_footer(CAT_NAME, test_name) + +def acc_test_change_role(sut: SystemUnderTest, user_added: bool, username: str): + """ + Performs the change role test + + Args: + sut: The system under test + user_added: Indicates if the test user was added + username: The username for testing + """ + + test_name = TEST_CHANGE_ROLE[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + + # Skip the test if the test user was not added + if not user_added: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "Failure of the '{}' test prevents performing this test.".format(TEST_ADD_USER[0])) + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Change the role of the user + test_roles = ["ReadOnly", "Operator", "Administrator"] + for role in test_roles: + try: + operation = "Setting user '{}' to role '{}'".format(username, role) + logger.logger.info(operation) + redfish_utilities.modify_user(sut.session, username, new_role=role) + if verify_user( sut.session, username, role = role ): + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + else: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to find user '{}' with the role '{}' after successful PATCH.".format(username, role)) + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to set user '{}' to '{}' ({}).".format(username, role, err)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + +def acc_test_delete_user(sut: SystemUnderTest, user_added: bool, username: str): + """ + Performs the delete user test + + Args: + sut: The system under test + user_added: Indicates if the test user was added + username: The username for testing + """ + + test_name = TEST_DELETE_USER[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + + # Skip the test if the test user was not added + if not user_added: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "Failure of the '{}' test prevents performing this test.".format(TEST_ADD_USER[0])) + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Delete the user + try: + operation = "Deleting user '{}'".format(username) + logger.logger.info(operation) + redfish_utilities.delete_user(sut.session, username) + if verify_user(sut.session, username): + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "User '{}' is still in the user list after successful DELETE.".format(username)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to delete user '{}' ({}).".format(username, err)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) diff --git a/redfish_use_case_checkers/boot_override.py b/redfish_use_case_checkers/boot_override.py new file mode 100644 index 0000000..d1bff85 --- /dev/null +++ b/redfish_use_case_checkers/boot_override.py @@ -0,0 +1,404 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +""" +Boot Override Use Cases + +File : boot_override.py + +Brief : This file contains the definitions and functionalities for testing + use cases for boot override +""" + +import logging +import redfish +import redfish_utilities +import time + +from redfish_use_case_checkers.system_under_test import SystemUnderTest +from redfish_use_case_checkers import logger + +CAT_NAME = "Boot Override" +TEST_SYSTEM_COUNT = ("System Count", "Verifies the system list is not empty", "Locates the ComputerSystemCollection resource and performs GET on all members.") +TEST_BOOT_OVERRIDE_CHECK = ("Boot Override Check", "Verifies that a system contains the boot override object", "Verifies the Boot property is present along with its boot override properties.") +TEST_CONTINUOUS_BOOT_SETTING = ("Continuous Boot Override", "Verifies the boot override supports the 'continuous' mode", "Performs a PATCH on the ComputerSystem resource to set the boot override to 'continuous' mode. Performs a GET on the ComputerSystem resource to verify the requested settings were applied.") +TEST_ONE_TIME_BOOT_SETTING = ("One-Time Boot Override", "Verifies the boot override supports the 'one-time' mode", "Performs a PATCH on the ComputerSystem resource to set the boot override to 'one-time' mode. Performs a GET on the ComputerSystem resource to verify the requested settings were applied.") +TEST_ONE_TIME_BOOT_CHECK = ("One-Time Boot Override Check", "Verifies the one-time boot override is performed", "Performs a POST to the Reset action on the ComputerSystem resource. Monitors the boot override mode transitions back to 'disabled' after the reset.") +TEST_DISABLE_BOOT_SETTING = ("Disable Boot Override", "Verifies the boot override can be disabled", "Performs a PATCH on the ComputerSystem resource to set the boot override to 'disabled' mode. Performs a GET on the ComputerSystem resource to verify the requested settings were applied.") + +def use_cases(sut: SystemUnderTest): + """ + Performs the use cases for boot override + + Args: + sut: The system under test + """ + + logger.log_use_case_category_header(CAT_NAME) + + # Set initial results + sut.add_results_category(CAT_NAME, [TEST_SYSTEM_COUNT, TEST_BOOT_OVERRIDE_CHECK, TEST_CONTINUOUS_BOOT_SETTING, TEST_ONE_TIME_BOOT_SETTING, TEST_ONE_TIME_BOOT_CHECK, TEST_DISABLE_BOOT_SETTING]) + + # Check that there is a system collection + if "Systems" not in sut.service_root: + msg = "Service does not contain a system collection." + sut.add_test_result(CAT_NAME, TEST_SYSTEM_COUNT[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_BOOT_OVERRIDE_CHECK[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_CONTINUOUS_BOOT_SETTING[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_ONE_TIME_BOOT_SETTING[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_ONE_TIME_BOOT_CHECK[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_DISABLE_BOOT_SETTING[0], "", "SKIP", msg) + logger.log_use_case_category_footer(CAT_NAME) + return + + # Go through the test cases + test_systems = boot_test_system_count(sut) + boot_params = boot_test_boot_check(sut, test_systems) + boot_test_continuous_boot_settings(sut, test_systems, boot_params) + one_time_systems = boot_test_one_time_boot_settings(sut, test_systems, boot_params) + boot_test_one_time_boot_check(sut, test_systems, one_time_systems) + boot_test_disable_boot_settings(sut, test_systems, boot_params) + + logger.log_use_case_category_footer(CAT_NAME) + +def boot_test_system_count(sut: SystemUnderTest): + """ + Performs the system count test + + Args: + sut: The system under test + + Returns: + An array of systems found + """ + + test_name = TEST_SYSTEM_COUNT[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + systems = [] + system_ids = [] + + # Get the list of systems + operation = "Counting the members of the system collection" + logger.logger.info(operation) + try: + system_ids = redfish_utilities.get_system_ids(sut.session) + if len(system_ids) == 0: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "No systems were found.") + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get the system list ({}).".format(err)) + + # Get each member of the system collection + for member in system_ids: + try: + operation = "Getting system '{}'".format(member) + logger.logger.info(operation) + system_resp = redfish_utilities.get_system(sut.session, member) + systems.append(system_resp.dict) + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get the system '{}' ({}).".format(member, err)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + return systems + +def boot_test_boot_check(sut: SystemUnderTest, systems: list): + """ + Performs the boot check test + + Args: + sut: The system under test + systems: The systems to test + + Returns: + An array of boot parameters for future tests + """ + + test_name = TEST_BOOT_OVERRIDE_CHECK[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + boot_override_params = [] + + # Skip the test if there are no systems + if len(systems) == 0: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "System collection is empty.") + logger.log_use_case_test_footer(CAT_NAME, test_name) + return boot_override_params + + # Try to get the boot override object for each system + for system in systems: + operation = "Checking the contents of the 'Boot' property in system '{}'".format(system["Id"]) + logger.logger.info(operation) + if "Boot" not in system: + # No boot object; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not contain the 'Boot' property.".format(system["Id"])) + boot_override_params.append(None) + continue + + if "BootSourceOverrideTarget" not in system["Boot"] and "BootSourceOverrideEnabled" not in system["Boot"]: + # No boot override properties; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not contain the boot override properties.".format(system["Id"])) + boot_override_params.append(None) + continue + + if "BootSourceOverrideTarget" in system["Boot"] and "BootSourceOverrideEnabled" in system["Boot"]: + # Both properties present; check for payload annotations to help users discover the supported values + if "BootSourceOverrideTarget@Redfish.AllowableValues" not in system["Boot"]: + sut.add_test_result(CAT_NAME, test_name, operation, "WARN", "System '{}' does not contain 'BootSourceOverrideTarget@Redfish.AllowableValues'.".format(system["Id"])) + elif "BootSourceOverrideEnabled@Redfish.AllowableValues" not in system["Boot"]: + sut.add_test_result(CAT_NAME, test_name, operation, "WARN", "System '{}' does not contain 'BootSourceOverrideEnabled@Redfish.AllowableValues'.".format(system["Id"])) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + # Cache the allowable boot parameters + # If the allowable values term is not present, assume it supports PXE, Continuous, and Once + boot_params = {} + boot_params["PXE"] = system["Boot"].get("BootSourceOverrideTarget@Redfish.AllowableValues", ["Pxe"]) + boot_params["USB"] = system["Boot"].get("BootSourceOverrideTarget@Redfish.AllowableValues", []) + boot_params["Continuous"] = system["Boot"].get("BootSourceOverrideEnabled@Redfish.AllowableValues", ["Continuous"]) + boot_params["Once"] = system["Boot"].get("BootSourceOverrideEnabled@Redfish.AllowableValues", ["Once"]) + boot_override_params.append(boot_params) + else: + # Only one of the properties is present; boot override is not useable without both + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "System '{}' contains 'BootSourceOverrideTarget' or 'BootSourceOverrideEnabled', but not both.".format(system["Id"])) + boot_override_params.append(None) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + return boot_override_params + +def boot_test_continuous_boot_settings(sut: SystemUnderTest, systems: list, boot_override_params: list): + """ + Performs the continuous boot settings test + + Args: + sut: The system under test + systems: The systems to test + boot_override_params: The boot override parameters for each system + """ + + test_name = TEST_CONTINUOUS_BOOT_SETTING[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + + # Skip the test if there are no systems + if len(systems) == 0: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "System collection is empty.") + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Try to get the boot override object for each system + for system, boot_param in zip(systems, boot_override_params): + operation = "Setting boot override to 'continuous' mode for system '{}'".format(system["Id"]) + logger.logger.info(operation) + if boot_param is None: + # No boot override; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support boot override.".format(system["Id"])) + continue + + if boot_param["Continuous"] is False: + # No continuous boot; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support 'continuous' boot override.".format(system["Id"])) + continue + + # Determine the boot path to test + boot_path = None + boot_mode = "Continuous" + if boot_param["PXE"]: + boot_path = "Pxe" + elif boot_param["USB"]: + boot_path = "Usb" + if boot_path is None: + # No PXE or USB; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support PXE or USB boot override.".format(system["Id"])) + continue + + # Set the boot override + try: + redfish_utilities.set_system_boot(sut.session, system_id=system["Id"], ov_target=boot_path, ov_enabled=boot_mode) + boot_obj = redfish_utilities.get_system_boot(sut.session, system["Id"]) + if boot_obj["BootSourceOverrideTarget"] != boot_path and boot_obj["BootSourceOverrideEnabled"] != boot_mode: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "'Boot' property contains '{}'/'{}' instead of '{}'/'{}' after PATCH operation.".format(system["Id"], boot_obj["BootSourceOverrideTarget"], boot_obj["BootSourceOverrideEnabled"], boot_path, boot_mode)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to set boot override for system '{}' to '{}'/'{}'.".format(system["Id"], boot_path, boot_mode)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + + +def boot_test_one_time_boot_settings(sut: SystemUnderTest, systems: list, boot_override_params: list): + """ + Performs the once-time boot override settings test + + Args: + sut: The system under test + systems: The systems to test + boot_override_params: The boot override parameters for each system + + Returns: + An array of the systems where the boot override was successfully set + """ + + test_name = TEST_ONE_TIME_BOOT_SETTING[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + one_time_systems = [] + + # Skip the test if there are no systems + if len(systems) == 0: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "System collection is empty.") + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Try to get the boot override object for each system + for system, boot_param in zip(systems, boot_override_params): + operation = "Setting boot override to 'one-time' mode for system '{}'".format(system["Id"]) + logger.logger.info(operation) + if boot_param is None: + # No boot override; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support boot override.".format(system["Id"])) + continue + + if boot_param["Once"] is False: + # No one-time boot; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support 'one-time' boot override.".format(system["Id"])) + continue + + # Determine the boot path to test + boot_path = None + boot_mode = "Once" + if boot_param["PXE"]: + boot_path = "Pxe" + elif boot_param["USB"]: + boot_path = "Usb" + if boot_path is None: + # No PXE or USB; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support PXE or USB boot override.".format(system["Id"])) + continue + + # Set the boot override + try: + redfish_utilities.set_system_boot(sut.session, system_id=system["Id"], ov_target=boot_path, ov_enabled=boot_mode) + boot_obj = redfish_utilities.get_system_boot(sut.session, system["Id"]) + if boot_obj["BootSourceOverrideTarget"] != boot_path and boot_obj["BootSourceOverrideEnabled"] != boot_mode: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "'Boot' property contains '{}'/'{}' instead of '{}'/'{}' after PATCH operation.".format(system["Id"], boot_obj["BootSourceOverrideTarget"], boot_obj["BootSourceOverrideEnabled"], boot_path, boot_mode)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + one_time_systems.append(system["Id"]) + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to set boot override for system '{}' to '{}'/'{}'.".format(system["Id"], boot_path, boot_mode)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + return one_time_systems + + +def boot_test_one_time_boot_check(sut: SystemUnderTest, systems: list, check_systems: list): + """ + Performs the one-time boot override check test + + Args: + sut: The system under test + systems: The systems to test + check_systems: Indicates which systems to check + """ + + test_name = TEST_ONE_TIME_BOOT_CHECK[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + + # Skip the test if there are no systems + if len(systems) == 0: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "System collection is empty.") + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Try to get the boot override object for each system + for system in systems: + operation = "Performing one-time boot for system '{}'".format(system["Id"]) + logger.logger.info(operation) + if system["Id"] not in check_systems: + # Did not set the boot override; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' was not set to 'one-time' in the previous test.".format(system["Id"])) + continue + + # Reset the system + operation = "Resetting system '{}'".format(system["Id"]) + logger.logger.info(operation) + reset_success = False + try: + response = redfish_utilities.system_reset(sut.session, system["Id"]) + response = redfish_utilities.poll_task_monitor(sut.session, response) + redfish_utilities.verify_response(response) + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + reset_success = True + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to reset system '{}' ({}).".format(system["Id"], err)) + + # Monitor the system to go back to None + if reset_success: + operation = "Monitoring the boot progress for system '{}'".format(system["Id"]) + logger.logger.info(operation) + try: + # Poll the boot object for up to 300 seconds + for i in range(0, 30): + logger.logger.debug("Monitoring check {}".format(i)) + time.sleep(10) + boot_obj = redfish_utilities.get_system_boot(sut.session, system["Id"]) + if boot_obj["BootSourceOverrideEnabled"] == "Disabled": + break + + logger.logger.info("Finished monitoring the boot progress for system '{}'; 'BootSourceOverrideEnabled' contains '{}'".format(system["Id"], boot_obj["BootSourceOverrideEnabled"])) + + # Log the results based on what the last reading was + if boot_obj["BootSourceOverrideEnabled"] == "Disabled": + logger.logger.info("System '{}' transitioned back to 'Disabled'".format(system["Id"])) + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + else: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Boot override for system '{}' did not transition back to 'Disabled' after reset.".format(system["Id"])) + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to monitor the boot progress for system '{}' ({}).".format(system["Id"], err)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' was not reset successfully.".format(system["Id"])) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + + +def boot_test_disable_boot_settings(sut: SystemUnderTest, systems: list, boot_override_params: list): + """ + Performs the disable boot override settings test + + Args: + sut: The system under test + systems: The systems to test + boot_override_params: The boot override parameters for each system + """ + + test_name = TEST_DISABLE_BOOT_SETTING[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + + # Skip the test if there are no systems + if len(systems) == 0: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "System collection is empty.") + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Try to get the boot override object for each system + for system, boot_param in zip(systems, boot_override_params): + operation = "Setting boot override to 'disabled' mode for system '{}'".format(system["Id"]) + logger.logger.info(operation) + if boot_param is None: + # No boot override; skip + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support boot override.".format(system["Id"])) + continue + + # Disable the boot override + boot_path = "None" + boot_mode = "Disabled" + try: + redfish_utilities.set_system_boot(sut.session, system_id=system["Id"], ov_target=boot_path, ov_enabled=boot_mode) + boot_obj = redfish_utilities.get_system_boot(sut.session, system["Id"]) + if boot_obj["BootSourceOverrideTarget"] != boot_path and boot_obj["BootSourceOverrideEnabled"] != boot_mode: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "'Boot' property contains '{}'/'{}' instead of '{}'/'{}' after PATCH operation.".format(system["Id"], boot_obj["BootSourceOverrideTarget"], boot_obj["BootSourceOverrideEnabled"], boot_path, boot_mode)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to set boot override for system '{}' to '{}'/'{}'.".format(system["Id"], boot_path, boot_mode)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) diff --git a/redfish_use_case_checkers/console_scripts.py b/redfish_use_case_checkers/console_scripts.py new file mode 100644 index 0000000..967972d --- /dev/null +++ b/redfish_use_case_checkers/console_scripts.py @@ -0,0 +1,114 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +""" +Redfish Use Case Checkers Console Scripts + +File : console_scripts.py + +Brief : This file contains the definitions and functionalities for invoking + the use case checkers. +""" + +import argparse +import colorama +import logging +import redfish +import sys +from datetime import datetime +from pathlib import Path + +from redfish_use_case_checkers.system_under_test import SystemUnderTest +from redfish_use_case_checkers import account_management +from redfish_use_case_checkers import boot_override +from redfish_use_case_checkers import logger +from redfish_use_case_checkers import power_control +from redfish_use_case_checkers import report + +tool_version = "1.0.9" + +def main(): + """ + Entry point for the use case checkers + """ + + # Get the input arguments + argget = argparse.ArgumentParser(description="Validate Redfish services against use cases") + argget.add_argument("--user", "-u", type=str, required=True, help="The username 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("--report-dir", type=str, default="reports", help="the directory for generated report files (default: 'reports')") + argget.add_argument("--debugging", action="store_true", help="Controls the verbosity of the debugging output; if not specified only INFO and higher are logged.") + args = argget.parse_args() + + # Create report directory if needed + report_dir = Path(args.report_dir) + if not report_dir.is_dir(): + report_dir.mkdir(parents=True) + + # Get the current time for report files + test_time = datetime.now() + + # Set the logging level + log_level = logging.INFO + if args.debugging: + log_level = logging.DEBUG + log_file = report_dir / "RedfishUseCaseCheckersDebug_{}.log".format(test_time.strftime("%m_%d_%Y_%H%M%S")) + log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + logger.logger = redfish.redfish_logger(log_file, log_format, log_level) + logger.logger.info("Redfish Use Case Checkers") + logger.logger.info("Version: {}".format(tool_version)) + logger.logger.info("System: {}".format(args.rhost)) + logger.logger.info("User: {}".format(args.user)) + print("Redfish Use Case Checkers, Version {}".format(tool_version)) + print() + + # Set up the system + sut = SystemUnderTest(args.rhost, args.user, args.password) + + # Run the tests + account_management.use_cases(sut) + power_control.use_cases(sut) + boot_override.use_cases(sut) + + # Log out + sut.logout() + + print_summary(sut) + results_file = report.html_report(sut, report_dir, test_time, tool_version) + print("HTML Report: {}".format(results_file)) + print("Debug Log: {}".format(log_file)) + +def summary_format(result, result_count): + """ + Returns a color-coded result format + + Args: + result: The type of result + result_count: The number of results for that type + """ + color_map = { + "PASS": (colorama.Fore.GREEN, colorama.Style.RESET_ALL), + "WARN": (colorama.Fore.YELLOW, colorama.Style.RESET_ALL), + "FAIL": (colorama.Fore.RED, colorama.Style.RESET_ALL), + } + start, end = ("", "") + if result_count: + start, end = color_map.get(result, ("", "")) + return start, result_count, end + +def print_summary(sut): + """ + Prints a stylized summary of the test results + + Args: + sut: The system under test + """ + colorama.init() + pass_start, passed, pass_end = summary_format("PASS", sut.pass_count) + warn_start, warned, warn_end = summary_format("WARN", sut.warn_count) + fail_start, failed, fail_end = summary_format("FAIL", sut.fail_count) + no_test_start, not_tested, no_test_end = (summary_format("SKIP", sut.skip_count)) + print("Summary - %sPASS: %s%s, %sWARN: %s%s, %sFAIL: %s%s, %sNOT TESTED: %s%s" % (pass_start, passed, pass_end, warn_start, warned, warn_end, fail_start, failed, fail_end, no_test_start, not_tested, no_test_end)) + colorama.deinit() diff --git a/redfish_use_case_checkers/logger.py b/redfish_use_case_checkers/logger.py new file mode 100644 index 0000000..dac1998 --- /dev/null +++ b/redfish_use_case_checkers/logger.py @@ -0,0 +1,70 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +""" +Redfish Use Case Checkers Logger + +File : logger.py + +Brief : This file contains the definitions and functionalities for handling + the debug log. +""" + +import logging + +logger = None +delimiter = "==================================================" + +def log_use_case_category_header(category_name): + """ + Logs the use case category header + + Args: + category_name: The name of the category + """ + logger.info(delimiter) + logger.info(delimiter) + logger.info("{} Use Cases (Start)".format(category_name)) + logger.info(delimiter) + logger.info(delimiter) + print("Performing {} use cases...".format(category_name)) + +def log_use_case_category_footer(category_name): + """ + Logs the use case category footer + + Args: + category_name: The name of the category + """ + logger.info(delimiter) + logger.info(delimiter) + logger.info("{} Use Cases (End)".format(category_name)) + logger.info(delimiter) + logger.info(delimiter) + print() + +def log_use_case_test_header(category_name, test_name): + """ + Logs the use case test header + + Args: + category_name: The name of the category + test_name: The name of the test + """ + logger.info(delimiter) + logger.info("{}: {} Test (Start)".format(category_name, test_name)) + logger.info(delimiter) + print("-- Running the {} test...".format(test_name)) + +def log_use_case_test_footer(category_name, test_name): + """ + Logs the use case test footer + + Args: + category_name: The name of the category + test_name: The name of the test + """ + logger.info(delimiter) + logger.info("{}: {} Test (End)".format(category_name, test_name)) + logger.info(delimiter) diff --git a/redfish_use_case_checkers/power_control.py b/redfish_use_case_checkers/power_control.py new file mode 100644 index 0000000..bbc48d6 --- /dev/null +++ b/redfish_use_case_checkers/power_control.py @@ -0,0 +1,233 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +""" +Power Control Use Cases + +File : power_control.py + +Brief : This file contains the definitions and functionalities for testing + use cases for power control +""" + +import logging +import redfish +import redfish_utilities +import time + +from redfish_use_case_checkers.system_under_test import SystemUnderTest +from redfish_use_case_checkers import logger + +CAT_NAME = "Power Control" +TEST_SYSTEM_COUNT = ("System Count", "Verifies the system list is not empty", "Locates the ComputerSystemCollection resource and performs GET on all members.") +TEST_RESET_TYPE = ("Reset Type", "Verifies the each system reports supported reset types", "Inspects the Reset action for each system for the supported reset types.") +TEST_RESET_OPERATION = ("Reset Operation", "Verifies that a system can be reset", "Performs a POST operation on the Reset action on the ComputerSystem resource. Performs a GET on the ComputerSystem resource and verifies it's in the desired power state.") + +def use_cases(sut: SystemUnderTest): + """ + Performs the use cases for boot override + + Args: + sut: The system under test + """ + + logger.log_use_case_category_header(CAT_NAME) + + # Set initial results + sut.add_results_category(CAT_NAME, [TEST_SYSTEM_COUNT, TEST_RESET_TYPE, TEST_RESET_OPERATION]) + + # Check that there is a system collection + if "Systems" not in sut.service_root: + msg = "Service does not contain a system collection." + sut.add_test_result(CAT_NAME, TEST_SYSTEM_COUNT[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_RESET_TYPE[0], "", "SKIP", msg) + sut.add_test_result(CAT_NAME, TEST_RESET_OPERATION[0], "", "SKIP", msg) + logger.log_use_case_category_footer(CAT_NAME) + return + + # Go through the test cases + test_systems = power_test_system_count(sut) + reset_capabilities = power_test_reset_type(sut, test_systems) + power_test_reset_operation(sut, test_systems, reset_capabilities) + + logger.log_use_case_category_footer(CAT_NAME) + +def power_test_system_count(sut: SystemUnderTest): + """ + Performs the system count test + + Args: + sut: The system under test + + Returns: + An array of systems found + """ + + test_name = TEST_SYSTEM_COUNT[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + systems = [] + system_ids = [] + + # Get the list of systems + operation = "Counting the members of the system collection" + logger.logger.info(operation) + try: + system_ids = redfish_utilities.get_system_ids(sut.session) + if len(system_ids) == 0: + sut.add_test_result(CAT_NAME,test_name, operation, "FAIL", "No systems were found.") + else: + sut.add_test_result(CAT_NAME,test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME,test_name, operation, "FAIL", "Failed to get the system list ({}).".format(err)) + + # Get each member of the system collection + for member in system_ids: + try: + operation = "Getting system '{}'".format(member) + logger.logger.info(operation) + system_resp = redfish_utilities.get_system(sut.session, member) + systems.append(system_resp.dict) + sut.add_test_result(CAT_NAME,test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME,test_name, operation, "FAIL", "Failed to get the system '{}' ({}).".format(member, err)) + + logger.log_use_case_test_footer(CAT_NAME,test_name) + return systems + + +def power_test_reset_type(sut: SystemUnderTest, systems: list): + """ + Performs the reset type test + + Args: + sut: The system under test + systems: The systems to test + + Returns: + A dictionary of reset capabilities for each system + """ + + test_name = TEST_RESET_TYPE[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + reset_capabilities = {} + + for system in systems: + operation = "Getting supported reset types for system '{}'".format(system["Id"]) + logger.logger.info(operation) + + # Skip if the system does not support the reset action + if "Actions" not in system: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support any actions.".format(system["Id"])) + continue + if "#ComputerSystem.Reset" not in system["Actions"]: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support the 'Reset' action.".format(system["Id"])) + continue + + # Get the reset types + try: + reset_types = None + reset_uri, reset_params = redfish_utilities.get_system_reset_info(sut.session, system["Id"]) + for param in reset_params: + if param["Name"] == "ResetType": + reset_types = param["AllowableValues"] + reset_capabilities[system["Id"]] = reset_types + if reset_types is None: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "System '{}' does not report supported reset types.".format(system["Id"])) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get the reset types supported for system '{}' ({}).".format(system["Id"], err)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + return reset_capabilities + +def power_test_reset_operation(sut: SystemUnderTest, systems: list, reset_capabilities: dict): + """ + Performs the reset operation test + + Args: + sut: The system under test + systems: The systems to test + reset_capabilities: The reset capabilities for each system + """ + + test_name = TEST_RESET_OPERATION[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + reset_types = ["On", "ForceOn", "ForceOff", "ForceRestart", "PowerCycle"] + + # Test each reset type + for reset_type in reset_types: + reset_success = {} + + # Reset each system + for system in systems: + operation = "Performing the reset action with the reset type '{}' for system '{}'".format(reset_type, system["Id"]) + logger.logger.info(operation) + + # Skip if the system does not support the reset action or shows reset capabilities + if system["Id"] not in reset_capabilities: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support the 'Reset' action or does not show supported reset types.".format(system["Id"])) + continue + + # Skip if the system does not support the reset type + if reset_type not in reset_capabilities[system["Id"]]: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support the reset action with reset type '{}'.".format(system["Id"], reset_type)) + continue + + # Perform the reset + reset_success[system["Id"]] = False + try: + response = redfish_utilities.system_reset(sut.session, system["Id"], reset_type) + response = redfish_utilities.poll_task_monitor(sut.session, response) + redfish_utilities.verify_response(response) + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + reset_success[system["Id"]] = True + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to reset system '{}' ({}).".format(system["Id"], err)) + + # Wait for all systems to reset + time.sleep(10) + + # Monitor each system to ensure it's in the desired power state + for system in systems: + if system["Id"] not in reset_success: + # Silently skip systems we already marked as skipped from the previous step + continue + + operation = "Monitoring the power state of system '{}'".format(system["Id"]) + logger.logger.info(operation) + + # Skip if the system does not support reporting the power state + if reset_success[system["Id"]] == False: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' failed the reset action with reset type '{}'.".format(system["Id"], reset_type)) + continue + + # Skip if the system does not support reporting the power state + if "PowerState" not in system: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support the 'PowerState' property.".format(system["Id"])) + continue + + # Monitor the system enter the desired power state + expected_power_state = "On" + if reset_type == "ForceOff": + expected_power_state = "Off" + try: + # Poll the power state for up to 50 seconds + for i in range(0, 10): + logger.logger.debug("Monitoring check {}".format(i)) + system_info = redfish_utilities.get_system(sut.session, system["Id"]) + if system_info.dict["PowerState"] == expected_power_state: + break + time.sleep(5) + + logger.logger.info("Finished monitoring the power state for system '{}'; 'PowerState' contains '{}'".format(system["Id"], system_info.dict["PowerState"])) + + if system_info.dict["PowerState"] != expected_power_state: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "System '{}' did not transition to the '{}' power state after performing a reset of type '{}'.".format(system["Id"], expected_power_state, reset_type)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to monitor the power state for system '{}' ({}).".format(system["Id"], err)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) diff --git a/redfish_use_case_checkers/redfish_logo.py b/redfish_use_case_checkers/redfish_logo.py new file mode 100644 index 0000000..b828cdc --- /dev/null +++ b/redfish_use_case_checkers/redfish_logo.py @@ -0,0 +1,321 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +""" +Redfish Logo + +File : redfish_logo.py + +Brief : This file contains the Base64 encoded image data for the Redfish logo +""" + +logo = """ +R0lGODlhLAHTAHAAACH5BAEAAPwALAAAAAAsAdMAhwAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZg +ArmQArzAAr/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCqmQCq +zACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/z +MrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV/zOAADOAMzOAZjOAmTOAzDOA/zOqADOq +MzOqZjOqmTOqzDOq/zPVADPVMzPVZjPVmTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZm +YAmWYAzGYA/2YrAGYrM2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaAM2aAZmaAmWaA +zGaA/2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/Zmb/mWb/zGb//5 +kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlVM5lVZplVmZlVzJlV/5mAAJmA +M5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq/5nVAJnVM5nVZpnVmZnVzJnV/5n/AJn/M5n/Zp +n/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr/8xVAMxVM8xVZsxVmcxV +zMxV/8yAAMyAM8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zVAMzVM8zVZszVmczVzMzV/8 +z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8rM/8rZv8rmf8rzP8r//9VAP9V +M/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+qM/+qZv+qmf+qzP+q///VAP/VM//VZv +/Vmf/VzP/V////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAj/APcJHEiwoMGDCBMqXMiwocOF +52C9gnXuocWLGDNq3Mixo8ePIENqbJeqZCpwtl65E8mypcuXMGPKnLmvHiyTqWDZ2vkqFc2fQIMKHU +r0YK9UPWHdzMYLpa2SFYtKnUq1qtWB7lJ9e6rK1jdVr2yhzAbWFqyVV/fRc4c2rdu3cBty3alN7M2w +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 +""" diff --git a/redfish_use_case_checkers/report.py b/redfish_use_case_checkers/report.py new file mode 100644 index 0000000..3900031 --- /dev/null +++ b/redfish_use_case_checkers/report.py @@ -0,0 +1,125 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +import html as html_mod +import json +from datetime import datetime + +from redfish_use_case_checkers import redfish_logo +from redfish_use_case_checkers.system_under_test import SystemUnderTest + +html_template = """ + + + Redfish Use Case Checkers Test Summary + + + + + + + + + + + + + {} +
+

##### Redfish Use Case Checkers Test Report #####

+

\"DMTF

+

+ https://github.com/DMTF/Redfish-Usecase-Checkers

+ Tool Version: {}
+ {}

+ This tool is provided and maintained by the DMTF. For feedback, please + open issues
in the tool's Github repository: + + https://github.com/DMTF/Redfish-Usecase-Checkers/issues
+
+ System: {}/redfish/v1/, User: {}, Password: {}
+ Product: {}
+ Manufacturer: {}, Model: {}, Firmware version: {}
+
+
Results Summary
+
Pass: {}, Warning: {}, Fail: {}, Not tested: {}
+
+ +""" + +section_header_html = """ + + + + +
+ {} +
+""" + +def html_report(sut: SystemUnderTest, report_dir, time, tool_version): + """ + Creates the HTML report for the system under test + + Args: + sut: The system under test + report_dir: The directory for the report + time: The time the tests finished + tool_version: The version of the tool + + Returns: + The path to the HTML report + """ + + file = report_dir / datetime.strftime(time, "RedfishUseCaseCheckersReport_%m_%d_%Y_%H%M%S.html") + html = "" + for test_category in sut._results: + html += section_header_html.format(test_category["Category"]) + for test in test_category["Tests"]: + html += "" + html += "".format(test["Name"], test["Description"], test["Details"]) + html += "".format("Operation", "Result", "Message") + for result in test["Results"]: + result_class = "" + if result["Result"] == "PASS": + result_class = "class=\"pass\"" + elif result["Result"] == "WARN": + result_class = "class=\"warn\"" + elif result["Result"] == "FAIL": + result_class = "class=\"fail\"" + operation = result["Operation"] + if operation == "": + operation = "No testing performed" + html += "".format(operation, result_class, result["Result"], html_mod.escape(result["Message"])) + html += "
{}: {}
{}
{}{}{}
{}{}{}
" + with open(str(file), "w", encoding="utf-8") as fd: + fd.write(html_template.format(redfish_logo.logo, tool_version, time.strftime("%c"), sut.rhost, sut.username, "********", sut.product, sut.manufacturer, sut.model, sut.firmware_version, sut.pass_count, sut.warn_count, sut.fail_count, sut.skip_count, html)) + return file diff --git a/redfish_use_case_checkers/system_under_test.py b/redfish_use_case_checkers/system_under_test.py new file mode 100644 index 0000000..f471f44 --- /dev/null +++ b/redfish_use_case_checkers/system_under_test.py @@ -0,0 +1,216 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +import redfish +import redfish_utilities + +from redfish_use_case_checkers import logger + +class SystemUnderTest(object): + def __init__(self, rhost, username, password): + """ + Constructor for new system under test + + Args: + rhost: The address of the Redfish service (with scheme) + username: The username for authentication + password: The password for authentication + """ + self._rhost = rhost + self._username = username + self._redfish_obj = redfish.redfish_client(base_url=rhost, username=username, password=password, timeout=15, max_retry=3) + self._redfish_obj.login(auth="session") + self._service_root = self._redfish_obj.root_resp.dict + self._results = [] + self._pass_count = 0 + self._warn_count = 0 + self._fail_count = 0 + self._skip_count = 0 + + # Find the manager to populate service info + self._product = None + self._product = self._service_root.get("Product", "N/A") + self._fw_version = None + self._model = None + self._manufacturer = None + if "Managers" in self._service_root: + try: + manager_ids = redfish_utilities.get_manager_ids(self._redfish_obj) + if len(manager_ids) > 0: + manager = redfish_utilities.get_manager(self._redfish_obj, manager_ids[0]) + self._fw_version = manager.dict.get("FirmwareVersion", "N/A") + self._model = manager.dict.get("Model", "N/A") + self._manufacturer = manager.dict.get("Manufacturer", "N/A") + except: + pass + + @property + def rhost(self): + """ + Accesses the address of the Redfish service + + Returns: + The address of the Redfish service + """ + return self._rhost + + @property + def username(self): + """ + Accesses the username for authentication + + Returns: + The username for authentication + """ + return self._username + + @property + def firmware_version(self): + """ + Accesses the firmware version of the service + + Returns: + The firmware version of the service + """ + return self._fw_version + + @property + def model(self): + """ + Accesses the model of the service + + Returns: + The model of the service + """ + return self._model + + @property + def product(self): + """ + Accesses the product of the service + + Returns: + The product of the service + """ + return self._product + + @property + def manufacturer(self): + """ + Accesses the manufacturer of the service + + Returns: + The manufacturer of the service + """ + return self._manufacturer + + @property + def session(self): + """ + Accesses the Redfish session + + Returns: + The Redfish client object + """ + return self._redfish_obj + + @property + def service_root(self): + """ + Accesses the service root data + + Returns: + The service root data as a dictionary + """ + return self._service_root + + @property + def pass_count(self): + """ + Accesses the pass count + + Returns: + The pass count + """ + return self._pass_count + + @property + def warn_count(self): + """ + Accesses the warning count + + Returns: + The warning count + """ + return self._warn_count + + @property + def fail_count(self): + """ + Accesses the fail count + + Returns: + The fail count + """ + return self._fail_count + + @property + def skip_count(self): + """ + Accesses the skip count + + Returns: + The skip count + """ + return self._skip_count + + def logout(self): + """ + Logs out of the Redfish service + """ + self._redfish_obj.logout() + + def add_results_category(self, category, tests): + """ + Adds a new category to the results + + Args: + category: The name of the category + tests: An array of test names and descriptions within the category + """ + new_category = {"Category": category, "Tests": []} + for test in tests: + new_category["Tests"].append({"Name": test[0], "Description": test[1], "Details": test[2], "Results": []}) + self._results.append(new_category) + + def add_test_result(self, category_name, test_name, operation, result, msg=""): + """ + Adds a new test result to the results + + Args: + category_name: The name of the category + test_name: The name of the test + operation: The operation performed for the test + result: The result of the test + msg: A message for the test + """ + for category in self._results: + if category["Category"] == category_name: + for test in category["Tests"]: + if test["Name"] == test_name: + test["Results"].append({"Operation": operation, "Result": result, "Message": msg}) + if result == "PASS": + self._pass_count += 1 + elif result == "WARN": + logger.logger.warn("Warning occurred during the {} test...".format(test_name)) + logger.logger.warn(msg) + self._warn_count += 1 + elif result == "FAIL": + logger.logger.error("Failing the {} test...".format(test_name)) + logger.logger.error(msg) + self._fail_count += 1 + elif result == "SKIP": + logger.logger.info("Skipping the {} test...".format(test_name)) + logger.logger.info(msg) + self._skip_count += 1 diff --git a/requirements.txt b/requirements.txt index fb622d2..0fa93d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ jsonschema +colorama redfish>=3.0.0 redfish_utilities>=1.1.4 diff --git a/rf_use_case_checkers.py b/rf_use_case_checkers.py new file mode 100644 index 0000000..f4f3d2c --- /dev/null +++ b/rf_use_case_checkers.py @@ -0,0 +1,8 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +from redfish_use_case_checkers.console_scripts import main + +if __name__ == '__main__': + main() From 8e8b4e978837954aba87f38c14d384a94e97eddb Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Tue, 4 Feb 2025 13:16:28 -0500 Subject: [PATCH 2/8] Added checks for addition boot properties based on the allowable boot override targets Signed-off-by: Mike Raineri --- redfish_use_case_checkers/boot_override.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/redfish_use_case_checkers/boot_override.py b/redfish_use_case_checkers/boot_override.py index d1bff85..00b97ab 100644 --- a/redfish_use_case_checkers/boot_override.py +++ b/redfish_use_case_checkers/boot_override.py @@ -163,6 +163,23 @@ def boot_test_boot_check(sut: SystemUnderTest, systems: list): sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "System '{}' contains 'BootSourceOverrideTarget' or 'BootSourceOverrideEnabled', but not both.".format(system["Id"])) boot_override_params.append(None) + # Check if the boot object contains other properties as needed by the allowable boot override targets + boot_override_targets = ["UefiTarget", "UefiHttp", "UefiBootNext"] + boot_override_properties = ["UefiTargetBootSourceOverride", "HttpBootUri", "BootNext"] + for target, prop in zip(boot_override_targets, boot_override_properties): + operation = "Checking that the 'Boot' property in system '{}' contains the '{}' property".format(system["Id"], target) + logger.logger.info(operation) + if "BootSourceOverrideTarget@Redfish.AllowableValues" not in system["Boot"]: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not contain 'BootSourceOverrideTarget@Redfish.AllowableValues'.".format(system["Id"])) + continue + if target in system["Boot"]["BootSourceOverrideTarget@Redfish.AllowableValues"]: + if prop not in system["Boot"]: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "System '{}' supports boot override to '{}' but does not contain '{}'.".format(system["Id"], target, prop)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + else: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support boot override to '{}'.".format(system["Id"], target)) + logger.log_use_case_test_footer(CAT_NAME, test_name) return boot_override_params From 08d0ee8e7d0bada2f74dc5c75bb8d9ba298a154d Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Tue, 4 Feb 2025 13:50:28 -0500 Subject: [PATCH 3/8] Fixed logging of the SKIP message if a reset failed during one-time boot testing Signed-off-by: Mike Raineri --- redfish_use_case_checkers/boot_override.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redfish_use_case_checkers/boot_override.py b/redfish_use_case_checkers/boot_override.py index 00b97ab..4a8a555 100644 --- a/redfish_use_case_checkers/boot_override.py +++ b/redfish_use_case_checkers/boot_override.py @@ -349,9 +349,9 @@ def boot_test_one_time_boot_check(sut: SystemUnderTest, systems: list, check_sys sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to reset system '{}' ({}).".format(system["Id"], err)) # Monitor the system to go back to None + operation = "Monitoring the boot progress for system '{}'".format(system["Id"]) + logger.logger.info(operation) if reset_success: - operation = "Monitoring the boot progress for system '{}'".format(system["Id"]) - logger.logger.info(operation) try: # Poll the boot object for up to 300 seconds for i in range(0, 30): From 7468228642348753af286778a7fe5d45a1887205 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Mon, 10 Feb 2025 10:35:34 -0500 Subject: [PATCH 4/8] Manager Ethernet interface testing added Signed-off-by: Mike Raineri --- .../manager_ethernet_interface_check.py | 183 ----------- manager_ethernet_interface/test_conf.json | 5 - manager_ethernet_interface/toolspath.py | 11 - .../account_management.py | 20 +- redfish_use_case_checkers/boot_override.py | 70 ++-- redfish_use_case_checkers/console_scripts.py | 5 +- .../manager_ethernet_interfaces.py | 309 ++++++++++++++++++ redfish_use_case_checkers/power_control.py | 29 +- .../system_under_test.py | 10 +- 9 files changed, 378 insertions(+), 264 deletions(-) delete mode 100644 manager_ethernet_interface/manager_ethernet_interface_check.py delete mode 100644 manager_ethernet_interface/test_conf.json delete mode 100644 manager_ethernet_interface/toolspath.py create mode 100644 redfish_use_case_checkers/manager_ethernet_interfaces.py diff --git a/manager_ethernet_interface/manager_ethernet_interface_check.py b/manager_ethernet_interface/manager_ethernet_interface_check.py deleted file mode 100644 index e8c2373..0000000 --- a/manager_ethernet_interface/manager_ethernet_interface_check.py +++ /dev/null @@ -1,183 +0,0 @@ -#! /usr/bin/python3 -# Copyright Notice: -# Copyright 2021 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -""" -Manager Ethernet Interface Usecase Test - -File : manager_ethernet_interface_check.py - -Brief : This file contains the definitions and functionalities for performing - the usecase test for verifying Ethernet interfaces of the managers -""" - -import argparse -import datetime -import logging -import sys -import time - -import redfish -import redfish_utilities - -import toolspath -from usecase.results import Results - -def dummy_address_check( address ): - """ - Determines if values contain dummy addresses - - Args: - address: Dictionary, list, or string containing addresses - - Returns: - True if any of the data contains a dummy address; False otherwise - """ - - dummy_addresses = [ "", "0.0.0.0", "::" ] - - if isinstance( address, dict ): - # Go through each property and check the value - for property in address: - if dummy_address_check( address[property] ): - return True - elif isinstance( address, list ): - # Go through each index and check the value - for value in address: - if dummy_address_check( value ): - return True - elif isinstance( address, str ): - if address in dummy_addresses: - return True - - return False - -if __name__ == '__main__': - - # Get the input arguments - argget = argparse.ArgumentParser( description = "Usecase checker for one time boot" ) - 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" ) - argget.add_argument( "--Secure", "-S", type = str, default = "Always", help = "When to use HTTPS (Always, IfSendingCredentials, IfLoginOrAuthenticatedApi, Never)" ) - argget.add_argument( "--directory", "-d", type = str, default = None, help = "Output directory for results.json" ) - argget.add_argument( "--debug", action = "store_true", help = "Creates debug file showing HTTP traces and exceptions" ) - args = argget.parse_args() - - if args.debug: - log_file = "manager_ethernet_interface_check-{}.log".format( datetime.datetime.now().strftime( "%Y-%m-%d-%H%M%S" ) ) - log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - logger = redfish.redfish_logger( log_file, log_format, logging.DEBUG ) - logger.info( "manager_ethernet_interface_check Trace" ) - - # Set up the Redfish object - base_url = "https://" + args.rhost - if args.Secure == "Never": - base_url = "http://" + args.rhost - with redfish.redfish_client( base_url = base_url, username = args.user, password = args.password ) as redfish_obj: - # Create the results object - service_root = redfish_obj.get( "/redfish/v1/" ) - results = Results( "Manager Ethernet Interface", service_root.dict ) - if args.directory is not None: - results.set_output_dir( args.directory ) - - # Get the available managers - test_managers = redfish_utilities.get_manager_ids( redfish_obj ) - manager_count = len( test_managers ) - print( "Found {} manager instances".format( manager_count ) ) - if manager_count == 0: - results.update_test_results( "Manager Count", 1, "No manager instances were found" ) - else: - results.update_test_results( "Manager Count", 0, None ) - - # Go through each manager and test each of its Ethernet interfaces - for manager in test_managers: - # Get the available Ethernet interfaces - test_interfaces = redfish_utilities.get_manager_ethernet_interface_ids( redfish_obj, manager ) - interface_count = len( test_interfaces ) - print( "Found {} Ethernet interface instances in manager '{}'".format( interface_count, manager ) ) - if interface_count == 0: - results.update_test_results( "Ethernet Interface Count", 1, "No Ethernet interface instances were found in manager '{}'".format( manager ) ) - else: - results.update_test_results( "Ethernet Interface Count", 0, None ) - - # Go through each Ethernet interface and test the response payloads - for interface in test_interfaces: - print( "Testing interface '{}'".format( interface ) ) - interface_resp = redfish_utilities.get_manager_ethernet_interface( redfish_obj, manager, interface ) - - # Check VLAN properties - if "VLAN" in interface_resp.dict: - property_check_list = [ "VLANEnable", "VLANId", "VLANPriority", "Tagged" ] - req_property_check_list = [ "VLANEnable", "VLANId" ] - for property in property_check_list: - # Check if the property is null - if property in interface_resp.dict["VLAN"]: - if interface_resp.dict["VLAN"][property] is None: - results.update_test_results( "Null Usage", 1, "'{}' contains null values in manager '{}' interface '{}'".format( property, manager, interface ) ) - else: - results.update_test_results( "Null Usage", 0, None ) - - # Check if the property is expected - if property in req_property_check_list: - if property in interface_resp.dict["VLAN"]: - results.update_test_results( "Expected Properties", 0, None ) - else: - results.update_test_results( "Expected Properties", 1, None, "VLAN does not contain {} in manager '{}' interface '{}'".format( property, manager, interface ) ) - - # Check usage of name servers - property_check_list = [ "NameServers", "StaticNameServers", "IPv4Addresses", "IPv4StaticAddresses", "IPv6Addresses", "IPv6StaticAddresses", "IPv6DefaultGateway", "IPv6StaticDefaultGateways" ] - property_status_list = [ "NameServers", "IPv4Addresses", "IPv6Addresses" ] - property_ip_list = [ "IPv4Addresses", "IPv4StaticAddresses", "IPv6Addresses", "IPv6StaticAddresses", "IPv6StaticDefaultGateways" ] - for property in property_check_list: - if property in interface_resp.dict: - # Status properties have an additional check to ensure null is not used; the array grows and shrinks based on what's active - if property in property_status_list: - if None in interface_resp.dict[property]: - results.update_test_results( "Null Usage", 1, "'{}' contains null values in manager '{}' interface '{}'".format( property, manager, interface ) ) - else: - results.update_test_results( "Null Usage", 0, None ) - - # Check that dummy addresses are not used - if dummy_address_check( interface_resp.dict[property] ): - results.update_test_results( "Dummy Value Usage", 1, "'{}' contains an empty string, 0.0.0.0, or :: rather than null in manager '{}' interface '{}'".format( property, manager, interface ) ) - else: - results.update_test_results( "Dummy Value Usage", 0, None ) - - # Check for expected IPv4 properties - if property in property_ip_list: - for i, address in enumerate( interface_resp.dict[property] ): - # Skip null entries - if address is None: - continue - - # Check that there is only a Gateway for index 0 - if "IPv4" in property: - if "Gateway" in address and i != 0: - results.update_test_results( "IPv4 Gateway", 1, "IPv4 gateway property found at non-first array index in manager '{}' interface '{}'".format( manager, interface ) ) - else: - results.update_test_results( "IPv4 Gateway", 0, None ) - - # Check for presence of properties - if "IPv4" in property: - ip_properties = [ "Gateway", "Address", "SubnetMask" ] - if "Static" not in property: - ip_properties.append( "AddressOrigin" ) - else: - ip_properties = [ "Address", "PrefixLength" ] - if "Static" not in property: - ip_properties.append( "AddressOrigin" ) - ip_properties.append( "AddressState" ) - for ip_property in ip_properties: - if ip_property == "Gateway" and i == 0: - continue - if ip_property not in address: - results.update_test_results( "Expected Properties", 1, None, "{} index {} does not contain {} in manager '{}' interface '{}'".format( property, i, ip_property, manager, interface ) ) - else: - results.update_test_results( "Expected Properties", 0, None ) - - # Save the results - results.write_results() - - sys.exit( results.get_return_code() ) diff --git a/manager_ethernet_interface/test_conf.json b/manager_ethernet_interface/test_conf.json deleted file mode 100644 index 10f334a..0000000 --- a/manager_ethernet_interface/test_conf.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "test": { - "command": "$interpreter manager_ethernet_interface_check.py -r $target_system -u $username -p $password -S $https -d $output_subdir" - } -} diff --git a/manager_ethernet_interface/toolspath.py b/manager_ethernet_interface/toolspath.py deleted file mode 100644 index 2053be7..0000000 --- a/manager_ethernet_interface/toolspath.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright Notice: -# Copyright 2021 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md - -import os -import sys - -cur_dir = os.path.dirname( __file__ ) -path_dir = os.path.abspath( os.path.join( cur_dir, os.path.pardir ) ) -if path_dir not in sys.path: - sys.path.insert( 0, path_dir ) diff --git a/redfish_use_case_checkers/account_management.py b/redfish_use_case_checkers/account_management.py index a5152ad..83d7a40 100644 --- a/redfish_use_case_checkers/account_management.py +++ b/redfish_use_case_checkers/account_management.py @@ -25,6 +25,7 @@ TEST_CREDENTIAL_CHECK = ("Credential Check", "Verifies the credentials of the new user are correctly enforced", "Creates a new Redfish session with the new user account. Attempts to read the members of the ManagerAccountCollection resource with the new session.") TEST_CHANGE_ROLE = ("Change Role", "Verifies that user roles can be modified", "Performs PATCH operations on the ManagerAccount resource of the new account to change the role. Performs a GET on the ManagerAccount resource and verifies the role was changed as requested.") TEST_DELETE_USER = ("Delete User", "Verifies that a user can be deleted", "Performs a DELETE operation on the ManagerAccount resource of the new account. Reads the members of the ManagerAccountCollection resource and verifies the user was deleted.") +TEST_LIST = [TEST_USER_COUNT, TEST_ADD_USER, TEST_ENABLE_USER, TEST_CREDENTIAL_CHECK, TEST_CHANGE_ROLE, TEST_DELETE_USER] def use_cases(sut: SystemUnderTest): """ @@ -37,17 +38,12 @@ def use_cases(sut: SystemUnderTest): logger.log_use_case_category_header(CAT_NAME) # Set initial results - sut.add_results_category(CAT_NAME, [TEST_USER_COUNT, TEST_ADD_USER, TEST_ENABLE_USER, TEST_CREDENTIAL_CHECK, TEST_CHANGE_ROLE, TEST_DELETE_USER]) + sut.add_results_category(CAT_NAME, TEST_LIST) # Check that there is an account service if "AccountService" not in sut.service_root: - msg = "Service does not contain an account service." - sut.add_test_result(CAT_NAME, TEST_USER_COUNT[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_ADD_USER[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_ENABLE_USER[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_CREDENTIAL_CHECK[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_CHANGE_ROLE[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_DELETE_USER[0], "", "SKIP", msg) + for test in TEST_LIST: + sut.add_test_result(CAT_NAME, test[0], "", "SKIP", "Service does not contain an account service.") logger.log_use_case_category_footer(CAT_NAME) return @@ -272,9 +268,9 @@ def acc_test_change_role(sut: SystemUnderTest, user_added: bool, username: str): # Change the role of the user test_roles = ["ReadOnly", "Operator", "Administrator"] for role in test_roles: + operation = "Setting user '{}' to role '{}'".format(username, role) + logger.logger.info(operation) try: - operation = "Setting user '{}' to role '{}'".format(username, role) - logger.logger.info(operation) redfish_utilities.modify_user(sut.session, username, new_role=role) if verify_user( sut.session, username, role = role ): sut.add_test_result(CAT_NAME, test_name, operation, "PASS") @@ -305,9 +301,9 @@ def acc_test_delete_user(sut: SystemUnderTest, user_added: bool, username: str): return # Delete the user + operation = "Deleting user '{}'".format(username) + logger.logger.info(operation) try: - operation = "Deleting user '{}'".format(username) - logger.logger.info(operation) redfish_utilities.delete_user(sut.session, username) if verify_user(sut.session, username): sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "User '{}' is still in the user list after successful DELETE.".format(username)) diff --git a/redfish_use_case_checkers/boot_override.py b/redfish_use_case_checkers/boot_override.py index 4a8a555..4360967 100644 --- a/redfish_use_case_checkers/boot_override.py +++ b/redfish_use_case_checkers/boot_override.py @@ -26,6 +26,7 @@ TEST_ONE_TIME_BOOT_SETTING = ("One-Time Boot Override", "Verifies the boot override supports the 'one-time' mode", "Performs a PATCH on the ComputerSystem resource to set the boot override to 'one-time' mode. Performs a GET on the ComputerSystem resource to verify the requested settings were applied.") TEST_ONE_TIME_BOOT_CHECK = ("One-Time Boot Override Check", "Verifies the one-time boot override is performed", "Performs a POST to the Reset action on the ComputerSystem resource. Monitors the boot override mode transitions back to 'disabled' after the reset.") TEST_DISABLE_BOOT_SETTING = ("Disable Boot Override", "Verifies the boot override can be disabled", "Performs a PATCH on the ComputerSystem resource to set the boot override to 'disabled' mode. Performs a GET on the ComputerSystem resource to verify the requested settings were applied.") +TEST_LIST = [TEST_SYSTEM_COUNT, TEST_BOOT_OVERRIDE_CHECK, TEST_CONTINUOUS_BOOT_SETTING, TEST_ONE_TIME_BOOT_SETTING, TEST_ONE_TIME_BOOT_CHECK, TEST_DISABLE_BOOT_SETTING] def use_cases(sut: SystemUnderTest): """ @@ -38,17 +39,12 @@ def use_cases(sut: SystemUnderTest): logger.log_use_case_category_header(CAT_NAME) # Set initial results - sut.add_results_category(CAT_NAME, [TEST_SYSTEM_COUNT, TEST_BOOT_OVERRIDE_CHECK, TEST_CONTINUOUS_BOOT_SETTING, TEST_ONE_TIME_BOOT_SETTING, TEST_ONE_TIME_BOOT_CHECK, TEST_DISABLE_BOOT_SETTING]) + sut.add_results_category(CAT_NAME, TEST_LIST) # Check that there is a system collection if "Systems" not in sut.service_root: - msg = "Service does not contain a system collection." - sut.add_test_result(CAT_NAME, TEST_SYSTEM_COUNT[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_BOOT_OVERRIDE_CHECK[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_CONTINUOUS_BOOT_SETTING[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_ONE_TIME_BOOT_SETTING[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_ONE_TIME_BOOT_CHECK[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_DISABLE_BOOT_SETTING[0], "", "SKIP", msg) + for test in TEST_LIST: + sut.add_test_result(CAT_NAME, test[0], "", "SKIP", "Service does not contain a system collection.") logger.log_use_case_category_footer(CAT_NAME) return @@ -92,9 +88,9 @@ def boot_test_system_count(sut: SystemUnderTest): # Get each member of the system collection for member in system_ids: + operation = "Getting system '{}'".format(member) + logger.logger.info(operation) try: - operation = "Getting system '{}'".format(member) - logger.logger.info(operation) system_resp = redfish_utilities.get_system(sut.session, member) systems.append(system_resp.dict) sut.add_test_result(CAT_NAME, test_name, operation, "PASS") @@ -126,42 +122,50 @@ def boot_test_boot_check(sut: SystemUnderTest, systems: list): logger.log_use_case_test_footer(CAT_NAME, test_name) return boot_override_params - # Try to get the boot override object for each system + # Inspect the boot object for each system for system in systems: - operation = "Checking the contents of the 'Boot' property in system '{}'".format(system["Id"]) + # Check if the system has a boot object + operation = "Checking for the 'Boot' property in system '{}'".format(system["Id"]) logger.logger.info(operation) if "Boot" not in system: # No boot object; skip sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not contain the 'Boot' property.".format(system["Id"])) boot_override_params.append(None) continue + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + # Check if the system has boot override properties + operation = "Checking for the 'BootSourceOverrideTarget' and 'BootSourceOverrideEnabled' properties in system '{}'".format(system["Id"]) + logger.logger.info(operation) if "BootSourceOverrideTarget" not in system["Boot"] and "BootSourceOverrideEnabled" not in system["Boot"]: - # No boot override properties; skip + # Both BootSourceOverrideTarget and BootSourceOverrideEnabled properties not present; skip sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not contain the boot override properties.".format(system["Id"])) boot_override_params.append(None) continue - - if "BootSourceOverrideTarget" in system["Boot"] and "BootSourceOverrideEnabled" in system["Boot"]: - # Both properties present; check for payload annotations to help users discover the supported values - if "BootSourceOverrideTarget@Redfish.AllowableValues" not in system["Boot"]: - sut.add_test_result(CAT_NAME, test_name, operation, "WARN", "System '{}' does not contain 'BootSourceOverrideTarget@Redfish.AllowableValues'.".format(system["Id"])) - elif "BootSourceOverrideEnabled@Redfish.AllowableValues" not in system["Boot"]: - sut.add_test_result(CAT_NAME, test_name, operation, "WARN", "System '{}' does not contain 'BootSourceOverrideEnabled@Redfish.AllowableValues'.".format(system["Id"])) - else: - sut.add_test_result(CAT_NAME, test_name, operation, "PASS") - # Cache the allowable boot parameters - # If the allowable values term is not present, assume it supports PXE, Continuous, and Once - boot_params = {} - boot_params["PXE"] = system["Boot"].get("BootSourceOverrideTarget@Redfish.AllowableValues", ["Pxe"]) - boot_params["USB"] = system["Boot"].get("BootSourceOverrideTarget@Redfish.AllowableValues", []) - boot_params["Continuous"] = system["Boot"].get("BootSourceOverrideEnabled@Redfish.AllowableValues", ["Continuous"]) - boot_params["Once"] = system["Boot"].get("BootSourceOverrideEnabled@Redfish.AllowableValues", ["Once"]) - boot_override_params.append(boot_params) - else: + elif "BootSourceOverrideTarget" not in system["Boot"] or "BootSourceOverrideEnabled" not in system["Boot"]: # Only one of the properties is present; boot override is not useable without both sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "System '{}' contains 'BootSourceOverrideTarget' or 'BootSourceOverrideEnabled', but not both.".format(system["Id"])) boot_override_params.append(None) + continue + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + + # Check for the allowable values properties + for allow_prop in ["BootSourceOverrideTarget", "BootSourceOverrideEnabled"]: + operation = "Checking for the '{}@Redfish.AllowableValues' property in system '{}'".format(allow_prop, system["Id"]) + logger.logger.info(operation) + if allow_prop + "@Redfish.AllowableValues" not in system["Boot"]: + sut.add_test_result(CAT_NAME, test_name, operation, "WARN", "System '{}' does not contain '{}'@Redfish.AllowableValues.".format(system["Id"], allow_prop)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + + # Cache the allowable boot parameters + # If the allowable values term is not present, assume it supports PXE, Continuous, and Once + boot_params = {} + boot_params["PXE"] = system["Boot"].get("BootSourceOverrideTarget@Redfish.AllowableValues", ["Pxe"]) + boot_params["USB"] = system["Boot"].get("BootSourceOverrideTarget@Redfish.AllowableValues", []) + boot_params["Continuous"] = system["Boot"].get("BootSourceOverrideEnabled@Redfish.AllowableValues", ["Continuous"]) + boot_params["Once"] = system["Boot"].get("BootSourceOverrideEnabled@Redfish.AllowableValues", ["Once"]) + boot_override_params.append(boot_params) # Check if the boot object contains other properties as needed by the allowable boot override targets boot_override_targets = ["UefiTarget", "UefiHttp", "UefiBootNext"] @@ -210,7 +214,6 @@ def boot_test_continuous_boot_settings(sut: SystemUnderTest, systems: list, boot # No boot override; skip sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support boot override.".format(system["Id"])) continue - if boot_param["Continuous"] is False: # No continuous boot; skip sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support 'continuous' boot override.".format(system["Id"])) @@ -273,7 +276,6 @@ def boot_test_one_time_boot_settings(sut: SystemUnderTest, systems: list, boot_o # No boot override; skip sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support boot override.".format(system["Id"])) continue - if boot_param["Once"] is False: # No one-time boot; skip sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "System '{}' does not support 'one-time' boot override.".format(system["Id"])) @@ -346,7 +348,7 @@ def boot_test_one_time_boot_check(sut: SystemUnderTest, systems: list, check_sys sut.add_test_result(CAT_NAME, test_name, operation, "PASS") reset_success = True except Exception as err: - sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to reset system '{}' ({}).".format(system["Id"], err)) + sut.add_test_result(CAT_NAME, test_name, operation, "FAILWARN", "Failed to reset system '{}' ({}).".format(system["Id"], err)) # Monitor the system to go back to None operation = "Monitoring the boot progress for system '{}'".format(system["Id"]) diff --git a/redfish_use_case_checkers/console_scripts.py b/redfish_use_case_checkers/console_scripts.py index 967972d..1405e33 100644 --- a/redfish_use_case_checkers/console_scripts.py +++ b/redfish_use_case_checkers/console_scripts.py @@ -23,6 +23,7 @@ from redfish_use_case_checkers import account_management from redfish_use_case_checkers import boot_override from redfish_use_case_checkers import logger +from redfish_use_case_checkers import manager_ethernet_interfaces from redfish_use_case_checkers import power_control from redfish_use_case_checkers import report @@ -39,6 +40,7 @@ def main(): 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("--report-dir", type=str, default="reports", help="the directory for generated report files (default: 'reports')") + argget.add_argument("--relaxed", action="store_true", help="Allows for some failures to be logged as warnings; useful if the criteria is to meet the literal 'shall' statements in the specification.") argget.add_argument("--debugging", action="store_true", help="Controls the verbosity of the debugging output; if not specified only INFO and higher are logged.") args = argget.parse_args() @@ -65,12 +67,13 @@ def main(): print() # Set up the system - sut = SystemUnderTest(args.rhost, args.user, args.password) + sut = SystemUnderTest(args.rhost, args.user, args.password, args.relaxed) # Run the tests account_management.use_cases(sut) power_control.use_cases(sut) boot_override.use_cases(sut) + manager_ethernet_interfaces.use_cases(sut) # Log out sut.logout() diff --git a/redfish_use_case_checkers/manager_ethernet_interfaces.py b/redfish_use_case_checkers/manager_ethernet_interfaces.py new file mode 100644 index 0000000..95d00c6 --- /dev/null +++ b/redfish_use_case_checkers/manager_ethernet_interfaces.py @@ -0,0 +1,309 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md + +""" +Manager Ethernet Interfaces Use Cases + +File : manager_ethernet_interfaces.py + +Brief : This file contains the definitions and functionalities for testing + use cases for manager Ethernet interfaces +""" + +import logging +import redfish +import redfish_utilities + +from redfish_use_case_checkers.system_under_test import SystemUnderTest +from redfish_use_case_checkers import logger + +CAT_NAME = "Manager Ethernet Interfaces" +TEST_ETH_INT_COUNT = ("Ethernet Interface Count", "Verifies the Ethernet interface list for each manager is not empty", "Locates the EthernetInterfaceCollection resource for each manager and performs GET on all members.") +TEST_VLAN_CHECK = ("VLAN Check", "Verifies that an Ethernet interface represents VLAN information correctly", "Verifies the VLAN property is present along with its configuration properties.") +TEST_ADDRESSES_CHECK = ("Addresses Check", "Verifies that an Ethernet interface represents IP addresses correctly", "Verifies the properties related to IP addresses are present and contain valid values.") +TEST_LIST = [TEST_ETH_INT_COUNT, TEST_VLAN_CHECK, TEST_ADDRESSES_CHECK] + +def use_cases(sut: SystemUnderTest): + """ + Performs the use cases for manager Ethernet Interfaces + + Args: + sut: The system under test + """ + + logger.log_use_case_category_header(CAT_NAME) + + # Set initial results + sut.add_results_category(CAT_NAME, TEST_LIST) + + # Check that there is a manager collection + if "Managers" not in sut.service_root: + for test in TEST_LIST: + sut.add_test_result(CAT_NAME, test[0], "", "SKIP", "Service does not contain a manager collection.") + logger.log_use_case_category_footer(CAT_NAME) + return + + # Go through the test cases + test_interfaces = mgr_eth_int_test_interface_count(sut) + mgr_eth_int_test_vlan_check(sut, test_interfaces) + mgr_eth_int_test_addresses_check(sut, test_interfaces) + + logger.log_use_case_category_footer(CAT_NAME) + +def invalid_address_check(address): + """ + Determines if values contain invalid addresses + + Args: + address: Dictionary, list, or string containing addresses + + Returns: + The first invalid address found; None otherwise + """ + + invalid_addresses = ["", "0.0.0.0", "::"] + + if isinstance(address, dict): + # Go through each property and check the value + for prop in address: + ret = invalid_address_check(address[prop]) + if ret is not None: + return ret + elif isinstance(address, list): + # Go through each index and check the value + for value in address: + ret = invalid_address_check(value) + if ret is not None: + return ret + elif isinstance(address, str): + if address in invalid_addresses: + return address + + return None + +def mgr_eth_int_test_interface_count(sut: SystemUnderTest): + """ + Performs the Ethernet interface count test + + Args: + sut: The system under test + + Returns: + An array of managers found + """ + + test_name = TEST_ETH_INT_COUNT[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + managers = {} + + # Get the list of managers + operation = "Counting the members of the manager collection" + logger.logger.info(operation) + manager_ids = [] + try: + manager_ids = redfish_utilities.get_manager_ids(sut.session) + if len(manager_ids) == 0: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "No managers were found.") + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get the manager list ({}).".format(err)) + + # Go through each manager to find their Ethernet interfaces + for manager_id in manager_ids: + # Get the manager + operation = "Getting manager '{}'".format(manager_id) + logger.logger.info(operation) + try: + manager_resp = redfish_utilities.get_manager(sut.session, manager_id) + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get manager '{}' ({}).".format(manager_id, err)) + continue + + # Get the list of Ethernet interfaces + operation = "Counting the members of the Ethernet interface collection" + logger.logger.info(operation) + managers[manager_id] = [] + if "EthernetInterfaces" not in manager_resp.dict: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "Manager '{}' does not contain an Ethernet interface collection.".format(manager_id)) + continue + eth_int_ids = [] + try: + eth_int_ids = redfish_utilities.get_manager_ethernet_interface_ids(sut.session, manager_id=manager_id) + if len(eth_int_ids) == 0: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "No Ethernet interfaces were found.") + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get the Ethernet interface list for manager '{}' ({}).".format(manager_id, err)) + + # Get each Ethernet interface + for interface_id in eth_int_ids: + operation = "Getting Ethernet interface '{}' from manager '{}'".format(interface_id, manager_id) + logger.logger.info(operation) + try: + eth_int_resp = redfish_utilities.get_manager_ethernet_interface(sut.session, manager_id=manager_id, interface_id=interface_id) + managers[manager_id].append(eth_int_resp.dict) + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + except Exception as err: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get Ethernet interface '{}' on manager '{}' ({}).".format(interface_id, manager_id, err)) + + logger.log_use_case_test_footer(CAT_NAME, test_name) + return managers + +def mgr_eth_int_test_vlan_check(sut: SystemUnderTest, eth_ints: dict): + """ + Performs the VLAN check test + + Args: + sut: The system under test + eth_ints: The managers with Ethernet interfaces to test + """ + + test_name = TEST_VLAN_CHECK[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + + # Skip the test if there are no managers + if len(eth_ints) == 0: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "No managers were found.") + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Go through each manager + for manager_id, eth_ints_list in eth_ints.items(): + # Skip the manager if there are no Ethernet interfaces + if len(eth_ints_list) == 0: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "Manager '{}' does not contain any Ethernet interfaces.".format(manager_id)) + continue + + # Go through each Ethernet interface + for eth_int in eth_ints_list: + # Skip the Ethernet interface if it does not have a VLAN property + operation = "Checking if Ethernet interface '{}' on manager '{}' has the 'VLAN' property".format(eth_int["Id"], manager_id) + logger.logger.info(operation) + if "VLAN" not in eth_int: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "Ethernet interface '{}' on manager '{}' does not have a VLAN.".format(eth_int["Id"], manager_id)) + continue + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + + # Check the properties inside the VLAN object + property_check_list = [ "VLANEnable", "VLANId", "VLANPriority", "Tagged" ] + req_property_check_list = [ "VLANEnable", "VLANId" ] + for prop in property_check_list: + operation = "Checking the '{}' property of Ethernet interface '{}' on manager '{}'".format(prop, eth_int["Id"], manager_id) + logger.logger.info(operation) + if prop not in eth_int["VLAN"] and prop in req_property_check_list: + # Some properties are always expected to be present to ensure the object is useful + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "The '{}' property is not present.".format(prop)) + elif prop not in eth_int["VLAN"]: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "The '{}' property is not present.".format(prop)) + else: + if eth_int["VLAN"][prop] is None: + # Null should only be used for error cases; the property should always have a valid value + sut.add_test_result(CAT_NAME, test_name, operation, "WARN", "The '{}' property is null.".format(prop)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + + logger.log_use_case_test_footer(CAT_NAME, test_name) + +def mgr_eth_int_test_addresses_check(sut: SystemUnderTest, eth_ints: dict): + """ + Performs the addresses check test + + Args: + sut: The system under test + eth_ints: The managers with Ethernet interfaces to test + """ + + test_name = TEST_ADDRESSES_CHECK[0] + logger.log_use_case_test_header(CAT_NAME, test_name) + + # Skip the test if there are no managers + if len(eth_ints) == 0: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "No managers were found.") + logger.log_use_case_test_footer(CAT_NAME, test_name) + return + + # Go through each manager + for manager_id, eth_ints_list in eth_ints.items(): + # Skip the manager if there are no Ethernet interfaces + if len(eth_ints_list) == 0: + sut.add_test_result(CAT_NAME, test_name, "", "SKIP", "Manager '{}' does not contain any Ethernet interfaces.".format(manager_id)) + continue + + # Go through each Ethernet interface + for eth_int in eth_ints_list: + address_props = ["NameServers", "StaticNameServers", "IPv4Addresses", "IPv4StaticAddresses", "IPv6Addresses", "IPv6StaticAddresses", "IPv6DefaultGateway", "IPv6StaticDefaultGateways"] + non_null_props = ["NameServers", "IPv4Addresses", "IPv6Addresses"] + ip_props = ["IPv4Addresses", "IPv4StaticAddresses", "IPv6Addresses", "IPv6StaticAddresses", "IPv6StaticDefaultGateways"] + for prop in address_props: + # Check for the presence of the property + operation = "Checking if Ethernet interface '{}' on manager '{}' contains the '{}' property".format(eth_int["Id"], manager_id, prop) + logger.logger.info(operation) + if prop not in eth_int: + sut.add_test_result(CAT_NAME, test_name, operation, "SKIP", "The '{}' property is not present.".format(prop)) + continue + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + + # Check for invalid addresses + operation = "Checking if Ethernet interface '{}' on manager '{}' does not contain invalid addresses in the '{}' property".format(eth_int["Id"], manager_id, prop) + logger.logger.info(operation) + inv_address = invalid_address_check(eth_int[prop]) + if inv_address is not None: + sut.add_test_result(CAT_NAME, test_name, operation, "FAILWARN", "The '{}' property contains the invalid address '{}'.".format(prop, inv_address)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + + # Check for null values + if prop in non_null_props: + operation = "Checking if Ethernet interface '{}' on manager '{}' does not contain null values in the '{}' property".format(eth_int["Id"], manager_id, prop) + logger.logger.info(operation) + if None in eth_int[prop]: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "The '{}' property contains one or more null values.".format(prop)) + else: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + + # Perform additional checks for IP address properties + if prop in ip_props: + if "IPv4" in prop: + # Check that the gateway is only in the first address + operation = "Checking if Ethernet interface '{}' on manager '{}' only contains one gateway in the '{}' property".format(eth_int["Id"], manager_id, prop) + logger.logger.info(operation) + gateway_pass = True + for i, address in enumerate(eth_int[prop]): + if "Gateway" in address and i != 0: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "The '{}' property contains a gateway address outside the first array member.".format(prop)) + gateway_pass = True + break + elif "Gateway" not in address and i == 0: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "The '{}' property does not have a gateway in the first array member.".format(prop)) + gateway_pass = True + break + if gateway_pass: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + + # Check for other expected properties + operation = "Checking if Ethernet interface '{}' on manager '{}' contains expected properties in the '{}' property".format(eth_int["Id"], manager_id, prop) + logger.logger.info(operation) + if "IPv4" in prop: + expected_properties = ["Address", "SubnetMask"] + if "Static" not in prop: + expected_properties.append("AddressOrigin") + else: + expected_properties = ["Address", "PrefixLength"] + if "Static" not in prop: + expected_properties.append("AddressOrigin") + expected_properties.append("AddressState") + exp_pass = True + for i, address in enumerate(eth_int[prop]): + for exp_prop in expected_properties: + if exp_prop not in address: + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "The '{}' property does not contain the '{}' property at index {}.".format(prop, exp_prop, i)) + exp_pass = False + break + if exp_pass: + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") + + logger.log_use_case_test_footer(CAT_NAME, test_name) diff --git a/redfish_use_case_checkers/power_control.py b/redfish_use_case_checkers/power_control.py index bbc48d6..9d4d090 100644 --- a/redfish_use_case_checkers/power_control.py +++ b/redfish_use_case_checkers/power_control.py @@ -23,6 +23,7 @@ TEST_SYSTEM_COUNT = ("System Count", "Verifies the system list is not empty", "Locates the ComputerSystemCollection resource and performs GET on all members.") TEST_RESET_TYPE = ("Reset Type", "Verifies the each system reports supported reset types", "Inspects the Reset action for each system for the supported reset types.") TEST_RESET_OPERATION = ("Reset Operation", "Verifies that a system can be reset", "Performs a POST operation on the Reset action on the ComputerSystem resource. Performs a GET on the ComputerSystem resource and verifies it's in the desired power state.") +TEST_LIST = [TEST_SYSTEM_COUNT, TEST_RESET_TYPE, TEST_RESET_OPERATION] def use_cases(sut: SystemUnderTest): """ @@ -35,14 +36,12 @@ def use_cases(sut: SystemUnderTest): logger.log_use_case_category_header(CAT_NAME) # Set initial results - sut.add_results_category(CAT_NAME, [TEST_SYSTEM_COUNT, TEST_RESET_TYPE, TEST_RESET_OPERATION]) + sut.add_results_category(CAT_NAME, TEST_LIST) # Check that there is a system collection if "Systems" not in sut.service_root: - msg = "Service does not contain a system collection." - sut.add_test_result(CAT_NAME, TEST_SYSTEM_COUNT[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_RESET_TYPE[0], "", "SKIP", msg) - sut.add_test_result(CAT_NAME, TEST_RESET_OPERATION[0], "", "SKIP", msg) + for test in TEST_LIST: + sut.add_test_result(CAT_NAME, test[0], "", "SKIP", "Service does not contain a system collection.") logger.log_use_case_category_footer(CAT_NAME) return @@ -75,24 +74,24 @@ def power_test_system_count(sut: SystemUnderTest): try: system_ids = redfish_utilities.get_system_ids(sut.session) if len(system_ids) == 0: - sut.add_test_result(CAT_NAME,test_name, operation, "FAIL", "No systems were found.") + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "No systems were found.") else: - sut.add_test_result(CAT_NAME,test_name, operation, "PASS") + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") except Exception as err: - sut.add_test_result(CAT_NAME,test_name, operation, "FAIL", "Failed to get the system list ({}).".format(err)) + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get the system list ({}).".format(err)) # Get each member of the system collection for member in system_ids: + operation = "Getting system '{}'".format(member) + logger.logger.info(operation) try: - operation = "Getting system '{}'".format(member) - logger.logger.info(operation) system_resp = redfish_utilities.get_system(sut.session, member) systems.append(system_resp.dict) - sut.add_test_result(CAT_NAME,test_name, operation, "PASS") + sut.add_test_result(CAT_NAME, test_name, operation, "PASS") except Exception as err: - sut.add_test_result(CAT_NAME,test_name, operation, "FAIL", "Failed to get the system '{}' ({}).".format(member, err)) + sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to get the system '{}' ({}).".format(member, err)) - logger.log_use_case_test_footer(CAT_NAME,test_name) + logger.log_use_case_test_footer(CAT_NAME, test_name) return systems @@ -133,7 +132,7 @@ def power_test_reset_type(sut: SystemUnderTest, systems: list): reset_types = param["AllowableValues"] reset_capabilities[system["Id"]] = reset_types if reset_types is None: - sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "System '{}' does not report supported reset types.".format(system["Id"])) + sut.add_test_result(CAT_NAME, test_name, operation, "FAILWARN", "System '{}' does not report supported reset types.".format(system["Id"])) else: sut.add_test_result(CAT_NAME, test_name, operation, "PASS") except Exception as err: @@ -184,7 +183,7 @@ def power_test_reset_operation(sut: SystemUnderTest, systems: list, reset_capabi sut.add_test_result(CAT_NAME, test_name, operation, "PASS") reset_success[system["Id"]] = True except Exception as err: - sut.add_test_result(CAT_NAME, test_name, operation, "FAIL", "Failed to reset system '{}' ({}).".format(system["Id"], err)) + sut.add_test_result(CAT_NAME, test_name, operation, "FAILWARN", "Failed to reset system '{}' ({}).".format(system["Id"], err)) # Wait for all systems to reset time.sleep(10) diff --git a/redfish_use_case_checkers/system_under_test.py b/redfish_use_case_checkers/system_under_test.py index f471f44..bb67fb4 100644 --- a/redfish_use_case_checkers/system_under_test.py +++ b/redfish_use_case_checkers/system_under_test.py @@ -8,7 +8,7 @@ from redfish_use_case_checkers import logger class SystemUnderTest(object): - def __init__(self, rhost, username, password): + def __init__(self, rhost, username, password, relaxed): """ Constructor for new system under test @@ -16,9 +16,11 @@ def __init__(self, rhost, username, password): rhost: The address of the Redfish service (with scheme) username: The username for authentication password: The password for authentication + relaxed: Whether or not to apply relaxed testing criteria """ self._rhost = rhost self._username = username + self._relaxed = relaxed self._redfish_obj = redfish.redfish_client(base_url=rhost, username=username, password=password, timeout=15, max_retry=3) self._redfish_obj.login(auth="session") self._service_root = self._redfish_obj.root_resp.dict @@ -202,12 +204,14 @@ def add_test_result(self, category_name, test_name, operation, result, msg=""): test["Results"].append({"Operation": operation, "Result": result, "Message": msg}) if result == "PASS": self._pass_count += 1 - elif result == "WARN": + elif result == "WARN" or (result == "FAILWARN" and self._relaxed is True): logger.logger.warn("Warning occurred during the {} test...".format(test_name)) + test["Results"][-1]["Result"] = "WARN" logger.logger.warn(msg) self._warn_count += 1 - elif result == "FAIL": + elif result == "FAIL" or result == "FAILWARN": logger.logger.error("Failing the {} test...".format(test_name)) + test["Results"][-1]["Result"] = "FAIL" logger.logger.error(msg) self._fail_count += 1 elif result == "SKIP": From 57cf9ecc14c1e8c0af69ff988a6721000b28fa00 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Wed, 12 Feb 2025 12:06:15 -0500 Subject: [PATCH 5/8] Updated readme for the new changes Signed-off-by: Mike Raineri --- README.md | 134 ++++++++++++++++++------------------------------------ 1 file changed, 45 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index f543462..2cab8f0 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,69 @@ -# Redfish Usecase Checkers +# Redfish Use Case Checkers -Copyright 2017-2021 DMTF. All rights reserved. +Copyright 2017-2025 DMTF. All rights reserved. ## About - Language: Python 3.x - -This is a collection of tools to exercise and validate common use cases for DMTF Redfish. -For example: -* Issue system reset commands (`On`, `GracefulShutdown`, `GracefulRestart`, etc.) -* Issue PATCH requests for boot override modes, modifying BIOS/UEFI boot sequence -* Add/modify/delete user accounts +The Redfish Use Case Checkers performs common management use cases to ensure a Redfish service meets functional expectations. +## Installation -## Prerequisites +From GitHub: -Install `jsonschema`, `redfish`, and `redfish_utilities`: + git clone https://github.com/DMTF/Redfish-Usecase-Checkers.git + cd Redfish-Usecase-Checkers -``` -pip install jsonschema -pip install redfish -pip install redfish_utilities -``` - - -## Test Details and Examples - -Each tool may be execuated with the `-h` option to get verbose help on parameters. - - -### One Time Boot Checker - -This checker logs into a specified service and traverses the systems collection. -It will perform the following operations on all systems: -* Reads the `Boot` object -* Sets the `BootSourceOverrideTarget` property to either `Pxe` or `Usb`, depending on what's allowed -* Performs a reset of the system -* Monitors the `BootSourceOverrideTarget` property after the reset to ensure it changes back to `None` - -Example: -``` -$ python3 one_time_boot_check.py -r 127.0.0.1:8000 -u -p -S Always -``` - - -### Power/Thermal Info Checker - -This checker logs into a specified service and traverses the chassis collection. -For each chassis found, it will ensure that it can collect at least one sensor reading from the `Power` and `Thermal` resources. -For each sensor reading found, it will ensure that the readings are consistent with the state of the sensor, as in there are no bogus readings for a device that isn't present. - -Example: -``` -$ python3 power_thermal_test.py -r 127.0.0.1:8000 -u -p -S Always -``` +## Requirements +The Redfish Use Case Checkers requires Python3. -### Power Control Checker +Required external packages: -This checker logs into a specified service and traverses the system collection. -It will perform the following operations on all systems: -* Reads the allowable `ResetType` parameter values -* Performs a reset using each of the allowable `ResetType` values - -Example: ``` -$ python3 power_control.py -r 127.0.0.1:8000 -u -p -S Always +jsonschema +colorama +redfish +redfish_utilities ``` +If installing from GitHub, you may install the external packages by running: -### Account Management Checker + pip install -r requirements.txt -This checker logs into a specified service and performs the following operations: -* Creates a new user -* Logs into the service with the new user -* Modifies the new user with different roles -* Deletes the new user +## Usage -Example: ``` -$ python3 account_management.py --r 127.0.0.1:8000 -u -p -S Always +usage: rf_use_case_checkers.py [-h] --user USER --password PASSWORD --rhost + RHOST [--report-dir REPORT_DIR] [--relaxed] + [--debugging] + +Validate Redfish services against use cases + +options: + -h, --help show this help message and exit + --user USER, -u USER The username for authentication + --password PASSWORD, -p PASSWORD + The password for authentication + --rhost RHOST, -r RHOST + The address of the Redfish service (with scheme) + --report-dir REPORT_DIR + the directory for generated report files (default: + 'reports') + --relaxed Allows for some failures to be logged as warnings; + useful if the criteria is to meet the literal 'shall' + statements in the specification. + --debugging Controls the verbosity of the debugging output; if not + specified only INFO and higher are logged. ``` - -### Query Parameter Checker - -This checker logs into a specified service and performs the following operations: -* Inspects the `ProtocolFeatures` property to see what query parameters are supported -* Tests `$filter` on the role collection within the account service -* Tests `$select` on a role within the role collection within the account service -* Tests `$expand` on service root -* Tests `only` on various resources found on service root - Example: -``` -$ python3 query_parameters_check.py --r 127.0.0.1:8000 -u -p -S Always -``` + rf_use_case_checkers -r https://192.168.1.100 -u USERNAME -p PASSWORD -### Manager Ethernet Interface Checker +## Release Process -This checker logs into a specified service and traverses the Ethernet interface collection in each manager found in the manager collection. -It will perform the following operations on all Ethernet interfaces: -* Inspects array properties to ensure `null` is used to show empty slots that a client is allowed to configure -* Inspects string properties containing IP addresses to ensure invalid addresses, such as `0.0.0.0`, are not used -* Inspects IPv4 address properties to ensure `Gateway` is only present in the first array index -* Ensures the minimum number of expected properties for configuring VLANs and IP addresses are present - -Example: -``` -$ python3 manager_ethernet_interface_check.py --r 127.0.0.1:8000 -u -p -S Always -``` \ No newline at end of file +1. Go to the "Actions" page +2. Select the "Release and Publish" workflow +3. Click "Run workflow" +4. Fill out the form +5. Click "Run workflow" From 187be395f34e0c284e9eec8b00a7ffeb777ac2c4 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Wed, 12 Feb 2025 15:13:04 -0500 Subject: [PATCH 6/8] Updates to prepare for renaming the repo Signed-off-by: Mike Raineri --- LICENSE.md | 2 +- README.md | 4 ++-- query_parameters/query_parameters_check.py | 2 +- query_parameters/toolspath.py | 2 +- redfish_use_case_checkers/account_management.py | 2 +- redfish_use_case_checkers/boot_override.py | 2 +- redfish_use_case_checkers/console_scripts.py | 2 +- redfish_use_case_checkers/logger.py | 2 +- .../manager_ethernet_interfaces.py | 2 +- redfish_use_case_checkers/power_control.py | 2 +- redfish_use_case_checkers/redfish_logo.py | 2 +- redfish_use_case_checkers/report.py | 10 +++++----- redfish_use_case_checkers/system_under_test.py | 2 +- rf_use_case_checkers.py | 2 +- usecase/__init__.py | 2 +- usecase/results.py | 2 +- usecase/validation.py | 2 +- 17 files changed, 22 insertions(+), 22 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 7892458..e1f50eb 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2017-2024, Contributing Member(s) of Distributed Management Task +Copyright (c) 2017-2025, Contributing Member(s) of Distributed Management Task Force, Inc.. All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/README.md b/README.md index 2cab8f0..effd98e 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ The Redfish Use Case Checkers performs common management use cases to ensure a R From GitHub: - git clone https://github.com/DMTF/Redfish-Usecase-Checkers.git - cd Redfish-Usecase-Checkers + git clone https://github.com/DMTF/Redfish-Use-Case-Checkers.git + cd Redfish-Use-Case-Checkers ## Requirements diff --git a/query_parameters/query_parameters_check.py b/query_parameters/query_parameters_check.py index 78cf0be..8243104 100644 --- a/query_parameters/query_parameters_check.py +++ b/query_parameters/query_parameters_check.py @@ -1,7 +1,7 @@ #! /usr/bin/python3 # Copyright Notice: # Copyright 2021 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md """ Query Parameters Usecase Test diff --git a/query_parameters/toolspath.py b/query_parameters/toolspath.py index 2053be7..5a94951 100644 --- a/query_parameters/toolspath.py +++ b/query_parameters/toolspath.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2021 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md import os import sys diff --git a/redfish_use_case_checkers/account_management.py b/redfish_use_case_checkers/account_management.py index 83d7a40..6f15c4a 100644 --- a/redfish_use_case_checkers/account_management.py +++ b/redfish_use_case_checkers/account_management.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md """ Account Management Use Cases diff --git a/redfish_use_case_checkers/boot_override.py b/redfish_use_case_checkers/boot_override.py index 4360967..c6cfa9f 100644 --- a/redfish_use_case_checkers/boot_override.py +++ b/redfish_use_case_checkers/boot_override.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md """ Boot Override Use Cases diff --git a/redfish_use_case_checkers/console_scripts.py b/redfish_use_case_checkers/console_scripts.py index 1405e33..d0f16a8 100644 --- a/redfish_use_case_checkers/console_scripts.py +++ b/redfish_use_case_checkers/console_scripts.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md """ Redfish Use Case Checkers Console Scripts diff --git a/redfish_use_case_checkers/logger.py b/redfish_use_case_checkers/logger.py index dac1998..fabbe5d 100644 --- a/redfish_use_case_checkers/logger.py +++ b/redfish_use_case_checkers/logger.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md """ Redfish Use Case Checkers Logger diff --git a/redfish_use_case_checkers/manager_ethernet_interfaces.py b/redfish_use_case_checkers/manager_ethernet_interfaces.py index 95d00c6..e9c7078 100644 --- a/redfish_use_case_checkers/manager_ethernet_interfaces.py +++ b/redfish_use_case_checkers/manager_ethernet_interfaces.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md """ Manager Ethernet Interfaces Use Cases diff --git a/redfish_use_case_checkers/power_control.py b/redfish_use_case_checkers/power_control.py index 9d4d090..e3baea8 100644 --- a/redfish_use_case_checkers/power_control.py +++ b/redfish_use_case_checkers/power_control.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md """ Power Control Use Cases diff --git a/redfish_use_case_checkers/redfish_logo.py b/redfish_use_case_checkers/redfish_logo.py index b828cdc..8864bd4 100644 --- a/redfish_use_case_checkers/redfish_logo.py +++ b/redfish_use_case_checkers/redfish_logo.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md """ Redfish Logo diff --git a/redfish_use_case_checkers/report.py b/redfish_use_case_checkers/report.py index 3900031..a947b1d 100644 --- a/redfish_use_case_checkers/report.py +++ b/redfish_use_case_checkers/report.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md import html as html_mod import json @@ -47,14 +47,14 @@

##### Redfish Use Case Checkers Test Report #####

\"DMTF

-

- https://github.com/DMTF/Redfish-Usecase-Checkers

+

+ https://github.com/DMTF/Redfish-Use-Case-Checkers

Tool Version: {}
{}

This tool is provided and maintained by the DMTF. For feedback, please open issues
in the tool's Github repository: - - https://github.com/DMTF/Redfish-Usecase-Checkers/issues
+ + https://github.com/DMTF/Redfish-Use-Case-Checkers/issues
diff --git a/redfish_use_case_checkers/system_under_test.py b/redfish_use_case_checkers/system_under_test.py index bb67fb4..54b7fd2 100644 --- a/redfish_use_case_checkers/system_under_test.py +++ b/redfish_use_case_checkers/system_under_test.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md import redfish import redfish_utilities diff --git a/rf_use_case_checkers.py b/rf_use_case_checkers.py index f4f3d2c..0d9cbc0 100644 --- a/rf_use_case_checkers.py +++ b/rf_use_case_checkers.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md from redfish_use_case_checkers.console_scripts import main diff --git a/usecase/__init__.py b/usecase/__init__.py index f2d3a9a..e0328fd 100644 --- a/usecase/__init__.py +++ b/usecase/__init__.py @@ -1,3 +1,3 @@ # Copyright Notice: # Copyright 2017 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md diff --git a/usecase/results.py b/usecase/results.py index 5100c15..8c3fc7b 100644 --- a/usecase/results.py +++ b/usecase/results.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md import datetime import json diff --git a/usecase/validation.py b/usecase/validation.py index d9101e6..5957743 100644 --- a/usecase/validation.py +++ b/usecase/validation.py @@ -1,6 +1,6 @@ # Copyright Notice: # Copyright 2017 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Usecase-Checkers/blob/main/LICENSE.md +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md import jsonschema import logging From f78fad2520ab9a4bb0f0fa7aad94b2284290ddc7 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Thu, 13 Feb 2025 09:46:55 -0500 Subject: [PATCH 7/8] Prepping for formal installation process and PyPI distribution Signed-off-by: Mike Raineri --- .gitignore | 130 ++++++++++++++++++++++++++++++++++++++++++++++- README.md | 13 +++-- requirements.txt | 1 - setup.py | 32 ++++++++++++ 4 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 41df51c..facfe7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,129 @@ +# Byte-compiled / optimized / DLL files __pycache__/ -results.json -reports +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cover +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Use case checkers reports +reports/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/README.md b/README.md index effd98e..4b71061 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,16 @@ The Redfish Use Case Checkers performs common management use cases to ensure a R ## Installation +From PyPI: + + pip install redfish_use_case_checkers + From GitHub: git clone https://github.com/DMTF/Redfish-Use-Case-Checkers.git cd Redfish-Use-Case-Checkers + python setup.py sdist + pip install dist/redfish_use_case_checkers-x.x.x.tar.gz ## Requirements @@ -20,7 +26,6 @@ The Redfish Use Case Checkers requires Python3. Required external packages: ``` -jsonschema colorama redfish redfish_utilities @@ -33,9 +38,9 @@ If installing from GitHub, you may install the external packages by running: ## Usage ``` -usage: rf_use_case_checkers.py [-h] --user USER --password PASSWORD --rhost - RHOST [--report-dir REPORT_DIR] [--relaxed] - [--debugging] +usage: rf_use_case_checkers [-h] --user USER --password PASSWORD --rhost RHOST + [--report-dir REPORT_DIR] [--relaxed] + [--debugging] Validate Redfish services against use cases diff --git a/requirements.txt b/requirements.txt index 0fa93d6..77e3e42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -jsonschema colorama redfish>=3.0.0 redfish_utilities>=1.1.4 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..69916f5 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +# Copyright Notice: +# Copyright 2017-2025 Distributed Management Task Force, Inc. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md + +from setuptools import setup +from codecs import open + +with open("README.md", "r", "utf-8") as f: + long_description = f.read() + +setup( + name="redfish_use_case_checkers", + version="1.0.9", + description="Redfish Use Case Checkers", + long_description=long_description, + long_description_content_type="text/markdown", + author="DMTF, https://www.dmtf.org/standards/feedback", + license="BSD 3-clause \"New\" or \"Revised License\"", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Topic :: Communications" + ], + keywords="Redfish", + url="https://github.com/DMTF/Redfish-Use-Case-Checkers", + packages=["redfish_use_case_checkers"], + entry_points={ + "console_scripts": ["rf_use_case_checkers=redfish_use_case_checkers.console_scripts:main"] + }, + install_requires=["colorama", "redfish>=3.0.0", "redfish_utilities>=1.1.4"] +) From 232893af8b3d71bc4340a0dd39dac77a3b47e31b Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Thu, 13 Feb 2025 09:47:48 -0500 Subject: [PATCH 8/8] Removed unused folders Signed-off-by: Mike Raineri --- query_parameters/query_parameters_check.py | 364 --------------------- query_parameters/test_conf.json | 5 - query_parameters/toolspath.py | 11 - usecase/__init__.py | 3 - usecase/results.py | 71 ---- usecase/validation.py | 125 ------- 6 files changed, 579 deletions(-) delete mode 100644 query_parameters/query_parameters_check.py delete mode 100644 query_parameters/test_conf.json delete mode 100644 query_parameters/toolspath.py delete mode 100644 usecase/__init__.py delete mode 100644 usecase/results.py delete mode 100644 usecase/validation.py diff --git a/query_parameters/query_parameters_check.py b/query_parameters/query_parameters_check.py deleted file mode 100644 index 8243104..0000000 --- a/query_parameters/query_parameters_check.py +++ /dev/null @@ -1,364 +0,0 @@ -#! /usr/bin/python3 -# Copyright Notice: -# Copyright 2021 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md - -""" -Query Parameters Usecase Test - -File : query_parameters_check.py - -Brief : This file contains the definitions and functionalities for performing - the usecase test for query parameters -""" - -import argparse -import datetime -import logging -import sys -import time - -import redfish -import redfish_utilities - -import toolspath -from usecase.results import Results - -def filter_test( redfish_obj, service_root, results ): - """ - Tests the $filter query parameter - - Args: - redfish_obj: The Redfish client object with an open session - service_root: The service root response - results: The results output - """ - - # Ensure there's an account service and at least one role available - if "AccountService" not in service_root.dict: - results.update_test_results( "Filter Check", 0, "Account service not found for testing.", skipped = True ) - return - account_service = redfish_obj.get( service_root.dict["AccountService"]["@odata.id"] ) - if "Roles" not in account_service.dict: - results.update_test_results( "Filter Check", 0, "Role collection not found for testing.", skipped = True ) - return - role_collection_uri = account_service.dict["Roles"]["@odata.id"] - role_collection = redfish_obj.get( role_collection_uri ) - role_count = len( role_collection.dict["Members"] ) - if role_count == 0: - results.update_test_results( "Filter Check", 0, "Role collection is empty.", skipped = True ) - return - - # Get the first and last roles to be used for building $filter parameters - role_first = redfish_obj.get( role_collection.dict["Members"][0]["@odata.id"] ) - role_first_uri = role_first.dict["@odata.id"] - role_last = redfish_obj.get( role_collection.dict["Members"][-1]["@odata.id"] ) - first_and_last = 2 - if role_first == role_last: - first_and_last = 1 - - # Perform various $filter requests on the collection and check the members returned - filter_checks = [ - { - "Description": "Match exactly one", - "Query": { "$filter": "Id eq '" + role_first.dict["Id"] + "'" }, - "ExpectedLength": 1 - }, - { - "Description": "Match exactly everything except one", - "Query": { "$filter": "not (Id eq '" + role_first.dict["Id"] + "')" }, - "ExpectedLength": role_count - 1 - }, - { - "Description": "Match first or last", - "Query": { "$filter": "Id eq '" + role_first.dict["Id"] + "'" + " or Id eq '" + role_last.dict["Id"] + "'" }, - "ExpectedLength": first_and_last - } - ] - for check in filter_checks: - query_str = "$filter=" + check["Query"]["$filter"] - print( "Performing $filter={} on {}".format( query_str, role_collection_uri ) ) - filter_list = redfish_obj.get( role_collection_uri, args = check["Query"] ) - redfish_utilities.verify_response( filter_list ) - filter_count = len( filter_list.dict["Members"] ) - if filter_count != check["ExpectedLength"]: - results.update_test_results( "Filter Check", 1, "Query ({}) expected to return {} member(s); received {}.".format( query_str, check["ExpectedLength"], filter_count ) ) - else: - results.update_test_results( "Filter Check", 0, None ) - - # Perform a $filter query on an individual role and ensure the request is rejected - query = { "$filter": "Id eq '" + role_first.dict["Id"] + "'" } - query_str = "$filter=" + query["$filter"] - print( "Performing {} on {}".format( query_str, role_first_uri ) ) - filter_response = redfish_obj.get( role_first_uri, args = query ) - try: - redfish_utilities.verify_response( filter_response ) - results.update_test_results( "Filter Check", 1, "Query ({}) expected to result in an error, but succeeded.".format( query_str ) ) - except: - results.update_test_results( "Filter Check", 0, None ) - -def select_test( redfish_obj, service_root, results ): - """ - Tests the $select query parameter - - Args: - redfish_obj: The Redfish client object with an open session - service_root: The service root response - results: The results output - """ - - # Ensure there's an account service and at least one role available - if "AccountService" not in service_root.dict: - results.update_test_results( "Select Check", 0, "Account service not found for testing.", skipped = True ) - return - account_service = redfish_obj.get( service_root.dict["AccountService"]["@odata.id"] ) - if "Roles" not in account_service.dict: - results.update_test_results( "Select Check", 0, "Role collection not found for testing.", skipped = True ) - return - role_collection = redfish_obj.get( account_service.dict["Roles"]["@odata.id"] ) - role_count = len( role_collection.dict["Members"] ) - if role_count == 0: - results.update_test_results( "Select Check", 0, "Role collection is empty.", skipped = True ) - return - - # Get the first role to be used for $select testing - role_first = redfish_obj.get( role_collection.dict["Members"][0]["@odata.id"] ) - role_first_uri = role_first.dict["@odata.id"] - - # Perform the query - query = { "$select": "Name,AssignedPrivileges" } - query_str = "$select=" + query["$select"] - print( "Performing {} on {}".format( query_str, role_first_uri ) ) - select_response = redfish_obj.get( role_first_uri, args = query ) - redfish_utilities.verify_response( select_response ) - - # Check the response for the expected properties - required_properties = [ "@odata.id", "@odata.type", "Name", "AssignedPrivileges" ] - optional_properties = [ "@odata.context", "@odata.etag" ] - select_dict = select_response.dict - for required in required_properties: - if required not in select_dict: - results.update_test_results( "Select Check", 1, "Query ({}) response expected to contain property {}.".format( query_str, required ) ) - return - if select_dict[required] != role_first.dict[required]: - results.update_test_results( "Select Check", 1, "Query ({}) response contains different property value for {}.".format( query_str, required ) ) - return - select_dict.pop( required ) - for optional in optional_properties: - if optional in select_dict and optional in role_first.dict: - if select_dict[optional] != role_first.dict[optional]: - results.update_test_results( "Select Check", 1, "Query ({}) response contains different property value for {}.".format( query_str, optional ) ) - return - select_dict.pop( optional ) - elif optional not in select_dict and optional in role_first.dict: - results.update_test_results( "Select Check", 1, "Query ({}) response expected to contain property {}.".format( query_str, optional ) ) - return - for extra in select_dict: - results.update_test_results( "Select Check", 1, "Query ({}) response contains extra property {}.".format( query_str, extra ) ) - return - - results.update_test_results( "Select Check", 0, None ) - -def verify_expand( results, query, name, value, is_expanded ): - """ - Verifies an object is expanded properly - - Args: - results: The results output - query: The query string used - name: The name of the property - value: The value of the property - is_expanded: If expansion is expected - """ - - query_str = "$expand=" + query["$expand"] - if "@odata.id" in value: - if len( value ) == 1: - if is_expanded: - results.update_test_results( "Expand Check", 1, "Query ({}) expected to expand resource {}.".format( query_str, name ) ) - else: - results.update_test_results( "Expand Check", 0, None ) - else: - if is_expanded: - results.update_test_results( "Expand Check", 0, None ) - else: - results.update_test_results( "Expand Check", 1, "Query ({}) expected to not expand subordinate resource {}.".format( query_str, name) ) - -def expand_test( redfish_obj, service_root, results ): - """ - Tests the $expand query parameter - - Args: - redfish_obj: The Redfish client object with an open session - service_root: The service root response - results: The results output - """ - - expand_checks = [ - { "Term": "ExpandAll", "Query": { "$expand": "*" }, "Sub": True, "Links": True, "Levels": False }, - { "Term": "NoLinks", "Query": { "$expand": "." }, "Sub": True, "Links": False, "Levels": False }, - { "Term": "Links", "Query": { "$expand": "~" }, "Sub": False, "Links": True, "Levels": False }, - { "Term": "ExpandAll", "Query": { "$expand": "*($levels=1)" }, "Sub": True, "Links": True, "Levels": True }, - { "Term": "NoLinks", "Query": { "$expand": ".($levels=1)" }, "Sub": True, "Links": False, "Levels": True }, - { "Term": "Links", "Query": { "$expand": "~($levels=1)" }, "Sub": False, "Links": True, "Levels": True } - ] - - # Go through each of the different expand types - check_uri = "/redfish/v1/" - for check in expand_checks: - if not service_root.dict["ProtocolFeaturesSupported"]["ExpandQuery"].get( check["Term"], False ): - results.update_test_results( "Expand Check", 0, "{} not supported.".format( check["Term"] ), skipped = True ) - continue - - if not service_root.dict["ProtocolFeaturesSupported"]["ExpandQuery"].get( "Levels", False ) and check["Levels"]: - results.update_test_results( "Expand Check", 0, "Levels not supported on $expand".format( check["Term"] ), skipped = True ) - continue - - # Perform the query on service root - print( "Performing $expand={} on {}".format( check["Query"]["$expand"], check_uri ) ) - expand_response = redfish_obj.get( check_uri, args = check["Query"] ) - redfish_utilities.verify_response( expand_response ) - - # Check the response to ensure things are expanded properly - for property in expand_response.dict: - if property == "Links": - # Links object; scan it for expansion - for link_property in expand_response.dict[property]: - if isinstance( expand_response.dict[property][link_property], dict ): - verify_expand( results, check["Query"], link_property, expand_response.dict[property][link_property], check["Links"] ) - elif isinstance( expand_response.dict[property][link_property], list ): - for link_item in expand_response.dict[property][link_property]: - verify_expand( results, check["Query"], link_property, link_item, check["Links"] ) - elif isinstance( expand_response.dict[property], dict ): - # Non-Links object; check if this is a reference object and if it was expanded properly - verify_expand( results, check["Query"], property, expand_response.dict[property], check["Sub"] ) - -def only_test( redfish_obj, service_root, results ): - """ - Tests the only query parameter - - Args: - redfish_obj: The Redfish client object with an open session - service_root: The service root response - results: The results output - """ - - # List of service root properties to inspect; True indicates if the reference is to a collection - only_checks = { - "AccountService": False, - "SessionService": False, - "Chassis": True, - "Systems": True, - "Managers": True - } - - # Go through each of the service root properties and test the only query - query = { "only": None } - query_str = "only" - for check in only_checks: - if check not in service_root.dict: - results.update_test_results( "Only Check", 0, "{} not found for testing.".format( check ), skipped = True ) - continue - - check_uri = service_root.dict[check]["@odata.id"] - if only_checks[check]: - # Testing a collection - print( "Performing {} on {}".format( query_str, check_uri ) ) - only_response = redfish_obj.get( check_uri, args = query ) - redfish_utilities.verify_response( only_response ) - resource_response = redfish_obj.get( check_uri ) - redfish_utilities.verify_response( resource_response ) - if len( resource_response.dict["Members"] ) == 1: - # Collection has exactly one member; query response is supposed to be the one member - if only_response.dict["@odata.id"] == resource_response.dict["Members"][0]["@odata.id"]: - results.update_test_results( "Only Check", 0, None ) - else: - results.update_test_results( "Only Check", 1, "Query ({}) response for {} expected the only collection member.".format( query_str, check_uri ) ) - else: - # Collection does not have exactly one member; query response is supposed to be the collection itself - if only_response.dict["@odata.id"] != resource_response.dict["@odata.id"] or "Members" not in only_response.dict: - results.update_test_results( "Only Check", 1, "Query ({}) response for {} expected the collection itself.".format( query_str, check_uri ) ) - else: - results.update_test_results( "Only Check", 0, None ) - else: - # Testing a singular resource; this is supposed to produce an error for the client - print( "Performing {} on {}".format( query_str, check_uri ) ) - only_response = redfish_obj.get( check_uri, args = query ) - try: - redfish_utilities.verify_response( only_response ) - results.update_test_results( "Only Check", 1, "Query ({}) expected to result in an error for {}, but succeeded.".format( query_str, check_uri ) ) - except: - results.update_test_results( "Only Check", 0, None ) - -if __name__ == '__main__': - - # Get the input arguments - argget = argparse.ArgumentParser( description = "Usecase checker for query parameters" ) - 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" ) - argget.add_argument( "--Secure", "-S", type = str, default = "Always", help = "When to use HTTPS (Always, IfSendingCredentials, IfLoginOrAuthenticatedApi, Never)" ) - argget.add_argument( "--directory", "-d", type = str, default = None, help = "Output directory for results.json" ) - argget.add_argument( "--debug", action = "store_true", help = "Creates debug file showing HTTP traces and exceptions" ) - args = argget.parse_args() - - if args.debug: - log_file = "query_parameters_check-{}.log".format( datetime.datetime.now().strftime( "%Y-%m-%d-%H%M%S" ) ) - log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - logger = redfish.redfish_logger( log_file, log_format, logging.DEBUG ) - logger.info( "query_parameters_check Trace" ) - - # Set up the Redfish object - base_url = "https://" + args.rhost - if args.Secure == "Never": - base_url = "http://" + args.rhost - with redfish.redfish_client( base_url = base_url, username = args.user, password = args.password ) as redfish_obj: - # Create the results object - service_root = redfish_obj.get( "/redfish/v1/" ) - results = Results( "Query Parameters", service_root.dict ) - if args.directory is not None: - results.set_output_dir( args.directory ) - - if "ProtocolFeaturesSupported" in service_root.dict: - if service_root.dict["ProtocolFeaturesSupported"].get( "FilterQuery", False ): - try: - filter_test( redfish_obj, service_root, results ) - except Exception as err: - results.update_test_results( "Filter Check", 1, "Failed to perform $filter test ({}).".format( err ) ) - else: - results.update_test_results( "Filter Check", 0, "Service does not support $filter.", skipped = True ) - - if service_root.dict["ProtocolFeaturesSupported"].get( "SelectQuery", False ): - try: - select_test( redfish_obj, service_root, results ) - except Exception as err: - results.update_test_results( "Select Check", 1, "Failed to perform $select test ({}).".format( err ) ) - else: - results.update_test_results( "Select Check", 0, "Service does not support $select.", skipped = True ) - - if "ExpandQuery" in service_root.dict["ProtocolFeaturesSupported"]: - try: - expand_test( redfish_obj, service_root, results ) - except Exception as err: - results.update_test_results( "Expand Check", 1, "Failed to perform $expand test ({}).".format( err ) ) - else: - results.update_test_results( "Expand Check", 0, "Service does not support $expand.", skipped = True ) - - if service_root.dict["ProtocolFeaturesSupported"].get( "OnlyMemberQuery", False ): - try: - only_test( redfish_obj, service_root, results ) - except Exception as err: - results.update_test_results( "Only Check", 1, "Failed to perform $select test ({}).".format( err ) ) - else: - results.update_test_results( "Only Check", 0, "Service does not support $select.", skipped = True ) - else: - print( "Service does not report supported protocol features" ) - results.update_test_results( "Filter Check", 0, "Service does not report supported protocol features.", skipped = True ) - results.update_test_results( "Select Check", 0, "Service does not report supported protocol features.", skipped = True ) - results.update_test_results( "Expand Check", 0, "Service does not report supported protocol features.", skipped = True ) - results.update_test_results( "Only Check", 0, "Service does not report supported protocol features.", skipped = True ) - - # Save the results - results.write_results() - - sys.exit( results.get_return_code() ) diff --git a/query_parameters/test_conf.json b/query_parameters/test_conf.json deleted file mode 100644 index 9bd9c86..0000000 --- a/query_parameters/test_conf.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "test": { - "command": "$interpreter query_parameters_check.py -r $target_system -u $username -p $password -S $https -d $output_subdir" - } -} diff --git a/query_parameters/toolspath.py b/query_parameters/toolspath.py deleted file mode 100644 index 5a94951..0000000 --- a/query_parameters/toolspath.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright Notice: -# Copyright 2021 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md - -import os -import sys - -cur_dir = os.path.dirname( __file__ ) -path_dir = os.path.abspath( os.path.join( cur_dir, os.path.pardir ) ) -if path_dir not in sys.path: - sys.path.insert( 0, path_dir ) diff --git a/usecase/__init__.py b/usecase/__init__.py deleted file mode 100644 index e0328fd..0000000 --- a/usecase/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright Notice: -# Copyright 2017 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md diff --git a/usecase/results.py b/usecase/results.py deleted file mode 100644 index 8c3fc7b..0000000 --- a/usecase/results.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright Notice: -# Copyright 2017 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md - -import datetime -import json -import os -import sys - - -class Results(object): - - def __init__(self, tool_name, service_root): - self.output_dir = os.getcwd() - self.results_filename = "results.json" - self.tool_name = tool_name - self.return_code = 0 - self.results = {"ToolName": tool_name} - self.results.update({"Timestamp": {"DateTime": - "{:%Y-%m-%dT%H:%M:%SZ}".format(datetime.datetime.now(datetime.timezone.utc))}}) - if service_root is not None: - self.results.update({"ServiceRoot": service_root}) - else: - self.results.update({"ServiceRoot": {}}) - - def update_test_results(self, test_name, rc, msg, skipped=False): - if "TestResults" not in self.results: - self.results.update({"TestResults": {}}) - if test_name not in self.results["TestResults"]: - self.results["TestResults"].update({test_name: {"pass": 0, "fail": 0, "skip": 0}}) - if skipped: - self.results["TestResults"][test_name]["skip"] += 1 - elif rc == 0: - self.results["TestResults"][test_name]["pass"] += 1 - else: - print("ERROR: {}".format(msg)) - self.results["TestResults"][test_name]["fail"] += 1 - if "ErrorMessages" not in self.results["TestResults"]: - self.results["TestResults"].update({"ErrorMessages": []}) - if msg is not None: - self.results["TestResults"]["ErrorMessages"].append(test_name + ": " + msg) - self.return_code = rc - - def add_cmd_line_args(self, args): - self.results.update({"CommandLineArgs": args}) - - def set_output_dir(self, output_dir): - self.output_dir = os.path.abspath(output_dir) - try: - if not os.path.isdir(self.output_dir): - os.mkdir(self.output_dir) - except OSError as e: - print("Error creating output directory {}, error: {}".format(self.output_dir, e), file=sys.stderr) - print("Will write results file to current working directory instead.", file=sys.stderr) - self.output_dir = os.getcwd() - - def write_results(self): - path = os.path.join(self.output_dir, self.results_filename) - try: - with open(path, 'w') as outfile: - json.dump(self.results, outfile, indent=4) - except OSError as e: - print("Error writing results file to {}, error: {}".format(path, e), file=sys.stderr) - print("Printing results to STDOUT instead.", file=sys.stderr) - print(json.dumps(self.results, indent=4)) - - def json_string(self): - return json.dumps(self.results) - - def get_return_code(self): - return self.return_code diff --git a/usecase/validation.py b/usecase/validation.py deleted file mode 100644 index 5957743..0000000 --- a/usecase/validation.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright Notice: -# Copyright 2017 Distributed Management Task Force, Inc. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Use-Case-Checkers/blob/main/LICENSE.md - -import jsonschema -import logging -import requests - - -class SchemaValidation(object): - - def __init__(self, rhost, service_root, results, auth=None, verify=True, nossl=False): - self.service_root = service_root - self.results = results - self.schema_dict = None - self.rhost = rhost - self.auth = auth - self.verify = verify - self.nossl = nossl - self.proto = 'http' if nossl else 'https' - # get /redfish/v1/JSONSchemas collection and store it as a dict - if service_root is not None and "JsonSchemas" in service_root and "@odata.id" in service_root["JsonSchemas"]: - json_schema_uri = service_root["JsonSchemas"]["@odata.id"] - logging.info("SchemaValidation:__init__: json_schema_uri = {}".format(json_schema_uri)) - schemas = self.get_resource(json_schema_uri) - if schemas is not None and "Members" in schemas: - self.schema_dict = {elt["@odata.id"].rsplit("/", 1)[1]: elt["@odata.id"] for elt in schemas["Members"]} - logging.debug("SchemaValidation:__init__: schema_dict = {}".format(self.schema_dict)) - else: - logging.warning("SchemaValidation:__init__: unable to read schema Members") - else: - logging.warning("SchemaValidation:__init__: unable to get JsonSchemas from service root") - - def get_resource(self, uri): - name = uri.split('/')[-1] - logging.debug("get_resource: Getting {} resource with uri {}".format(name, uri)) - try: - r = requests.get(self.proto + '://' + self.rhost + uri, auth=self.auth, verify=self.verify) - if r.status_code == requests.codes.ok: - d = r.json() - if d is not None: - logging.debug("get_resource: {} resource: {}".format(name, d)) - return d - else: - logging.error("get_resource: No JSON content for {} found in response".format(uri)) - else: - logging.error("get_resource: Received unexpected response for resource {}: {}".format(name, r)) - return None - except requests.exceptions.RequestException as e: - logging.error("get_resource: Exception received while tying to fetch uri {}, error = {}".format(uri, e)) - return None - - @staticmethod - def split_odata_type(json_data): - """ - Get the @odata.type entry from the JSON payload, split into namespace and type and return as a tuple - """ - ns = type_name = None - if json_data is not None and "@odata.type" in json_data: - odata_type = json_data["@odata.type"] - ns, type_name = odata_type.rsplit('.', 1) - ns = ns.strip('#') - logging.info("SchemaValidation:split_odata_type: odata = {}, namespace = {}, typename = {}" - .format(odata_type, ns, type_name)) - else: - logging.info("SchemaValidation:split_odata_type: JSON payload empty or no @odata.type found") - return ns, type_name - - def get_json_schema(self, json_data): - """ - Fetch the JSON schema from the Redfish service. The schema to fetch is based on - the @odata.type entry in the JSON payload - """ - schema = None - if self.schema_dict is None: - return None - ns, type_name = self.split_odata_type(json_data) - if ns is None: - logging.error("SchemaValidation:get_json_schema: No '@odata.type' found in JSON payload") - return None - data = uri = None - if ns is not None and ns in self.schema_dict: - uri = self.schema_dict[ns] - elif type_name is not None and type_name in self.schema_dict: - uri = self.schema_dict[type_name] - if uri is not None: - data = self.get_resource(uri) - if data is not None and "Location" in data: - location = data["Location"] - if len(location) > 0 and "Uri" in location[0]: - uri = location[0]["Uri"] - schema = self.get_resource(uri) - else: - logging.error("SchemaValidation:get_json_schema: 'Uri' not found in Location[0]") - else: - logging.error("SchemaValidation:get_json_schema: 'Location' not found from uri {}".format(uri)) - return schema - - @staticmethod - def validate_json(json_data, schema): - """ - Validate the JSON response against the schema - """ - if json_data is None: - # JSON payload not required - logging.info("SchemaValidation:validate_json: No JSON payload to validate") - return 0, None - if schema is None: - # Redfish schema not required - logging.info("SchemaValidation:validate_json: No JSON schema for validation") - return 0, None - # validate the json response against the schema - try: - logging.debug("SchemaValidation:validate_json: JSON to be validated: {}".format(json_data)) - logging.debug("SchemaValidation:validate_json: JSON schema for validation: {}".format(schema)) - jsonschema.validate(json_data, schema) - except jsonschema.ValidationError as e: - logging.error("SchemaValidation:validate_json: JSON schema validation error: {}".format(e.message)) - return 4, e.message - except jsonschema.SchemaError as e: - logging.error("SchemaValidation:validate_json: JSON schema error: {}".format(e.message)) - return 8, e.message - else: - logging.info("SchemaValidation:validate_json: JSON schema validation successful") - return 0, None