From 3fb1792c91ae92e21623d993be4525d3c44a5722 Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Sat, 8 Mar 2025 18:10:21 -0700 Subject: [PATCH 01/17] Added a session token auth method to rf_diagnostic_data Signed-off-by: Brandon Biggs --- scripts/rf_diagnostic_data.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index 80fc191..da40ac2 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -25,8 +25,14 @@ argget = argparse.ArgumentParser( description="A tool to collect diagnostic data from a log service on a Redfish service" ) -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") + +# Add username and password arguments +argget.add_argument("--user", "-u", type=str, help="The user name for authentication") +argget.add_argument("--password", "-p", type=str, help="The password for authentication") + +# Add session token argument +argget.add_argument("--session-token", "-t", type=str, help="The session token for authentication") + argget.add_argument("--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)") argget.add_argument( "--manager", "-m", type=str, nargs="?", default=False, help="The ID of the manager containing the log service" @@ -62,6 +68,14 @@ argget.add_argument("--debug", action="store_true", help="Creates debug file showing HTTP traces and exceptions") args = argget.parse_args() +# Validate either username + password OR session_token +if (args.user and args.password and not args.session_token) or ( + args.session_token and not (args.user or args.password) +): + pass # Valid input +else: + argget.error("You must specify either both --user and --password, or --session-token") + # Determine the target log service based on the inputs # Effectively if the user gives multiple targets, some will be ignored container_type = redfish_utilities.log_container.MANAGER @@ -85,10 +99,15 @@ # Set up the Redfish object redfish_obj = None try: - redfish_obj = redfish.redfish_client( - base_url=args.rhost, username=args.user, password=args.password, timeout=15, max_retry=3 - ) - redfish_obj.login(auth="session") + if args.session_token: + sessionkey = str.encode(args.session_token) + redfish_obj = redfish.redfish_client(base_url=args.rhost, sessionkey=sessionkey, timeout=15, max_retry=3) + else: + redfish_obj = redfish.redfish_client( + base_url=args.rhost, username=args.user, password=args.password, timeout=15, max_retry=3 + ) + # Don't need to login if we're using a session key + redfish_obj.login(auth="session") except RedfishPasswordChangeRequiredError: redfish_utilities.print_password_change_required_and_logout(redfish_obj, args) sys.exit(1) @@ -127,5 +146,6 @@ print(e) finally: # Log out - redfish_utilities.logout(redfish_obj) + if not args.session_token: + redfish_utilities.logout(redfish_obj) sys.exit(exit_code) From 25e54a1b74be42503af1e4ff4027c85c9eb8755f Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Tue, 11 Mar 2025 21:36:01 -0600 Subject: [PATCH 02/17] Added a clean section to the makefile Signed-off-by: Brandon Biggs --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index bb3e0f7..32b41e9 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,10 @@ endif build: ##@ Build the python package python setup.py sdist +clean: ##@ Clean up build + rm -rf dist/* + rm -rf redfish_utilities.egg-info + install: ##@ Install with pip pip install dist/redfish_utilities-${VERSION}.tar.gz From 78cc94fee009b271406d2ae97d31c0068510eba7 Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Tue, 11 Mar 2025 21:39:29 -0600 Subject: [PATCH 03/17] Moving commonly used arguments to their own file. Also created a new logger script that allows for more options when logging. Updated rf_diagnostic_data with the new proposed argument and logger scheme Signed-off-by: Brandon Biggs --- redfish_utilities/arguments.py | 43 ++++++++++++++++++++++++++++++ redfish_utilities/logger.py | 42 +++++++++++++++++++++++++++++ scripts/rf_diagnostic_data.py | 48 ++++++++++++---------------------- 3 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 redfish_utilities/arguments.py create mode 100644 redfish_utilities/logger.py diff --git a/redfish_utilities/arguments.py b/redfish_utilities/arguments.py new file mode 100644 index 0000000..c31e90f --- /dev/null +++ b/redfish_utilities/arguments.py @@ -0,0 +1,43 @@ +import argparse + + +def validate_auth(args): + if (args.user and args.password and not args.session_token) or ( + args.session_token and not (args.user or args.password) + ): + return + else: + print("You must specify both --user and --password or --session-token") + quit(1) + + +def create_parent_parser(description: str = "", auth: bool = False, rhost: bool = False): + parent_parser = argparse.ArgumentParser(description=description, add_help=False) + + parent_parser.add_argument( + "--log-level", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + default="INFO", + help="Creates debug file showing HTTP traces and exceptions", + ) + parent_parser.add_argument("--log-to-console", action="store_true", help="Enable logging to console") + parent_parser.add_argument("--log-to-file", action="store_true", help="Enable logging to a file") + parent_parser.add_argument( + "--debug", action="store_true", help="Creates debug file showing HTTP traces and exceptions" + ) + + if auth: + parent_parser.add_argument("--user", "-u", type=str, help="The user name for authentication") + parent_parser.add_argument("--password", "-p", type=str, help="The password for authentication") + parent_parser.add_argument("--session-token", "-t", type=str, help="The session token for authentication") + + if rhost: + parent_parser.add_argument( + "--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)" + ) + + return parent_parser + + +def validate_args(args): + validate_auth(args) diff --git a/redfish_utilities/logger.py b/redfish_utilities/logger.py new file mode 100644 index 0000000..5bae261 --- /dev/null +++ b/redfish_utilities/logger.py @@ -0,0 +1,42 @@ +import logging +import datetime +import redfish +import os + + +def get_debug_level(level): + if level == "DEBUG": + return logging.DEBUG + elif level == "INFO": + return logging.INFO + elif level == "WARNING": + return logging.WARNING + elif level == "ERROR": + return logging.ERROR + elif level == "CRITICAL": + return logging.CRITICAL + else: + raise ValueError(f"Invalid debug level: {args.debug_level}") + + +def setup_logger( + file_log: bool = False, stream_log: bool = True, log_level: str = "INFO", file_name: str = "redfish_utils" +): + log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + log_level = get_debug_level(log_level) + logger = logging.getLogger(__name__) + + if file_log: + file_name = os.path.basename(file_name) + timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H%M%S") + log_file = f"{file_name}-{timestamp}.log".format() + logger = redfish.redfish_logger(log_file, log_format, log_level) + + if stream_log: + formatter = logging.Formatter(log_format) + sh = logging.StreamHandler() + sh.setFormatter(formatter) + logger.addHandler(sh) + logger.setLevel(log_level) + + return logger diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index da40ac2..03db05a 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -13,27 +13,22 @@ import argparse import datetime -import logging + +# import logging import os import redfish import redfish_utilities import traceback import sys from redfish.messages import RedfishPasswordChangeRequiredError +from redfish_utilities.arguments import create_parent_parser, validate_args +from redfish_utilities.logger import setup_logger # Get the input arguments -argget = argparse.ArgumentParser( - description="A tool to collect diagnostic data from a log service on a Redfish service" -) - -# Add username and password arguments -argget.add_argument("--user", "-u", type=str, help="The user name for authentication") -argget.add_argument("--password", "-p", type=str, help="The password for authentication") - -# Add session token argument -argget.add_argument("--session-token", "-t", type=str, help="The session token for authentication") +description = "A tool to collect diagnostic data from a log service on a Redfish service" +parent_parser = create_parent_parser(description=description, auth=True, rhost=True) +argget = argparse.ArgumentParser(parents=[parent_parser]) -argget.add_argument("--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)") argget.add_argument( "--manager", "-m", type=str, nargs="?", default=False, help="The ID of the manager containing the log service" ) @@ -44,6 +39,7 @@ "--chassis", "-c", type=str, nargs="?", default=False, help="The ID of the chassis containing the log service" ) argget.add_argument("--log", "-l", type=str, help="The ID of the log service") + argget.add_argument( "--type", "-type", @@ -65,16 +61,13 @@ help="The directory to save the diagnostic data; defaults to the current directory if not specified", default=".", ) -argget.add_argument("--debug", action="store_true", help="Creates debug file showing HTTP traces and exceptions") + args = argget.parse_args() +validate_args(args) +logger = setup_logger( + file_log=args.log_to_file, stream_log=args.log_to_console, log_level=args.log_level, file_name=__file__ +) -# Validate either username + password OR session_token -if (args.user and args.password and not args.session_token) or ( - args.session_token and not (args.user or args.password) -): - pass # Valid input -else: - argget.error("You must specify either both --user and --password, or --session-token") # Determine the target log service based on the inputs # Effectively if the user gives multiple targets, some will be ignored @@ -90,12 +83,6 @@ container_type = redfish_utilities.log_container.CHASSIS container_id = args.chassis -if args.debug: - log_file = "rf_diagnostic_data-{}.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("rf_diagnostic_data Trace") - # Set up the Redfish object redfish_obj = None try: @@ -116,7 +103,7 @@ exit_code = 0 try: - print("Collecting diagnostic data...") + logger.info("Collecting diagnostic data...") response = redfish_utilities.collect_diagnostic_data( redfish_obj, container_type, container_id, args.log, args.type, args.oemtype ) @@ -138,12 +125,11 @@ path = os.path.join(args.directory, filename) with open(path, "wb") as file: file.write(data) - print("Saved diagnostic data to '{}'".format(path)) + logger.info("Saved diagnostic data to '{}'".format(path)) except Exception as e: - if args.debug: - logger.error("Caught exception:\n\n{}\n".format(traceback.format_exc())) + logger.debug("Caught exception:\n\n{}\n".format(traceback.format_exc())) exit_code = 1 - print(e) + logger.info(e) finally: # Log out if not args.session_token: From 73aecd4f2c2beba6d85baf382e1e1be97be0a39f Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Tue, 11 Mar 2025 21:47:01 -0600 Subject: [PATCH 04/17] Updated the make lint section to include ruff and fixed my ruff errors Signed-off-by: Brandon Biggs --- Makefile | 3 ++- redfish_utilities/logger.py | 2 +- scripts/rf_diagnostic_data.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 32b41e9..1b4ecff 100644 --- a/Makefile +++ b/Makefile @@ -57,4 +57,5 @@ install-uv: ##@ Install with uv uv pip install dist/redfish_utilities-${VERSION}.tar.gz lint: ##@ Run linting - black . \ No newline at end of file + black . + ruff check . \ No newline at end of file diff --git a/redfish_utilities/logger.py b/redfish_utilities/logger.py index 5bae261..1f9ca89 100644 --- a/redfish_utilities/logger.py +++ b/redfish_utilities/logger.py @@ -16,7 +16,7 @@ def get_debug_level(level): elif level == "CRITICAL": return logging.CRITICAL else: - raise ValueError(f"Invalid debug level: {args.debug_level}") + raise ValueError(f"Invalid debug level: {level}") def setup_logger( diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index 03db05a..2ac68c3 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -12,7 +12,6 @@ """ import argparse -import datetime # import logging import os From 1f56b3776c638a5990e1ff2be2c61b3e31b65201 Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Sat, 10 May 2025 21:34:56 -0600 Subject: [PATCH 05/17] Added the copywrite and tried to turn debug to the standard debugging that is used everywhere else --- redfish_utilities/arguments.py | 14 ++++++++++++++ redfish_utilities/logger.py | 13 +++++++++++++ scripts/rf_diagnostic_data.py | 6 ++++++ 3 files changed, 33 insertions(+) diff --git a/redfish_utilities/arguments.py b/redfish_utilities/arguments.py index c31e90f..2a4dbb0 100644 --- a/redfish_utilities/arguments.py +++ b/redfish_utilities/arguments.py @@ -1,3 +1,17 @@ +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019-2025 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md + +""" +Argument parser + +File : arguments.py + +Brief : A single location for arguments to pull from rather than having them defined in a bunch of places +""" + + import argparse diff --git a/redfish_utilities/logger.py b/redfish_utilities/logger.py index 1f9ca89..cc9f482 100644 --- a/redfish_utilities/logger.py +++ b/redfish_utilities/logger.py @@ -1,3 +1,16 @@ +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019-2025 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md + +""" +Logger + +File : logger.py + +Brief : Contains a unified logger that can be referenced from other files +""" + import logging import datetime import redfish diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index 2ac68c3..f5344b1 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -82,6 +82,12 @@ container_type = redfish_utilities.log_container.CHASSIS container_id = args.chassis +if args.debug: + log_file = "rf_diagnostic_data-{}.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("rf_diagnostic_data Trace") + # Set up the Redfish object redfish_obj = None try: From b8c1c2fa11f1eb15b5e3d71a1913fd328a2e1808 Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Sat, 8 Mar 2025 18:10:21 -0700 Subject: [PATCH 06/17] Added a session token auth method to rf_diagnostic_data Signed-off-by: Brandon Biggs --- scripts/rf_diagnostic_data.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index 80fc191..da40ac2 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -25,8 +25,14 @@ argget = argparse.ArgumentParser( description="A tool to collect diagnostic data from a log service on a Redfish service" ) -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") + +# Add username and password arguments +argget.add_argument("--user", "-u", type=str, help="The user name for authentication") +argget.add_argument("--password", "-p", type=str, help="The password for authentication") + +# Add session token argument +argget.add_argument("--session-token", "-t", type=str, help="The session token for authentication") + argget.add_argument("--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)") argget.add_argument( "--manager", "-m", type=str, nargs="?", default=False, help="The ID of the manager containing the log service" @@ -62,6 +68,14 @@ argget.add_argument("--debug", action="store_true", help="Creates debug file showing HTTP traces and exceptions") args = argget.parse_args() +# Validate either username + password OR session_token +if (args.user and args.password and not args.session_token) or ( + args.session_token and not (args.user or args.password) +): + pass # Valid input +else: + argget.error("You must specify either both --user and --password, or --session-token") + # Determine the target log service based on the inputs # Effectively if the user gives multiple targets, some will be ignored container_type = redfish_utilities.log_container.MANAGER @@ -85,10 +99,15 @@ # Set up the Redfish object redfish_obj = None try: - redfish_obj = redfish.redfish_client( - base_url=args.rhost, username=args.user, password=args.password, timeout=15, max_retry=3 - ) - redfish_obj.login(auth="session") + if args.session_token: + sessionkey = str.encode(args.session_token) + redfish_obj = redfish.redfish_client(base_url=args.rhost, sessionkey=sessionkey, timeout=15, max_retry=3) + else: + redfish_obj = redfish.redfish_client( + base_url=args.rhost, username=args.user, password=args.password, timeout=15, max_retry=3 + ) + # Don't need to login if we're using a session key + redfish_obj.login(auth="session") except RedfishPasswordChangeRequiredError: redfish_utilities.print_password_change_required_and_logout(redfish_obj, args) sys.exit(1) @@ -127,5 +146,6 @@ print(e) finally: # Log out - redfish_utilities.logout(redfish_obj) + if not args.session_token: + redfish_utilities.logout(redfish_obj) sys.exit(exit_code) From 52c0eea8ead06096a82809e181ac21e63b06bdb1 Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Tue, 11 Mar 2025 21:36:01 -0600 Subject: [PATCH 07/17] Added a clean section to the makefile Signed-off-by: Brandon Biggs --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index bb3e0f7..32b41e9 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,10 @@ endif build: ##@ Build the python package python setup.py sdist +clean: ##@ Clean up build + rm -rf dist/* + rm -rf redfish_utilities.egg-info + install: ##@ Install with pip pip install dist/redfish_utilities-${VERSION}.tar.gz From 6f36a466252c70cb80097743866b27905b85003b Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Tue, 11 Mar 2025 21:39:29 -0600 Subject: [PATCH 08/17] Moving commonly used arguments to their own file. Also created a new logger script that allows for more options when logging. Updated rf_diagnostic_data with the new proposed argument and logger scheme Signed-off-by: Brandon Biggs --- redfish_utilities/arguments.py | 43 ++++++++++++++++++++++++++++++ redfish_utilities/logger.py | 42 +++++++++++++++++++++++++++++ scripts/rf_diagnostic_data.py | 48 ++++++++++++---------------------- 3 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 redfish_utilities/arguments.py create mode 100644 redfish_utilities/logger.py diff --git a/redfish_utilities/arguments.py b/redfish_utilities/arguments.py new file mode 100644 index 0000000..c31e90f --- /dev/null +++ b/redfish_utilities/arguments.py @@ -0,0 +1,43 @@ +import argparse + + +def validate_auth(args): + if (args.user and args.password and not args.session_token) or ( + args.session_token and not (args.user or args.password) + ): + return + else: + print("You must specify both --user and --password or --session-token") + quit(1) + + +def create_parent_parser(description: str = "", auth: bool = False, rhost: bool = False): + parent_parser = argparse.ArgumentParser(description=description, add_help=False) + + parent_parser.add_argument( + "--log-level", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + default="INFO", + help="Creates debug file showing HTTP traces and exceptions", + ) + parent_parser.add_argument("--log-to-console", action="store_true", help="Enable logging to console") + parent_parser.add_argument("--log-to-file", action="store_true", help="Enable logging to a file") + parent_parser.add_argument( + "--debug", action="store_true", help="Creates debug file showing HTTP traces and exceptions" + ) + + if auth: + parent_parser.add_argument("--user", "-u", type=str, help="The user name for authentication") + parent_parser.add_argument("--password", "-p", type=str, help="The password for authentication") + parent_parser.add_argument("--session-token", "-t", type=str, help="The session token for authentication") + + if rhost: + parent_parser.add_argument( + "--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)" + ) + + return parent_parser + + +def validate_args(args): + validate_auth(args) diff --git a/redfish_utilities/logger.py b/redfish_utilities/logger.py new file mode 100644 index 0000000..5bae261 --- /dev/null +++ b/redfish_utilities/logger.py @@ -0,0 +1,42 @@ +import logging +import datetime +import redfish +import os + + +def get_debug_level(level): + if level == "DEBUG": + return logging.DEBUG + elif level == "INFO": + return logging.INFO + elif level == "WARNING": + return logging.WARNING + elif level == "ERROR": + return logging.ERROR + elif level == "CRITICAL": + return logging.CRITICAL + else: + raise ValueError(f"Invalid debug level: {args.debug_level}") + + +def setup_logger( + file_log: bool = False, stream_log: bool = True, log_level: str = "INFO", file_name: str = "redfish_utils" +): + log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + log_level = get_debug_level(log_level) + logger = logging.getLogger(__name__) + + if file_log: + file_name = os.path.basename(file_name) + timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H%M%S") + log_file = f"{file_name}-{timestamp}.log".format() + logger = redfish.redfish_logger(log_file, log_format, log_level) + + if stream_log: + formatter = logging.Formatter(log_format) + sh = logging.StreamHandler() + sh.setFormatter(formatter) + logger.addHandler(sh) + logger.setLevel(log_level) + + return logger diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index da40ac2..03db05a 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -13,27 +13,22 @@ import argparse import datetime -import logging + +# import logging import os import redfish import redfish_utilities import traceback import sys from redfish.messages import RedfishPasswordChangeRequiredError +from redfish_utilities.arguments import create_parent_parser, validate_args +from redfish_utilities.logger import setup_logger # Get the input arguments -argget = argparse.ArgumentParser( - description="A tool to collect diagnostic data from a log service on a Redfish service" -) - -# Add username and password arguments -argget.add_argument("--user", "-u", type=str, help="The user name for authentication") -argget.add_argument("--password", "-p", type=str, help="The password for authentication") - -# Add session token argument -argget.add_argument("--session-token", "-t", type=str, help="The session token for authentication") +description = "A tool to collect diagnostic data from a log service on a Redfish service" +parent_parser = create_parent_parser(description=description, auth=True, rhost=True) +argget = argparse.ArgumentParser(parents=[parent_parser]) -argget.add_argument("--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)") argget.add_argument( "--manager", "-m", type=str, nargs="?", default=False, help="The ID of the manager containing the log service" ) @@ -44,6 +39,7 @@ "--chassis", "-c", type=str, nargs="?", default=False, help="The ID of the chassis containing the log service" ) argget.add_argument("--log", "-l", type=str, help="The ID of the log service") + argget.add_argument( "--type", "-type", @@ -65,16 +61,13 @@ help="The directory to save the diagnostic data; defaults to the current directory if not specified", default=".", ) -argget.add_argument("--debug", action="store_true", help="Creates debug file showing HTTP traces and exceptions") + args = argget.parse_args() +validate_args(args) +logger = setup_logger( + file_log=args.log_to_file, stream_log=args.log_to_console, log_level=args.log_level, file_name=__file__ +) -# Validate either username + password OR session_token -if (args.user and args.password and not args.session_token) or ( - args.session_token and not (args.user or args.password) -): - pass # Valid input -else: - argget.error("You must specify either both --user and --password, or --session-token") # Determine the target log service based on the inputs # Effectively if the user gives multiple targets, some will be ignored @@ -90,12 +83,6 @@ container_type = redfish_utilities.log_container.CHASSIS container_id = args.chassis -if args.debug: - log_file = "rf_diagnostic_data-{}.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("rf_diagnostic_data Trace") - # Set up the Redfish object redfish_obj = None try: @@ -116,7 +103,7 @@ exit_code = 0 try: - print("Collecting diagnostic data...") + logger.info("Collecting diagnostic data...") response = redfish_utilities.collect_diagnostic_data( redfish_obj, container_type, container_id, args.log, args.type, args.oemtype ) @@ -138,12 +125,11 @@ path = os.path.join(args.directory, filename) with open(path, "wb") as file: file.write(data) - print("Saved diagnostic data to '{}'".format(path)) + logger.info("Saved diagnostic data to '{}'".format(path)) except Exception as e: - if args.debug: - logger.error("Caught exception:\n\n{}\n".format(traceback.format_exc())) + logger.debug("Caught exception:\n\n{}\n".format(traceback.format_exc())) exit_code = 1 - print(e) + logger.info(e) finally: # Log out if not args.session_token: From 0c8aca7b5bd7f9d593cdab3028c328e55607ade0 Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Tue, 11 Mar 2025 21:47:01 -0600 Subject: [PATCH 09/17] Updated the make lint section to include ruff and fixed my ruff errors Signed-off-by: Brandon Biggs --- Makefile | 3 ++- redfish_utilities/logger.py | 2 +- scripts/rf_diagnostic_data.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 32b41e9..1b4ecff 100644 --- a/Makefile +++ b/Makefile @@ -57,4 +57,5 @@ install-uv: ##@ Install with uv uv pip install dist/redfish_utilities-${VERSION}.tar.gz lint: ##@ Run linting - black . \ No newline at end of file + black . + ruff check . \ No newline at end of file diff --git a/redfish_utilities/logger.py b/redfish_utilities/logger.py index 5bae261..1f9ca89 100644 --- a/redfish_utilities/logger.py +++ b/redfish_utilities/logger.py @@ -16,7 +16,7 @@ def get_debug_level(level): elif level == "CRITICAL": return logging.CRITICAL else: - raise ValueError(f"Invalid debug level: {args.debug_level}") + raise ValueError(f"Invalid debug level: {level}") def setup_logger( diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index 03db05a..2ac68c3 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -12,7 +12,6 @@ """ import argparse -import datetime # import logging import os From 20e13fcf811e4c005e2ed1979395ae9ea4c0a6c4 Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Fri, 18 Apr 2025 10:41:00 -0400 Subject: [PATCH 10/17] Fixed files to always be Unix format Signed-off-by: Mike Raineri Signed-off-by: Brandon Biggs --- .github/workflows/release.yml | 6 + docs/rf_assembly.md | 220 +++++----- docs/rf_firmware_inventory.md | 100 ++--- redfish_utilities/assembly.py | 354 +++++++-------- redfish_utilities/certificates.py | 702 +++++++++++++++--------------- scripts/rf_assembly.py | 170 ++++---- scripts/rf_firmware_inventory.py | 132 +++--- scripts/rf_test_event_listener.py | 222 +++++----- 8 files changed, 956 insertions(+), 950 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8c0eaf..8304451 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,12 @@ jobs: if [[ -n "${{github.event.inputs.changes_8}}" ]]; then echo "- ${{github.event.inputs.changes_8}}" >> $GITHUB_ENV; fi echo "" >> $GITHUB_ENV echo 'EOF' >> $GITHUB_ENV + - name: Convert all files to Unix format + run: | + sudo apt-get install dos2unix + find . -type f -name "*.py" -print0 | xargs -0 dos2unix + find . -type f -name "*.md" -print0 | xargs -0 dos2unix + find . -type f -name "*.txt" -print0 | xargs -0 dos2unix - name: Update version numbers run: | sed -i -E 's/ version=.+,/ version="'${{github.event.inputs.version}}'",/' setup.py diff --git a/docs/rf_assembly.md b/docs/rf_assembly.md index dbfe931..0213ea2 100644 --- a/docs/rf_assembly.md +++ b/docs/rf_assembly.md @@ -1,110 +1,110 @@ -# Assembly (rf_assembly.py) - -Copyright 2019-2025 DMTF. All rights reserved. - -## About - -A tool to manage assemblies on a Redfish service. - -## Usage - -``` -usage: rf_assembly.py [-h] --user USER --password PASSWORD --rhost RHOST - --assembly ASSEMBLY [--index INDEX] [--debug] - {info,download,upload} ... - -A tool to manage assemblies on a Redfish service - -positional arguments: - {info,download,upload} - info Displays information about the an assembly - download Downloads assembly data to a file - upload Uploads assembly data from a file - -required arguments: - --user USER, -u USER The user name for authentication - --password PASSWORD, -p PASSWORD - The password for authentication - --rhost RHOST, -r RHOST - The address of the Redfish service (with scheme) - --assembly ASSEMBLY, -a ASSEMBLY - The URI of the target assembly - -optional arguments: - -h, --help show this help message and exit - --index INDEX, -i INDEX - The target assembly index - --debug Creates debug file showing HTTP traces and exceptions -``` - -### Info - -Displays information about the an assembly. - -``` -usage: rf_assembly.py info [-h] - -optional arguments: - -h, --help show this help message and exit -``` - -The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. -It will then get the assembly information from the URI specified by the *assembly* argument and displays its information. - -Example: - -``` -$ rf_assembly.py -u root -p root -r https://192.168.1.100 -a /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Assembly info - 0 | Contoso Power Supply - | Model: 345TTT - | PartNumber: 923943 - | SerialNumber: 345394834 - | Producer: Contoso Supply Co. - | Vendor: Contoso - | ProductionDate: 2017-04-01T14:55:33+03:00 -``` - -### Download - -Downloads assembly data to a file. - -``` -usage: rf_assembly.py download [-h] --file FILE - -required arguments: - --file FILE, -f FILE The file, and optional path, to save the assembly data - -optional arguments: - -h, --help show this help message and exit -``` - -The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. -It will then get the assembly information from the URI specified by the *assembly* argument and download the binary data contents to the file specified by the *file* argument. - -``` -$ rf_assembly.py -u root -p root -r https://192.168.1.100 -a /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Assembly download -f data.bin -Saving data to 'data.bin'... -``` - -### Upload - -Uploads assembly data from a file. - -``` -usage: rf_assembly.py upload [-h] --file FILE - -required arguments: - --file FILE, -f FILE The file, and optional path, containing the assembly - data to upload - -optional arguments: - -h, --help show this help message and exit -``` - -The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. -It will then get the assembly information from the URI specified by the *assembly* argument and upload the contents of the file specified by the *file* argument to the binary data. - -``` -$ rf_assembly.py -u root -p root -r https://192.168.1.100 -a /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Assembly upload -f data.bin -Writing data from 'data.bin'... -``` +# Assembly (rf_assembly.py) + +Copyright 2019-2025 DMTF. All rights reserved. + +## About + +A tool to manage assemblies on a Redfish service. + +## Usage + +``` +usage: rf_assembly.py [-h] --user USER --password PASSWORD --rhost RHOST + --assembly ASSEMBLY [--index INDEX] [--debug] + {info,download,upload} ... + +A tool to manage assemblies on a Redfish service + +positional arguments: + {info,download,upload} + info Displays information about the an assembly + download Downloads assembly data to a file + upload Uploads assembly data from a file + +required arguments: + --user USER, -u USER The user name for authentication + --password PASSWORD, -p PASSWORD + The password for authentication + --rhost RHOST, -r RHOST + The address of the Redfish service (with scheme) + --assembly ASSEMBLY, -a ASSEMBLY + The URI of the target assembly + +optional arguments: + -h, --help show this help message and exit + --index INDEX, -i INDEX + The target assembly index + --debug Creates debug file showing HTTP traces and exceptions +``` + +### Info + +Displays information about the an assembly. + +``` +usage: rf_assembly.py info [-h] + +optional arguments: + -h, --help show this help message and exit +``` + +The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. +It will then get the assembly information from the URI specified by the *assembly* argument and displays its information. + +Example: + +``` +$ rf_assembly.py -u root -p root -r https://192.168.1.100 -a /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Assembly info + 0 | Contoso Power Supply + | Model: 345TTT + | PartNumber: 923943 + | SerialNumber: 345394834 + | Producer: Contoso Supply Co. + | Vendor: Contoso + | ProductionDate: 2017-04-01T14:55:33+03:00 +``` + +### Download + +Downloads assembly data to a file. + +``` +usage: rf_assembly.py download [-h] --file FILE + +required arguments: + --file FILE, -f FILE The file, and optional path, to save the assembly data + +optional arguments: + -h, --help show this help message and exit +``` + +The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. +It will then get the assembly information from the URI specified by the *assembly* argument and download the binary data contents to the file specified by the *file* argument. + +``` +$ rf_assembly.py -u root -p root -r https://192.168.1.100 -a /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Assembly download -f data.bin +Saving data to 'data.bin'... +``` + +### Upload + +Uploads assembly data from a file. + +``` +usage: rf_assembly.py upload [-h] --file FILE + +required arguments: + --file FILE, -f FILE The file, and optional path, containing the assembly + data to upload + +optional arguments: + -h, --help show this help message and exit +``` + +The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. +It will then get the assembly information from the URI specified by the *assembly* argument and upload the contents of the file specified by the *file* argument to the binary data. + +``` +$ rf_assembly.py -u root -p root -r https://192.168.1.100 -a /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Assembly upload -f data.bin +Writing data from 'data.bin'... +``` diff --git a/docs/rf_firmware_inventory.md b/docs/rf_firmware_inventory.md index 1422810..f88dc1c 100644 --- a/docs/rf_firmware_inventory.md +++ b/docs/rf_firmware_inventory.md @@ -1,50 +1,50 @@ -# Firmware Inventory (rf_firmware_inventory.py) - -Copyright 2019-2025 DMTF. All rights reserved. - -## About - -A tool to collect firmware inventory from a Redfish service. - -## Usage - -``` -usage: rf_firmware_inventory.py [-h] --user USER --password PASSWORD --rhost - RHOST [--details] [--id] [--debug] - -A tool to collect firmware inventory from a Redfish service - -required arguments: - --user USER, -u USER The user name for authentication - --password PASSWORD, -p PASSWORD - The password for authentication - --rhost RHOST, -r RHOST - The address of the Redfish service (with scheme) - -optional arguments: - -h, --help show this help message and exit - --details, -details Indicates details to be shown for each firmware entry - --id, -i Construct inventory names using 'Id' values - --debug Creates debug file showing HTTP traces and exceptions -``` - -The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. -It then retrieves the firmware inventory collection under the update service and prints its contents. - -Example: - -``` -$ rf_firmware_inventory.py -u root -p root -r https://192.168.1.100 -details - Contoso BMC Firmware | Version: 1.45.455b66-rev4 - | Manufacturer: Contoso - | SoftwareId: 1624A9DF-5E13-47FC-874A-DF3AFF143089 - | ReleaseDate: 2017-08-22T12:00:00Z - Contoso Simple Storage Firmware | Version: 2.50 - | Manufacturer: Contoso - | ReleaseDate: 2021-10-18T12:00:00Z - Contoso BIOS Firmware | Version: P79 v1.45 - | Manufacturer: Contoso - | SoftwareId: FEE82A67-6CE2-4625-9F44-237AD2402C28 - | ReleaseDate: 2017-12-06T12:00:00Z - -``` +# Firmware Inventory (rf_firmware_inventory.py) + +Copyright 2019-2025 DMTF. All rights reserved. + +## About + +A tool to collect firmware inventory from a Redfish service. + +## Usage + +``` +usage: rf_firmware_inventory.py [-h] --user USER --password PASSWORD --rhost + RHOST [--details] [--id] [--debug] + +A tool to collect firmware inventory from a Redfish service + +required arguments: + --user USER, -u USER The user name for authentication + --password PASSWORD, -p PASSWORD + The password for authentication + --rhost RHOST, -r RHOST + The address of the Redfish service (with scheme) + +optional arguments: + -h, --help show this help message and exit + --details, -details Indicates details to be shown for each firmware entry + --id, -i Construct inventory names using 'Id' values + --debug Creates debug file showing HTTP traces and exceptions +``` + +The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. +It then retrieves the firmware inventory collection under the update service and prints its contents. + +Example: + +``` +$ rf_firmware_inventory.py -u root -p root -r https://192.168.1.100 -details + Contoso BMC Firmware | Version: 1.45.455b66-rev4 + | Manufacturer: Contoso + | SoftwareId: 1624A9DF-5E13-47FC-874A-DF3AFF143089 + | ReleaseDate: 2017-08-22T12:00:00Z + Contoso Simple Storage Firmware | Version: 2.50 + | Manufacturer: Contoso + | ReleaseDate: 2021-10-18T12:00:00Z + Contoso BIOS Firmware | Version: P79 v1.45 + | Manufacturer: Contoso + | SoftwareId: FEE82A67-6CE2-4625-9F44-237AD2402C28 + | ReleaseDate: 2017-12-06T12:00:00Z + +``` diff --git a/redfish_utilities/assembly.py b/redfish_utilities/assembly.py index 80cbb15..7c6d5ab 100644 --- a/redfish_utilities/assembly.py +++ b/redfish_utilities/assembly.py @@ -1,177 +1,177 @@ -#! /usr/bin/python -# Copyright Notice: -# Copyright 2019-2025 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md - -""" -Assembly Module - -File : assembly.py - -Brief : This file contains the definitions and functionalities for managing - assemblies on a Redfish service -""" - -from .messages import verify_response - - -class RedfishAssemblyNotFoundError(Exception): - """ - Raised when an assembly index cannot be found - """ - - pass - - -class RedfishAssemblyNoBinaryDataError(Exception): - """ - Raised when an assembly index does not contain binary data - """ - - pass - - -def get_assembly(context, uri): - """ - Collects assembly information from a Redfish service - - Args: - context: The Redfish client object with an open session - uri: The URI of the assembly to get - - Returns: - A list containing all assemblies from the URI - """ - - # Get the assembly - assembly = context.get(uri) - verify_response(assembly) - return assembly.dict.get("Assemblies", []) - - -def print_assembly(assemblies, index=None): - """ - Prints assembly information into a table - - Args: - assemblies: An array of assembly information to print - index: If specified, prints only the desired index - """ - - assembly_format_header = " {:5s} | {} {}" - assembly_format = " {:5s} | {}: {}" - assembly_properties = [ - "Model", - "PartNumber", - "SparePartNumber", - "SKU", - "SerialNumber", - "Producer", - "Vendor", - "ProductionDate", - "Version", - "EngineeringChangeLevel", - ] - - # If an index is specified, isolate to the one index - if index is not None: - if index < 0 or index >= len(assemblies): - raise RedfishAssemblyNotFoundError( - "Assembly contains {} entries; index {} is not valid".format(len(assemblies), index) - ) - assemblies = [assemblies[index]] - - # Go through each assembly - for assembly in assemblies: - # Print the heading - heading_details = [] - state = assembly.get("Status", {}).get("State") - if state: - heading_details.append(state) - health = assembly.get("Status", {}).get("Health") - if health: - heading_details.append(health) - heading_details = ", ".join(heading_details) - if len(heading_details) != 0: - heading_details = "(" + heading_details + ")" - print(assembly_format_header.format(assembly["MemberId"], assembly["Name"], heading_details)) - - # Print any of the found properties - for property in assembly_properties: - if property in assembly: - print(assembly_format.format("", property, assembly[property])) - - -def download_assembly(context, assemblies, filepath, index=None): - """ - Downloads the binary data of an assembly to a file - - Args: - context: The Redfish client object with an open session - assemblies: An array of assembly information - filepath: The filepath to download the binary data - index: The index into the assemblies array to download; if None, perform on index 0 if there's only 1 assembly - """ - - # Get the binary data URI - binary_data_uri = get_assembly_binary_data_uri(assemblies, index) - - # Download the data and save it - response = context.get(binary_data_uri) - verify_response(response) - with open(filepath, "wb") as binary_file: - binary_file.write(response.read) - - -def upload_assembly(context, assemblies, filepath, index=None): - """ - Uploads the binary data of a file to an assembly - - Args: - context: The Redfish client object with an open session - assemblies: An array of assembly information - filepath: The filepath of the binary data to upload - index: The index into the assemblies array to upload; if None, perform on index 0 if there's only 1 assembly - """ - - # Get the binary data URI - binary_data_uri = get_assembly_binary_data_uri(assemblies, index) - - # Upload the binary data - with open(filepath, "rb") as binary_file: - data = binary_file.read() - response = context.put(binary_data_uri, body=data) - verify_response(response) - - -def get_assembly_binary_data_uri(assemblies, index=None): - """ - Locates the binary data URI for a target assembly - - Args: - assemblies: An array of assembly information - index: The index into the assemblies array to download; if None, perform on index 0 if there's only 1 assembly - - Returns: - A string containing the binary data URI - """ - - # If an index is specified, isolate to the one index - if index is None: - index = 0 - if len(assemblies) != 1: - raise RedfishAssemblyNotFoundError( - "Assembly contains {} entries; an index needs to be specified".format(len(assemblies)) - ) - else: - if index < 0 or index >= len(assemblies): - raise RedfishAssemblyNotFoundError( - "Assembly contains {} entries; index {} is not valid".format(len(assemblies), index) - ) - - # Get the binary data URI - binary_data_uri = assemblies[index].get("BinaryDataURI") - if binary_data_uri is None: - # No binary data - raise RedfishAssemblyNoBinaryDataError("Assembly index {} does not contain binary data".format(index)) - return binary_data_uri +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019-2025 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md + +""" +Assembly Module + +File : assembly.py + +Brief : This file contains the definitions and functionalities for managing + assemblies on a Redfish service +""" + +from .messages import verify_response + + +class RedfishAssemblyNotFoundError(Exception): + """ + Raised when an assembly index cannot be found + """ + + pass + + +class RedfishAssemblyNoBinaryDataError(Exception): + """ + Raised when an assembly index does not contain binary data + """ + + pass + + +def get_assembly(context, uri): + """ + Collects assembly information from a Redfish service + + Args: + context: The Redfish client object with an open session + uri: The URI of the assembly to get + + Returns: + A list containing all assemblies from the URI + """ + + # Get the assembly + assembly = context.get(uri) + verify_response(assembly) + return assembly.dict.get("Assemblies", []) + + +def print_assembly(assemblies, index=None): + """ + Prints assembly information into a table + + Args: + assemblies: An array of assembly information to print + index: If specified, prints only the desired index + """ + + assembly_format_header = " {:5s} | {} {}" + assembly_format = " {:5s} | {}: {}" + assembly_properties = [ + "Model", + "PartNumber", + "SparePartNumber", + "SKU", + "SerialNumber", + "Producer", + "Vendor", + "ProductionDate", + "Version", + "EngineeringChangeLevel", + ] + + # If an index is specified, isolate to the one index + if index is not None: + if index < 0 or index >= len(assemblies): + raise RedfishAssemblyNotFoundError( + "Assembly contains {} entries; index {} is not valid".format(len(assemblies), index) + ) + assemblies = [assemblies[index]] + + # Go through each assembly + for assembly in assemblies: + # Print the heading + heading_details = [] + state = assembly.get("Status", {}).get("State") + if state: + heading_details.append(state) + health = assembly.get("Status", {}).get("Health") + if health: + heading_details.append(health) + heading_details = ", ".join(heading_details) + if len(heading_details) != 0: + heading_details = "(" + heading_details + ")" + print(assembly_format_header.format(assembly["MemberId"], assembly["Name"], heading_details)) + + # Print any of the found properties + for property in assembly_properties: + if property in assembly: + print(assembly_format.format("", property, assembly[property])) + + +def download_assembly(context, assemblies, filepath, index=None): + """ + Downloads the binary data of an assembly to a file + + Args: + context: The Redfish client object with an open session + assemblies: An array of assembly information + filepath: The filepath to download the binary data + index: The index into the assemblies array to download; if None, perform on index 0 if there's only 1 assembly + """ + + # Get the binary data URI + binary_data_uri = get_assembly_binary_data_uri(assemblies, index) + + # Download the data and save it + response = context.get(binary_data_uri) + verify_response(response) + with open(filepath, "wb") as binary_file: + binary_file.write(response.read) + + +def upload_assembly(context, assemblies, filepath, index=None): + """ + Uploads the binary data of a file to an assembly + + Args: + context: The Redfish client object with an open session + assemblies: An array of assembly information + filepath: The filepath of the binary data to upload + index: The index into the assemblies array to upload; if None, perform on index 0 if there's only 1 assembly + """ + + # Get the binary data URI + binary_data_uri = get_assembly_binary_data_uri(assemblies, index) + + # Upload the binary data + with open(filepath, "rb") as binary_file: + data = binary_file.read() + response = context.put(binary_data_uri, body=data) + verify_response(response) + + +def get_assembly_binary_data_uri(assemblies, index=None): + """ + Locates the binary data URI for a target assembly + + Args: + assemblies: An array of assembly information + index: The index into the assemblies array to download; if None, perform on index 0 if there's only 1 assembly + + Returns: + A string containing the binary data URI + """ + + # If an index is specified, isolate to the one index + if index is None: + index = 0 + if len(assemblies) != 1: + raise RedfishAssemblyNotFoundError( + "Assembly contains {} entries; an index needs to be specified".format(len(assemblies)) + ) + else: + if index < 0 or index >= len(assemblies): + raise RedfishAssemblyNotFoundError( + "Assembly contains {} entries; index {} is not valid".format(len(assemblies), index) + ) + + # Get the binary data URI + binary_data_uri = assemblies[index].get("BinaryDataURI") + if binary_data_uri is None: + # No binary data + raise RedfishAssemblyNoBinaryDataError("Assembly index {} does not contain binary data".format(index)) + return binary_data_uri diff --git a/redfish_utilities/certificates.py b/redfish_utilities/certificates.py index efb2901..c7a6877 100644 --- a/redfish_utilities/certificates.py +++ b/redfish_utilities/certificates.py @@ -1,351 +1,351 @@ -#! /usr/bin/python -# Copyright Notice: -# Copyright 2019-2025 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md - -""" -Certificates Module - -File : certificates.py - -Brief : This file contains the definitions and functionalities for managing - certificates on a Redfish service -""" - -from .messages import verify_response - - -class RedfishCertificateServiceNotFoundError(Exception): - """ - Raised when the certificate service cannot be found - """ - - pass - - -class RedfishCertificateLocationsNotFoundError(Exception): - """ - Raised when the certificate locations cannot be found - """ - - pass - - -class RedfishGenerateCSRActionNotFoundError(Exception): - """ - Raised when the GenerateCSR action cannot be found - """ - - pass - - -class RedfishReplaceCertificateActionNotFoundError(Exception): - """ - Raised when the ReplaceCertificate action cannot be found - """ - - pass - - -def get_all_certificates(context): - """ - Collects certificate information from a Redfish service - - Args: - context: The Redfish client object with an open session - - Returns: - A list containing all certificates - """ - - certificate_list = [] - - # Get the certificate locations - certificate_service = get_certificate_service(context) - if "CertificateLocations" not in certificate_service: - raise RedfishCertificateLocationsNotFoundError("Service does not contain certificate locations") - certificate_locations = context.get(certificate_service["CertificateLocations"]["@odata.id"]) - verify_response(certificate_locations) - - # Get each member and add it to the response list - locations = certificate_locations.dict.get("Links", {}).get("Certificates", []) - for certificate_ref in locations: - certificate = context.get(certificate_ref["@odata.id"]) - verify_response(certificate) - certificate_info = { - "URI": certificate.dict["@odata.id"], - "Id": certificate.dict["Id"], - "Subject": certificate.dict.get("Subject", {}), - "Issuer": certificate.dict.get("Issuer", {}), - "ValidNotBefore": certificate.dict.get("ValidNotBefore"), - "ValidNotAfter": certificate.dict.get("ValidNotAfter"), - "KeyUsage": certificate.dict.get("KeyUsage"), - "SerialNumber": certificate.dict.get("SerialNumber"), - "Fingerprint": certificate.dict.get("Fingerprint"), - "FingerprintHashAlgorithm": certificate.dict.get("FingerprintHashAlgorithm"), - "SignatureAlgorithm": certificate.dict.get("SignatureAlgorithm"), - "CertificateUsageTypes": certificate.dict.get("CertificateUsageTypes"), - } - certificate_list.append(certificate_info) - - return certificate_list - - -def print_certificates(certificate_list, details=False): - """ - Prints the certificate list into a table - - Args: - certificate_list: The certificate list to print - details: True to print all the detailed info - """ - - # Go through each certificate - for certificate in certificate_list: - print("Certificate: {}".format(certificate["URI"])) - print(" Subject: {}".format(build_identifier_string(certificate["Subject"]))) - print(" Issuer: {}".format(build_identifier_string(certificate["Issuer"]))) - print( - " Valid Not Before: {}, Valid Not After: {}".format( - certificate["ValidNotBefore"], certificate["ValidNotAfter"] - ) - ) - if details: - if certificate["KeyUsage"] is not None: - print(" Key Usage: {}".format(", ".join(certificate["KeyUsage"]))) - if certificate["CertificateUsageTypes"] is not None: - print(" Certificate Usage: {}".format(", ".join(certificate["CertificateUsageTypes"]))) - if certificate["SerialNumber"] is not None: - print(" Serial Number: {}".format(certificate["SerialNumber"])) - if certificate["Fingerprint"] is not None: - print(" Fingerprint: {}".format(certificate["Fingerprint"])) - if certificate["FingerprintHashAlgorithm"] is not None: - print(" Fingerprint Hash Algorithm: {}".format(certificate["FingerprintHashAlgorithm"])) - if certificate["SignatureAlgorithm"] is not None: - print(" Signature Algorithm: {}".format(certificate["SignatureAlgorithm"])) - print("") - - -def get_generate_csr_info(context): - """ - Finds information about the support for generating CSRs - - Args: - context: The Redfish client object with an open session - - Returns: - The URI of the GenerateCSR action - A list of parameter requirements from the action info - """ - - # Check that there is a GenerateCSR action - certificate_service = get_certificate_service(context) - if "Actions" not in certificate_service: - raise RedfishGenerateCSRActionNotFoundError("Service does not support the GenerateCSR action") - if "#CertificateService.GenerateCSR" not in certificate_service["Actions"]: - raise RedfishGenerateCSRActionNotFoundError("Service does not support the GenerateCSR action") - - # Extract the info about the GenerateCSR action - generate_csr_action = certificate_service["Actions"]["#CertificateService.GenerateCSR"] - generate_csr_uri = generate_csr_action["target"] - - if "@Redfish.ActionInfo" not in generate_csr_action: - # No action info; due to the action's complexity, don't try to produce a default set of parameters - generate_csr_parameters = None - else: - # Get the action info and its parameter listing - action_info = context.get(generate_csr_action["@Redfish.ActionInfo"]) - generate_csr_parameters = action_info.dict["Parameters"] - - return generate_csr_uri, generate_csr_parameters - - -def generate_csr( - context, - common_name, - organization, - organizational_unit, - city, - state, - country, - cert_col, - email=None, - key_pair_alg=None, - key_bit_len=None, - key_curve_id=None, -): - """ - Generates a certificate signing request - - Args: - context: The Redfish client object with an open session - common_name: The common name of the component to secure - organization: The name of the organization making the request - organizational_unit: The name of the unit in the organization making the request - city: The city or locality of the organization making the request - state: The state, province, or region of the organization making the request - country: The two-letter country code of the organization making the request - cert_col: The URI of the certificate collection where the signed certificate will be installed - email: The email address of the contact within the organization making the request - key_pair_alg: The type of key-pair for use with signing algorithms - key_bit_len: The length of the key, in bits, if the key pair algorithm supports key size - key_curve_id: The curve ID to use with the key if the key pair algorithm supports curves - - Returns: - The response of the action - """ - - # Locate the GenerateCSR action - generate_csr_uri, generate_csr_parameters = get_generate_csr_info(context) - - # Build the payload - payload = { - "CertificateCollection": {"@odata.id": cert_col}, - "CommonName": common_name, - "Organization": organization, - "OrganizationalUnit": organizational_unit, - "City": city, - "State": state, - "Country": country, - } - if email is not None: - payload["Email"] = email - if key_pair_alg is not None: - payload["KeyPairAlgorithm"] = key_pair_alg - if key_bit_len is not None: - payload["KeyBitLength"] = key_bit_len - if key_curve_id is not None: - payload["KeyCurveId"] = key_curve_id - - # Generate a CSR - response = context.post(generate_csr_uri, body=payload) - verify_response(response) - return response - - -def install_certificate(context, destination, cert_file, key_file=None): - """ - Replaces an existing certificate with a new certificate - - Args: - context: The Redfish client object with an open session - destination: The installation URI of the certificate; either a certificate collection to insert, or an existing certificate to replace - cert_file: The file, and optional path, of the certificate to install - key_file: The file, and optional path, of the private key for the certificate to install - - Returns: - The response of the operation - """ - - # Read the certificate and determine its type - with open(cert_file, "r") as file_handle: - cert_string = file_handle.read() - cert_type = "PEM" - if "BEGIN PKCS7" in cert_string: - cert_type = "PKCS7" - elif cert_string.count("BEGIN CERTIFICATE") > 1: - cert_type = "PEMchain" - - # Read the key if needed, and prepend it to the certificate - if key_file is not None: - with open(key_file, "r") as file_handle: - key_string = file_handle.read() - cert_string = key_string + cert_string - - # Build the payload - payload = { - "CertificateUri": {"@odata.id": destination}, - "CertificateString": cert_string, - "CertificateType": cert_type, - } - - # Check if the referenced destination is a certificate collection or individual certificate - dest_response = context.get(destination) - verify_response(dest_response) - if dest_response.dict["@odata.type"] == "#CertificateCollection.CertificateCollection": - # Certificate collection; just perform a POST operation on the collection - payload.pop("CertificateUri") - else: - # Individual certificate; perform a replacement operation from the certificate service - - # Locate the ReplaceCertificate action - certificate_service = get_certificate_service(context) - if "Actions" not in certificate_service: - raise RedfishReplaceCertificateActionNotFoundError("Service does not support the ReplaceCertificate action") - if "#CertificateService.ReplaceCertificate" not in certificate_service["Actions"]: - raise RedfishReplaceCertificateActionNotFoundError("Service does not support the ReplaceCertificate action") - destination = certificate_service["Actions"]["#CertificateService.ReplaceCertificate"]["target"] - - # Install the certificate - response = context.post(destination, body=payload) - verify_response(response) - return response - - -def delete_certificate(context, certificate): - """ - Replaces an existing certificate with a new certificate - - Args: - context: The Redfish client object with an open session - certificate: The URI of the certificate to delete - - Returns: - The response of the operation - """ - - # Delete the certificate - response = context.delete(certificate) - verify_response(response) - return response - - -def get_certificate_service(context): - """ - Collects the certificate service information from a Redfish service - - Args: - context: The Redfish client object with an open session - - Returns: - An object containing information about the certificate service - """ - - # Get the service root to find the certificate service - service_root = context.get("/redfish/v1/") - if "CertificateService" not in service_root.dict: - # No event service - raise RedfishCertificateServiceNotFoundError("Service does not contain a certificate service") - - # Get the certificate service - certificate_service = context.get(service_root.dict["CertificateService"]["@odata.id"]) - verify_response(certificate_service) - return certificate_service.dict - - -def build_identifier_string(identifier): - """ - Creates an identifier string for a subject or issuer object - - Args: - identifier: The identifier object to parse - - Returns: - A string containing the identifier info - """ - - field_map = { - "CommonName": "CN", - "Organization": "O", - "OrganizationalUnit": "OU", - "Country": "C", - "State": "ST", - "City": "L", - "Email": "emailAddress", - } - - strings = [] - for map in field_map: - if map in identifier and identifier[map] is not None: - strings.append("{}={}".format(field_map[map], identifier[map])) - return ", ".join(strings) +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019-2025 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md + +""" +Certificates Module + +File : certificates.py + +Brief : This file contains the definitions and functionalities for managing + certificates on a Redfish service +""" + +from .messages import verify_response + + +class RedfishCertificateServiceNotFoundError(Exception): + """ + Raised when the certificate service cannot be found + """ + + pass + + +class RedfishCertificateLocationsNotFoundError(Exception): + """ + Raised when the certificate locations cannot be found + """ + + pass + + +class RedfishGenerateCSRActionNotFoundError(Exception): + """ + Raised when the GenerateCSR action cannot be found + """ + + pass + + +class RedfishReplaceCertificateActionNotFoundError(Exception): + """ + Raised when the ReplaceCertificate action cannot be found + """ + + pass + + +def get_all_certificates(context): + """ + Collects certificate information from a Redfish service + + Args: + context: The Redfish client object with an open session + + Returns: + A list containing all certificates + """ + + certificate_list = [] + + # Get the certificate locations + certificate_service = get_certificate_service(context) + if "CertificateLocations" not in certificate_service: + raise RedfishCertificateLocationsNotFoundError("Service does not contain certificate locations") + certificate_locations = context.get(certificate_service["CertificateLocations"]["@odata.id"]) + verify_response(certificate_locations) + + # Get each member and add it to the response list + locations = certificate_locations.dict.get("Links", {}).get("Certificates", []) + for certificate_ref in locations: + certificate = context.get(certificate_ref["@odata.id"]) + verify_response(certificate) + certificate_info = { + "URI": certificate.dict["@odata.id"], + "Id": certificate.dict["Id"], + "Subject": certificate.dict.get("Subject", {}), + "Issuer": certificate.dict.get("Issuer", {}), + "ValidNotBefore": certificate.dict.get("ValidNotBefore"), + "ValidNotAfter": certificate.dict.get("ValidNotAfter"), + "KeyUsage": certificate.dict.get("KeyUsage"), + "SerialNumber": certificate.dict.get("SerialNumber"), + "Fingerprint": certificate.dict.get("Fingerprint"), + "FingerprintHashAlgorithm": certificate.dict.get("FingerprintHashAlgorithm"), + "SignatureAlgorithm": certificate.dict.get("SignatureAlgorithm"), + "CertificateUsageTypes": certificate.dict.get("CertificateUsageTypes"), + } + certificate_list.append(certificate_info) + + return certificate_list + + +def print_certificates(certificate_list, details=False): + """ + Prints the certificate list into a table + + Args: + certificate_list: The certificate list to print + details: True to print all the detailed info + """ + + # Go through each certificate + for certificate in certificate_list: + print("Certificate: {}".format(certificate["URI"])) + print(" Subject: {}".format(build_identifier_string(certificate["Subject"]))) + print(" Issuer: {}".format(build_identifier_string(certificate["Issuer"]))) + print( + " Valid Not Before: {}, Valid Not After: {}".format( + certificate["ValidNotBefore"], certificate["ValidNotAfter"] + ) + ) + if details: + if certificate["KeyUsage"] is not None: + print(" Key Usage: {}".format(", ".join(certificate["KeyUsage"]))) + if certificate["CertificateUsageTypes"] is not None: + print(" Certificate Usage: {}".format(", ".join(certificate["CertificateUsageTypes"]))) + if certificate["SerialNumber"] is not None: + print(" Serial Number: {}".format(certificate["SerialNumber"])) + if certificate["Fingerprint"] is not None: + print(" Fingerprint: {}".format(certificate["Fingerprint"])) + if certificate["FingerprintHashAlgorithm"] is not None: + print(" Fingerprint Hash Algorithm: {}".format(certificate["FingerprintHashAlgorithm"])) + if certificate["SignatureAlgorithm"] is not None: + print(" Signature Algorithm: {}".format(certificate["SignatureAlgorithm"])) + print("") + + +def get_generate_csr_info(context): + """ + Finds information about the support for generating CSRs + + Args: + context: The Redfish client object with an open session + + Returns: + The URI of the GenerateCSR action + A list of parameter requirements from the action info + """ + + # Check that there is a GenerateCSR action + certificate_service = get_certificate_service(context) + if "Actions" not in certificate_service: + raise RedfishGenerateCSRActionNotFoundError("Service does not support the GenerateCSR action") + if "#CertificateService.GenerateCSR" not in certificate_service["Actions"]: + raise RedfishGenerateCSRActionNotFoundError("Service does not support the GenerateCSR action") + + # Extract the info about the GenerateCSR action + generate_csr_action = certificate_service["Actions"]["#CertificateService.GenerateCSR"] + generate_csr_uri = generate_csr_action["target"] + + if "@Redfish.ActionInfo" not in generate_csr_action: + # No action info; due to the action's complexity, don't try to produce a default set of parameters + generate_csr_parameters = None + else: + # Get the action info and its parameter listing + action_info = context.get(generate_csr_action["@Redfish.ActionInfo"]) + generate_csr_parameters = action_info.dict["Parameters"] + + return generate_csr_uri, generate_csr_parameters + + +def generate_csr( + context, + common_name, + organization, + organizational_unit, + city, + state, + country, + cert_col, + email=None, + key_pair_alg=None, + key_bit_len=None, + key_curve_id=None, +): + """ + Generates a certificate signing request + + Args: + context: The Redfish client object with an open session + common_name: The common name of the component to secure + organization: The name of the organization making the request + organizational_unit: The name of the unit in the organization making the request + city: The city or locality of the organization making the request + state: The state, province, or region of the organization making the request + country: The two-letter country code of the organization making the request + cert_col: The URI of the certificate collection where the signed certificate will be installed + email: The email address of the contact within the organization making the request + key_pair_alg: The type of key-pair for use with signing algorithms + key_bit_len: The length of the key, in bits, if the key pair algorithm supports key size + key_curve_id: The curve ID to use with the key if the key pair algorithm supports curves + + Returns: + The response of the action + """ + + # Locate the GenerateCSR action + generate_csr_uri, generate_csr_parameters = get_generate_csr_info(context) + + # Build the payload + payload = { + "CertificateCollection": {"@odata.id": cert_col}, + "CommonName": common_name, + "Organization": organization, + "OrganizationalUnit": organizational_unit, + "City": city, + "State": state, + "Country": country, + } + if email is not None: + payload["Email"] = email + if key_pair_alg is not None: + payload["KeyPairAlgorithm"] = key_pair_alg + if key_bit_len is not None: + payload["KeyBitLength"] = key_bit_len + if key_curve_id is not None: + payload["KeyCurveId"] = key_curve_id + + # Generate a CSR + response = context.post(generate_csr_uri, body=payload) + verify_response(response) + return response + + +def install_certificate(context, destination, cert_file, key_file=None): + """ + Replaces an existing certificate with a new certificate + + Args: + context: The Redfish client object with an open session + destination: The installation URI of the certificate; either a certificate collection to insert, or an existing certificate to replace + cert_file: The file, and optional path, of the certificate to install + key_file: The file, and optional path, of the private key for the certificate to install + + Returns: + The response of the operation + """ + + # Read the certificate and determine its type + with open(cert_file, "r") as file_handle: + cert_string = file_handle.read() + cert_type = "PEM" + if "BEGIN PKCS7" in cert_string: + cert_type = "PKCS7" + elif cert_string.count("BEGIN CERTIFICATE") > 1: + cert_type = "PEMchain" + + # Read the key if needed, and prepend it to the certificate + if key_file is not None: + with open(key_file, "r") as file_handle: + key_string = file_handle.read() + cert_string = key_string + cert_string + + # Build the payload + payload = { + "CertificateUri": {"@odata.id": destination}, + "CertificateString": cert_string, + "CertificateType": cert_type, + } + + # Check if the referenced destination is a certificate collection or individual certificate + dest_response = context.get(destination) + verify_response(dest_response) + if dest_response.dict["@odata.type"] == "#CertificateCollection.CertificateCollection": + # Certificate collection; just perform a POST operation on the collection + payload.pop("CertificateUri") + else: + # Individual certificate; perform a replacement operation from the certificate service + + # Locate the ReplaceCertificate action + certificate_service = get_certificate_service(context) + if "Actions" not in certificate_service: + raise RedfishReplaceCertificateActionNotFoundError("Service does not support the ReplaceCertificate action") + if "#CertificateService.ReplaceCertificate" not in certificate_service["Actions"]: + raise RedfishReplaceCertificateActionNotFoundError("Service does not support the ReplaceCertificate action") + destination = certificate_service["Actions"]["#CertificateService.ReplaceCertificate"]["target"] + + # Install the certificate + response = context.post(destination, body=payload) + verify_response(response) + return response + + +def delete_certificate(context, certificate): + """ + Replaces an existing certificate with a new certificate + + Args: + context: The Redfish client object with an open session + certificate: The URI of the certificate to delete + + Returns: + The response of the operation + """ + + # Delete the certificate + response = context.delete(certificate) + verify_response(response) + return response + + +def get_certificate_service(context): + """ + Collects the certificate service information from a Redfish service + + Args: + context: The Redfish client object with an open session + + Returns: + An object containing information about the certificate service + """ + + # Get the service root to find the certificate service + service_root = context.get("/redfish/v1/") + if "CertificateService" not in service_root.dict: + # No event service + raise RedfishCertificateServiceNotFoundError("Service does not contain a certificate service") + + # Get the certificate service + certificate_service = context.get(service_root.dict["CertificateService"]["@odata.id"]) + verify_response(certificate_service) + return certificate_service.dict + + +def build_identifier_string(identifier): + """ + Creates an identifier string for a subject or issuer object + + Args: + identifier: The identifier object to parse + + Returns: + A string containing the identifier info + """ + + field_map = { + "CommonName": "CN", + "Organization": "O", + "OrganizationalUnit": "OU", + "Country": "C", + "State": "ST", + "City": "L", + "Email": "emailAddress", + } + + strings = [] + for map in field_map: + if map in identifier and identifier[map] is not None: + strings.append("{}={}".format(field_map[map], identifier[map])) + return ", ".join(strings) diff --git a/scripts/rf_assembly.py b/scripts/rf_assembly.py index d30b111..0db79e6 100644 --- a/scripts/rf_assembly.py +++ b/scripts/rf_assembly.py @@ -1,85 +1,85 @@ -#! /usr/bin/python -# Copyright Notice: -# Copyright 2019-2025 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md - -""" -Redfish Assembly - -File : rf_assembly.py - -Brief : This script uses the redfish_utilities module to manage assemblies -""" - -import argparse -import datetime -import logging -import redfish -import redfish_utilities -import traceback -import sys -from redfish.messages import RedfishPasswordChangeRequiredError - -# Get the input arguments -argget = argparse.ArgumentParser(description="A tool to manage assemblies on a Redfish service") -argget.add_argument("--user", "-u", type=str, required=True, help="The user name for authentication") -argget.add_argument("--password", "-p", type=str, required=True, help="The password for authentication") -argget.add_argument("--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)") -argget.add_argument("--assembly", "-a", type=str, required=True, help="The URI of the target assembly") -argget.add_argument("--index", "-i", type=int, help="The target assembly index") -argget.add_argument("--debug", action="store_true", help="Creates debug file showing HTTP traces and exceptions") -subparsers = argget.add_subparsers(dest="command") -info_argget = subparsers.add_parser("info", help="Displays information about the an assembly") -download_argget = subparsers.add_parser("download", help="Downloads assembly data to a file") -download_argget.add_argument( - "--file", "-f", type=str, required=True, help="The file, and optional path, to save the assembly data" -) -upload_argget = subparsers.add_parser("upload", help="Uploads assembly data from a file") -upload_argget.add_argument( - "--file", "-f", type=str, required=True, help="The file, and optional path, containing the assembly data to upload" -) -args = argget.parse_args() - -if args.index and args.index < 0: - print("rf_assembly.py: error: the assembly index cannot be negative") - sys.exit(1) - -if args.debug: - log_file = "rf_assembly-{}.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("rf_assembly Trace") - -# Set up the Redfish object -redfish_obj = None -try: - redfish_obj = redfish.redfish_client( - base_url=args.rhost, username=args.user, password=args.password, timeout=15, max_retry=3 - ) - redfish_obj.login(auth="session") -except RedfishPasswordChangeRequiredError: - redfish_utilities.print_password_change_required_and_logout(redfish_obj, args) - sys.exit(1) -except Exception: - raise - -exit_code = 0 -try: - assembly_info = redfish_utilities.get_assembly(redfish_obj, args.assembly) - if args.command == "download": - print("Saving data to '{}'...".format(args.file)) - redfish_utilities.download_assembly(redfish_obj, assembly_info, args.file, args.index) - elif args.command == "upload": - print("Writing data from '{}'...".format(args.file)) - redfish_utilities.upload_assembly(redfish_obj, assembly_info, args.file, args.index) - else: - redfish_utilities.print_assembly(assembly_info, args.index) -except Exception as e: - if args.debug: - logger.error("Caught exception:\n\n{}\n".format(traceback.format_exc())) - exit_code = 1 - print(e) -finally: - # Log out - redfish_utilities.logout(redfish_obj) -sys.exit(exit_code) +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019-2025 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md + +""" +Redfish Assembly + +File : rf_assembly.py + +Brief : This script uses the redfish_utilities module to manage assemblies +""" + +import argparse +import datetime +import logging +import redfish +import redfish_utilities +import traceback +import sys +from redfish.messages import RedfishPasswordChangeRequiredError + +# Get the input arguments +argget = argparse.ArgumentParser(description="A tool to manage assemblies on a Redfish service") +argget.add_argument("--user", "-u", type=str, required=True, help="The user name for authentication") +argget.add_argument("--password", "-p", type=str, required=True, help="The password for authentication") +argget.add_argument("--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)") +argget.add_argument("--assembly", "-a", type=str, required=True, help="The URI of the target assembly") +argget.add_argument("--index", "-i", type=int, help="The target assembly index") +argget.add_argument("--debug", action="store_true", help="Creates debug file showing HTTP traces and exceptions") +subparsers = argget.add_subparsers(dest="command") +info_argget = subparsers.add_parser("info", help="Displays information about the an assembly") +download_argget = subparsers.add_parser("download", help="Downloads assembly data to a file") +download_argget.add_argument( + "--file", "-f", type=str, required=True, help="The file, and optional path, to save the assembly data" +) +upload_argget = subparsers.add_parser("upload", help="Uploads assembly data from a file") +upload_argget.add_argument( + "--file", "-f", type=str, required=True, help="The file, and optional path, containing the assembly data to upload" +) +args = argget.parse_args() + +if args.index and args.index < 0: + print("rf_assembly.py: error: the assembly index cannot be negative") + sys.exit(1) + +if args.debug: + log_file = "rf_assembly-{}.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("rf_assembly Trace") + +# Set up the Redfish object +redfish_obj = None +try: + redfish_obj = redfish.redfish_client( + base_url=args.rhost, username=args.user, password=args.password, timeout=15, max_retry=3 + ) + redfish_obj.login(auth="session") +except RedfishPasswordChangeRequiredError: + redfish_utilities.print_password_change_required_and_logout(redfish_obj, args) + sys.exit(1) +except Exception: + raise + +exit_code = 0 +try: + assembly_info = redfish_utilities.get_assembly(redfish_obj, args.assembly) + if args.command == "download": + print("Saving data to '{}'...".format(args.file)) + redfish_utilities.download_assembly(redfish_obj, assembly_info, args.file, args.index) + elif args.command == "upload": + print("Writing data from '{}'...".format(args.file)) + redfish_utilities.upload_assembly(redfish_obj, assembly_info, args.file, args.index) + else: + redfish_utilities.print_assembly(assembly_info, args.index) +except Exception as e: + if args.debug: + logger.error("Caught exception:\n\n{}\n".format(traceback.format_exc())) + exit_code = 1 + print(e) +finally: + # Log out + redfish_utilities.logout(redfish_obj) +sys.exit(exit_code) diff --git a/scripts/rf_firmware_inventory.py b/scripts/rf_firmware_inventory.py index 35e0709..70ee67c 100644 --- a/scripts/rf_firmware_inventory.py +++ b/scripts/rf_firmware_inventory.py @@ -1,66 +1,66 @@ -#! /usr/bin/python -# Copyright Notice: -# Copyright 2019-2025 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md - -""" -Redfish Firmware Inventory - -File : rf_firmware_inventory.py - -Brief : This script uses the redfish_utilities module to manage firmware inventory -""" - -import argparse -import datetime -import logging -import redfish -import redfish_utilities -import traceback -import sys -from redfish.messages import RedfishPasswordChangeRequiredError - -# Get the input arguments -argget = argparse.ArgumentParser(description="A tool to collect firmware inventory from a Redfish service") -argget.add_argument("--user", "-u", type=str, required=True, help="The user name for authentication") -argget.add_argument("--password", "-p", type=str, required=True, help="The password for authentication") -argget.add_argument("--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)") -argget.add_argument( - "--details", "-details", action="store_true", help="Indicates details to be shown for each firmware entry" -) -argget.add_argument("--id", "-i", action="store_true", help="Construct inventory names using 'Id' values") -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 = "rf_firmware_inventory-{}.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("rf_firmware_inventory Trace") - -# Set up the Redfish object -redfish_obj = None -try: - redfish_obj = redfish.redfish_client( - base_url=args.rhost, username=args.user, password=args.password, timeout=15, max_retry=3 - ) - redfish_obj.login(auth="session") -except RedfishPasswordChangeRequiredError: - redfish_utilities.print_password_change_required_and_logout(redfish_obj, args) - sys.exit(1) -except Exception: - raise - -exit_code = 0 -try: - firmware_inventory = redfish_utilities.get_firmware_inventory(redfish_obj) - redfish_utilities.print_software_inventory(firmware_inventory, details=args.details, use_id=args.id) -except Exception as e: - if args.debug: - logger.error("Caught exception:\n\n{}\n".format(traceback.format_exc())) - exit_code = 1 - print(e) -finally: - # Log out - redfish_utilities.logout(redfish_obj) -sys.exit(exit_code) +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019-2025 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md + +""" +Redfish Firmware Inventory + +File : rf_firmware_inventory.py + +Brief : This script uses the redfish_utilities module to manage firmware inventory +""" + +import argparse +import datetime +import logging +import redfish +import redfish_utilities +import traceback +import sys +from redfish.messages import RedfishPasswordChangeRequiredError + +# Get the input arguments +argget = argparse.ArgumentParser(description="A tool to collect firmware inventory from a Redfish service") +argget.add_argument("--user", "-u", type=str, required=True, help="The user name for authentication") +argget.add_argument("--password", "-p", type=str, required=True, help="The password for authentication") +argget.add_argument("--rhost", "-r", type=str, required=True, help="The address of the Redfish service (with scheme)") +argget.add_argument( + "--details", "-details", action="store_true", help="Indicates details to be shown for each firmware entry" +) +argget.add_argument("--id", "-i", action="store_true", help="Construct inventory names using 'Id' values") +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 = "rf_firmware_inventory-{}.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("rf_firmware_inventory Trace") + +# Set up the Redfish object +redfish_obj = None +try: + redfish_obj = redfish.redfish_client( + base_url=args.rhost, username=args.user, password=args.password, timeout=15, max_retry=3 + ) + redfish_obj.login(auth="session") +except RedfishPasswordChangeRequiredError: + redfish_utilities.print_password_change_required_and_logout(redfish_obj, args) + sys.exit(1) +except Exception: + raise + +exit_code = 0 +try: + firmware_inventory = redfish_utilities.get_firmware_inventory(redfish_obj) + redfish_utilities.print_software_inventory(firmware_inventory, details=args.details, use_id=args.id) +except Exception as e: + if args.debug: + logger.error("Caught exception:\n\n{}\n".format(traceback.format_exc())) + exit_code = 1 + print(e) +finally: + # Log out + redfish_utilities.logout(redfish_obj) +sys.exit(exit_code) diff --git a/scripts/rf_test_event_listener.py b/scripts/rf_test_event_listener.py index b77db4b..9341568 100644 --- a/scripts/rf_test_event_listener.py +++ b/scripts/rf_test_event_listener.py @@ -1,111 +1,111 @@ -#! /usr/bin/python -# Copyright Notice: -# Copyright 2019-2025 DMTF. All rights reserved. -# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md - -""" -Redfish Test Event Listener - -File : rf_test_event_listener.py - -Brief : This script performs POST operations with event payloads to help verify - an event listener. -""" - -import argparse -import json -import requests -from datetime import datetime - -event_headers = {"Content-Type": "application/json"} - -event_payload = { - "@odata.type": "#Event.v1_7_0.Event", - "Id": "1", - "Name": "Sample Event", - "Context": "Sample Event for Listener", - "Events": [ - { - "EventType": "Other", - "EventId": "1", - "Severity": "OK", - "MessageSeverity": "OK", - "Message": "Test message.", - "MessageId": "Resource.1.3.TestMessage", - } - ], -} - -argget = argparse.ArgumentParser(description="A tool to help verify a Redfish event listener") -argget.add_argument( - "--listener", "-l", type=str, required=True, help="The absolute URI of the Redfish event listener (with scheme)" -) -argget.add_argument( - "--file", - "-file", - type=str, - help="The filepath to a JSON file containing the event payload; if this argument is specified, all other arguments controlling the event data is ignored", -) -argget.add_argument("--id", "-id", type=str, help="The value to specify in the Id property of the event") -argget.add_argument("--name", "-name", type=str, help="The value to specify in the Name property of the event") -argget.add_argument("--context", "-context", type=str, help="The value to specify in the Context property of the event") -argget.add_argument( - "--eventtype", "-eventtype", type=str, help="The value to specify in the EventType property of the event" -) -argget.add_argument("--eventid", "-eventid", type=str, help="The value to specify in the EventId property of the event") -argget.add_argument( - "--severity", "-severity", type=str, help="The value to specify in the Severity property of the event" -) -argget.add_argument("--message", "-message", type=str, help="The value to specify in the Message property of the event") -argget.add_argument( - "--messageid", "-messageid", type=str, help="The value to specify in the MessageId property of the event" -) -argget.add_argument( - "--timestamp", "-timestamp", type=str, help="The value to specify in the EventTimestamp property of the event" -) -argget.add_argument( - "--header", - "-header", - type=str, - nargs=2, - metavar=("name", "value"), - action="append", - help="Name-value pairs of HTTP headers to provide with the request", -) -args = argget.parse_args() - -# Update the event payload based on the specified arguments -if args.file: - with open(args.file) as json_file: - event_payload = json.load(json_file) -else: - if args.id: - event_payload["Id"] = args.id - if args.name: - event_payload["Name"] = args.name - if args.context: - event_payload["Context"] = args.context - if args.eventtype: - event_payload["Events"][0]["EventType"] = args.eventtype - if args.eventid: - event_payload["Events"][0]["EventId"] = args.eventid - if args.severity: - event_payload["Events"][0]["Severity"] = args.severity - event_payload["Events"][0]["MessageSeverity"] = args.severity - if args.message: - event_payload["Events"][0]["Message"] = args.message - if args.messageid: - event_payload["Events"][0]["MessageId"] = args.messageid - if args.timestamp: - event_payload["Events"][0]["EventTimestamp"] = args.timestamp - else: - event_payload["Events"][0]["EventTimestamp"] = datetime.now().replace(microsecond=0).astimezone().isoformat() - -# Update the HTTP headers based on the specified arguments -if args.header: - for header in args.header: - event_headers[header[0]] = header[1] - -# Send the request -response = requests.post(args.listener, json=event_payload, headers=event_headers, timeout=15, verify=False) -print("Listener responded with {} {}".format(response.status_code, response.reason)) +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019-2025 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md + +""" +Redfish Test Event Listener + +File : rf_test_event_listener.py + +Brief : This script performs POST operations with event payloads to help verify + an event listener. +""" + +import argparse +import json +import requests +from datetime import datetime + +event_headers = {"Content-Type": "application/json"} + +event_payload = { + "@odata.type": "#Event.v1_7_0.Event", + "Id": "1", + "Name": "Sample Event", + "Context": "Sample Event for Listener", + "Events": [ + { + "EventType": "Other", + "EventId": "1", + "Severity": "OK", + "MessageSeverity": "OK", + "Message": "Test message.", + "MessageId": "Resource.1.3.TestMessage", + } + ], +} + +argget = argparse.ArgumentParser(description="A tool to help verify a Redfish event listener") +argget.add_argument( + "--listener", "-l", type=str, required=True, help="The absolute URI of the Redfish event listener (with scheme)" +) +argget.add_argument( + "--file", + "-file", + type=str, + help="The filepath to a JSON file containing the event payload; if this argument is specified, all other arguments controlling the event data is ignored", +) +argget.add_argument("--id", "-id", type=str, help="The value to specify in the Id property of the event") +argget.add_argument("--name", "-name", type=str, help="The value to specify in the Name property of the event") +argget.add_argument("--context", "-context", type=str, help="The value to specify in the Context property of the event") +argget.add_argument( + "--eventtype", "-eventtype", type=str, help="The value to specify in the EventType property of the event" +) +argget.add_argument("--eventid", "-eventid", type=str, help="The value to specify in the EventId property of the event") +argget.add_argument( + "--severity", "-severity", type=str, help="The value to specify in the Severity property of the event" +) +argget.add_argument("--message", "-message", type=str, help="The value to specify in the Message property of the event") +argget.add_argument( + "--messageid", "-messageid", type=str, help="The value to specify in the MessageId property of the event" +) +argget.add_argument( + "--timestamp", "-timestamp", type=str, help="The value to specify in the EventTimestamp property of the event" +) +argget.add_argument( + "--header", + "-header", + type=str, + nargs=2, + metavar=("name", "value"), + action="append", + help="Name-value pairs of HTTP headers to provide with the request", +) +args = argget.parse_args() + +# Update the event payload based on the specified arguments +if args.file: + with open(args.file) as json_file: + event_payload = json.load(json_file) +else: + if args.id: + event_payload["Id"] = args.id + if args.name: + event_payload["Name"] = args.name + if args.context: + event_payload["Context"] = args.context + if args.eventtype: + event_payload["Events"][0]["EventType"] = args.eventtype + if args.eventid: + event_payload["Events"][0]["EventId"] = args.eventid + if args.severity: + event_payload["Events"][0]["Severity"] = args.severity + event_payload["Events"][0]["MessageSeverity"] = args.severity + if args.message: + event_payload["Events"][0]["Message"] = args.message + if args.messageid: + event_payload["Events"][0]["MessageId"] = args.messageid + if args.timestamp: + event_payload["Events"][0]["EventTimestamp"] = args.timestamp + else: + event_payload["Events"][0]["EventTimestamp"] = datetime.now().replace(microsecond=0).astimezone().isoformat() + +# Update the HTTP headers based on the specified arguments +if args.header: + for header in args.header: + event_headers[header[0]] = header[1] + +# Send the request +response = requests.post(args.listener, json=event_payload, headers=event_headers, timeout=15, verify=False) +print("Listener responded with {} {}".format(response.status_code, response.reason)) From 5b8393210f1424e5224d368dc8cdd3b179414f9e Mon Sep 17 00:00:00 2001 From: GitHub Release Workflow <> Date: Mon, 21 Apr 2025 14:19:34 +0000 Subject: [PATCH 11/17] 3.3.9 versioning Signed-off-by: GitHub Release Workflow <> Signed-off-by: Brandon Biggs --- CHANGELOG.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5a8db..40e1e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## [3.3.9] - 2025-04-21 +- Fixed files to always be Unix format + ## [3.3.8] - 2025-01-24 - Added 'rf_sel.py' script to simplify the ability for a user to find the SEL on a service diff --git a/setup.py b/setup.py index 22a0172..62a7e5d 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ def run(self): setup( name="redfish_utilities", - version="3.3.8", + version="3.3.9", description="Redfish Utilities", long_description=long_description, long_description_content_type="text/markdown", From a7ba74b6024fbc96900158f08f19f277df1a90aa Mon Sep 17 00:00:00 2001 From: Mike Raineri Date: Thu, 1 May 2025 15:54:24 -0400 Subject: [PATCH 12/17] Corrected the usage of the 'DateTimeLocalOffset' property when setting a manager's time Signed-off-by: Mike Raineri Signed-off-by: Brandon Biggs --- redfish_utilities/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redfish_utilities/managers.py b/redfish_utilities/managers.py index 801fdcf..3c9e9b8 100644 --- a/redfish_utilities/managers.py +++ b/redfish_utilities/managers.py @@ -145,7 +145,7 @@ def set_manager(context, manager_id=None, date_time=None, date_time_offset=None) if date_time is not None: payload["DateTime"] = date_time if date_time_offset is not None: - payload["DateTimeOffset"] = date_time_offset + payload["DateTimeLocalOffset"] = date_time_offset # Update the manager headers = None From 158351e56c2512d67d335d49c6ee9f6aaf0ad382 Mon Sep 17 00:00:00 2001 From: GitHub Release Workflow <> Date: Fri, 2 May 2025 19:43:17 +0000 Subject: [PATCH 13/17] 3.4.0 versioning Signed-off-by: GitHub Release Workflow <> Signed-off-by: Brandon Biggs --- CHANGELOG.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e1e13..f29fef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## [3.4.0] - 2025-05-02 +- Corrected the usage of the 'DateTimeLocalOffset' property when setting a manager's time + ## [3.3.9] - 2025-04-21 - Fixed files to always be Unix format diff --git a/setup.py b/setup.py index 62a7e5d..9a8b2d9 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ def run(self): setup( name="redfish_utilities", - version="3.3.9", + version="3.4.0", description="Redfish Utilities", long_description=long_description, long_description_content_type="text/markdown", From f9da079db1366686a776f3885f1bb48544fa94de Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Sat, 10 May 2025 21:34:56 -0600 Subject: [PATCH 14/17] Added the copywrite and tried to turn debug to the standard debugging that is used everywhere else Signed-off-by: Brandon Biggs --- redfish_utilities/arguments.py | 14 ++++++++++++++ redfish_utilities/logger.py | 13 +++++++++++++ scripts/rf_diagnostic_data.py | 6 ++++++ 3 files changed, 33 insertions(+) diff --git a/redfish_utilities/arguments.py b/redfish_utilities/arguments.py index c31e90f..2a4dbb0 100644 --- a/redfish_utilities/arguments.py +++ b/redfish_utilities/arguments.py @@ -1,3 +1,17 @@ +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019-2025 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md + +""" +Argument parser + +File : arguments.py + +Brief : A single location for arguments to pull from rather than having them defined in a bunch of places +""" + + import argparse diff --git a/redfish_utilities/logger.py b/redfish_utilities/logger.py index 1f9ca89..cc9f482 100644 --- a/redfish_utilities/logger.py +++ b/redfish_utilities/logger.py @@ -1,3 +1,16 @@ +#! /usr/bin/python +# Copyright Notice: +# Copyright 2019-2025 DMTF. All rights reserved. +# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md + +""" +Logger + +File : logger.py + +Brief : Contains a unified logger that can be referenced from other files +""" + import logging import datetime import redfish diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index 2ac68c3..f5344b1 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -82,6 +82,12 @@ container_type = redfish_utilities.log_container.CHASSIS container_id = args.chassis +if args.debug: + log_file = "rf_diagnostic_data-{}.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("rf_diagnostic_data Trace") + # Set up the Redfish object redfish_obj = None try: From 33010c597b4636cde2b7174a2f0de90270856211 Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Sat, 10 May 2025 21:43:55 -0600 Subject: [PATCH 15/17] Ran linter and fixed issue --- scripts/rf_diagnostic_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index f5344b1..ef5b06a 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -13,7 +13,7 @@ import argparse -# import logging +import logging import os import redfish import redfish_utilities @@ -22,6 +22,7 @@ from redfish.messages import RedfishPasswordChangeRequiredError from redfish_utilities.arguments import create_parent_parser, validate_args from redfish_utilities.logger import setup_logger +import datetime # Get the input arguments description = "A tool to collect diagnostic data from a log service on a Redfish service" From 7d1c19d27d3fcc90a91a3c7193e48f6a9a0866aa Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Sat, 10 May 2025 21:48:41 -0600 Subject: [PATCH 16/17] Added back some other removals Signed-off-by: Brandon Biggs --- scripts/rf_diagnostic_data.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index ef5b06a..b1346ef 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -12,7 +12,7 @@ """ import argparse - +import datetime import logging import os import redfish @@ -22,7 +22,6 @@ from redfish.messages import RedfishPasswordChangeRequiredError from redfish_utilities.arguments import create_parent_parser, validate_args from redfish_utilities.logger import setup_logger -import datetime # Get the input arguments description = "A tool to collect diagnostic data from a log service on a Redfish service" @@ -109,6 +108,7 @@ exit_code = 0 try: + print("Collecting diagnostic data...") logger.info("Collecting diagnostic data...") response = redfish_utilities.collect_diagnostic_data( redfish_obj, container_type, container_id, args.log, args.type, args.oemtype @@ -131,11 +131,13 @@ path = os.path.join(args.directory, filename) with open(path, "wb") as file: file.write(data) + print("Saved diagnostic data to '{}'".format(path)) logger.info("Saved diagnostic data to '{}'".format(path)) except Exception as e: - logger.debug("Caught exception:\n\n{}\n".format(traceback.format_exc())) + if args.debug: + logger.error("Caught exception:\n\n{}\n".format(traceback.format_exc())) exit_code = 1 - logger.info(e) + print(e) finally: # Log out if not args.session_token: From a8c6563ea917afe4bb7390f68d8d8441c3d323d4 Mon Sep 17 00:00:00 2001 From: Brandon Biggs Date: Sat, 10 May 2025 21:50:42 -0600 Subject: [PATCH 17/17] Removed a few more logging lines Signed-off-by: Brandon Biggs --- scripts/rf_diagnostic_data.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/rf_diagnostic_data.py b/scripts/rf_diagnostic_data.py index b1346ef..278588f 100644 --- a/scripts/rf_diagnostic_data.py +++ b/scripts/rf_diagnostic_data.py @@ -109,7 +109,6 @@ exit_code = 0 try: print("Collecting diagnostic data...") - logger.info("Collecting diagnostic data...") response = redfish_utilities.collect_diagnostic_data( redfish_obj, container_type, container_id, args.log, args.type, args.oemtype ) @@ -132,7 +131,6 @@ with open(path, "wb") as file: file.write(data) print("Saved diagnostic data to '{}'".format(path)) - logger.info("Saved diagnostic data to '{}'".format(path)) except Exception as e: if args.debug: logger.error("Caught exception:\n\n{}\n".format(traceback.format_exc()))