From 33f0c12c0cba1e47857a29a9453d9c57443e96f5 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kostiuk Date: Thu, 4 Sep 2025 16:55:22 +0300 Subject: [PATCH 1/6] Use argparse instead of getopt Signed-off-by: Kostiantyn Kostiuk --- .../hw_submission_automation.py | 138 +++++++++--------- 1 file changed, 68 insertions(+), 70 deletions(-) diff --git a/hw_submission_automation/hw_submission_automation.py b/hw_submission_automation/hw_submission_automation.py index 586bff3..ca7615c 100644 --- a/hw_submission_automation/hw_submission_automation.py +++ b/hw_submission_automation/hw_submission_automation.py @@ -28,7 +28,7 @@ python3 hw_submission_automation.py -n test_rng -g 11.latest -p /home/271_RNG_win11_unsigned.hlkx -d 2025-06-24 """ -import getopt +import argparse import json import os import re @@ -324,8 +324,48 @@ def format_date_to_iso(date_str): return datetime.strptime(date_str, "%Y-%m-%d").isoformat() -def usage(): - print(__doc__) +def parse_arguments(): + """Parse command line arguments using argparse""" + parser = argparse.ArgumentParser( + description="Hardware submission automation tool for SDCM", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__ + ) + parser.add_argument( + "-t", "--test_harness", + default="HLK", + help="parse test_harness, valid value: HLK(default),Attestation" + ) + parser.add_argument( + "-n", "--product_name", + required=True, + help="parse product name, eg: 'Red Hat VirtIO RNG Drivers for Windows 11'" + ) + parser.add_argument( + "-a", "--guest_arch", + default="x64", + help="parse specified guest architecture. Valid value: x86,x64(default),mixed,ARM64" + ) + parser.add_argument( + "-g", "--guest_names", + required=True, + help="parse specified guest platform" + ) + parser.add_argument( + "-s", "--submission_name", + help="parse submission name, default value is the same with product_name" + ) + parser.add_argument( + "-p", "--package_path", + required=True, + help="parse package file path eg: /home/271_RNG_win11_unsigned.hlkx" + ) + parser.add_argument( + "-d", "--announcement_date", + default="2025-01-01", + help="Parse announcement date (GA) in YYYY-MM-DD format (e.g., 2025-06-24)" + ) + return parser.parse_args() def gen_guest_mapping(): @@ -402,54 +442,11 @@ def gen_guest_mapping(): return mapping -def main(argv): - test_harness = "HLK" - product_name = None - guest_names = [] - guest_arch = "x64" - submission_name = None - package_path = None - announcement_date = "2025-01-01" +def main(): + # Parse command line arguments + args = parse_arguments() + marketing_names = [] - try: - opts, args = getopt.getopt( - argv, - "ht:n:g:a:s:p:d:", - [ - "help", - "test_harness=", - "product_name=", - "guest_names=", - "guest_arch=", - "submission_name=", - "package_path=", - "announcement_date=", - ], - ) - except getopt.GetoptError: - usage() - sys.exit(2) - - for opt, arg in opts: - print(arg) - print(opt) - if opt in ("-h", "--help"): - usage() - sys.exit() - elif opt in ("-t", "--test_harness"): - test_harness = arg - elif opt in ("-n", "--product_name"): - product_name = arg - elif opt in ("-g", "--guest_names"): - guest_names = arg - elif opt in ("-a", "--guest_arch"): - guest_arch = arg - elif opt in ("-s", "--submission_name"): - submission_name = arg - elif opt in ("-p", "--package_path"): - package_path = arg - elif opt in ("-d", "--announcement_date"): - announcement_date = arg guest_mapping = gen_guest_mapping() @@ -460,11 +457,11 @@ def main(argv): requested_signatures = [] selected_product_types = [] - for guest_name in guest_names.split(","): - if guest_arch == "mixed": + for guest_name in args.guest_names.split(","): + if args.guest_arch == "mixed": arch_list = ["x86", "x64"] else: - arch_list = [guest_arch] + arch_list = [args.guest_arch] for arch in arch_list: current_mappings = guest_mapping[arch][guest_name] for mapping in current_mappings: @@ -474,43 +471,44 @@ def main(argv): requested_signatures = list(set(requested_signatures)) selected_product_types = list(set(selected_product_types)) + submission_name = args.submission_name if not submission_name: - print(f"Submission name is not specified, using product name: {product_name}") - submission_name = product_name + print(f"Submission name is not specified, using product name: {args.product_name}") + submission_name = args.product_name - marketing_names = [product_name] + marketing_names = [args.product_name] - if test_harness == "Attestation": + if args.test_harness == "Attestation": marketing_names = [] selected_product_types = [] print("Attestation test harness selected, no marketing names or product types will be set.") # we always keep these three value the same when submit manually. - announcement_date = format_date_to_iso(announcement_date) + announcement_date = format_date_to_iso(args.announcement_date) print("SDCM product creation parameters:") - print(f" Test harness: {test_harness}") - print(f" Product name: {product_name}") - print(f" Guest names: {guest_names}") - print(f" Guest architecture: {guest_arch}") + print(f" Test harness: {args.test_harness}") + print(f" Product name: {args.product_name}") + print(f" Guest names: {args.guest_names}") + print(f" Guest architecture: {args.guest_arch}") print(f" - Requested signatures: {requested_signatures}") print(f" - Selected product types: {selected_product_types}") - print(f" Package path: {package_path}") + print(f" Package path: {args.package_path}") print(f" Announcement date: {announcement_date}") print(f" Submission name: {submission_name}") print(f" Marketing names: {marketing_names}") wrapper = SDCMWrapper() pid = wrapper.create_product( - product_name, - test_harness, + args.product_name, + args.test_harness, announcement_date, marketing_names, selected_product_types, requested_signatures, ) if not pid: - print(f"Failed to create product: {product_name}") + print(f"Failed to create product: {args.product_name}") sys.exit(1) sid = wrapper.create_submission(pid, submission_name) @@ -518,19 +516,19 @@ def main(argv): print(f"Failed to create submission: {submission_name}") sys.exit(1) - wrapper.upload_package(package_path, pid, sid) + wrapper.upload_package(args.package_path, pid, sid) wrapper.commit_submission(pid, sid) create_results = { "product_id": pid, "submission_id": sid, - "product_name": product_name, + "product_name": args.product_name, "submission_name": submission_name, } - create_results_file = slugify(f"Result_Product_{(product_name)}.json") + create_results_file = slugify(f"Result_Product_{args.product_name}.json") with open(create_results_file, "w") as f: json.dump(create_results, f, indent=4) if __name__ == "__main__": - main(sys.argv[1:]) + main() From c0a7fb07c3f4374579bac4a328291abd8e38e4e9 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kostiuk Date: Thu, 4 Sep 2025 18:01:28 +0300 Subject: [PATCH 2/6] Add initial skeleton for download action Signed-off-by: Kostiantyn Kostiuk --- .../hw_submission_automation.py | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/hw_submission_automation/hw_submission_automation.py b/hw_submission_automation/hw_submission_automation.py index ca7615c..03289c2 100644 --- a/hw_submission_automation/hw_submission_automation.py +++ b/hw_submission_automation/hw_submission_automation.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 """ -Usage: python3 hw_submission_automation.py [options] [string...] +Usage: python3 hw_submission_automation.py [options] [action] + +Actions: + submit (default) Submit a hardware package for certification + download Download submission results (not yet implemented) Options: -h, --help show this help message @@ -26,6 +30,7 @@ Examples: python3 hw_submission_automation.py -n test_rng -g 11.latest -p /home/271_RNG_win11_unsigned.hlkx -d 2025-06-24 + python3 hw_submission_automation.py submit -n test_rng -g 11.latest -p /home/271_RNG_win11_unsigned.hlkx -d 2025-06-24 """ import argparse @@ -338,7 +343,6 @@ def parse_arguments(): ) parser.add_argument( "-n", "--product_name", - required=True, help="parse product name, eg: 'Red Hat VirtIO RNG Drivers for Windows 11'" ) parser.add_argument( @@ -348,7 +352,6 @@ def parse_arguments(): ) parser.add_argument( "-g", "--guest_names", - required=True, help="parse specified guest platform" ) parser.add_argument( @@ -357,7 +360,6 @@ def parse_arguments(): ) parser.add_argument( "-p", "--package_path", - required=True, help="parse package file path eg: /home/271_RNG_win11_unsigned.hlkx" ) parser.add_argument( @@ -365,6 +367,13 @@ def parse_arguments(): default="2025-01-01", help="Parse announcement date (GA) in YYYY-MM-DD format (e.g., 2025-06-24)" ) + parser.add_argument( + "action", + nargs="?", + default="submit", + choices=["submit", "download"], + help="Action to perform: submit (default) or download" + ) return parser.parse_args() @@ -442,10 +451,18 @@ def gen_guest_mapping(): return mapping -def main(): - # Parse command line arguments - args = parse_arguments() - +def main_submit(args): + # Validate required arguments for submit action + if not args.product_name: + print("Error: --product_name is required for submit action") + sys.exit(1) + if not args.guest_names: + print("Error: --guest_names is required for submit action") + sys.exit(1) + if not args.package_path: + print("Error: --package_path is required for submit action") + sys.exit(1) + marketing_names = [] guest_mapping = gen_guest_mapping() @@ -530,5 +547,23 @@ def main(): json.dump(create_results, f, indent=4) +def main_download(args): + print(f"[INFO] Download action selected") + print(f"[INFO] This feature is not yet implemented") + + if __name__ == "__main__": - main() + # Parse command line arguments + args = parse_arguments() + + # Check that action is supported + if args.action not in ["submit", "download"]: + print(f"Error: Unsupported action '{args.action}'") + print("Supported actions: submit, download") + sys.exit(1) + + # Call appropriate main function based on action + if args.action == "submit": + main_submit(args) + elif args.action == "download": + main_download(args) From 186f522950bf73252d7ab9b02fe7e99c7710aa21 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kostiuk Date: Fri, 19 Sep 2025 10:41:33 +0300 Subject: [PATCH 3/6] Add authconfig.json to gitignore Signed-off-by: Kostiantyn Kostiuk --- hw_submission_automation/.gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hw_submission_automation/.gitignore b/hw_submission_automation/.gitignore index e2d6db8..c159d34 100644 --- a/hw_submission_automation/.gitignore +++ b/hw_submission_automation/.gitignore @@ -208,3 +208,6 @@ __marimo__/ # Streamlit .streamlit/secrets.toml + +# SDCM credentials +authconfig.json From 4fa922dd2d80580a0c23c0a3021975f2929e30db Mon Sep 17 00:00:00 2001 From: Kostiantyn Kostiuk Date: Fri, 19 Sep 2025 10:52:08 +0300 Subject: [PATCH 4/6] Fix error message when SDCM fails Signed-off-by: Kostiantyn Kostiuk --- hw_submission_automation/hw_submission_automation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hw_submission_automation/hw_submission_automation.py b/hw_submission_automation/hw_submission_automation.py index 03289c2..2f5a43e 100644 --- a/hw_submission_automation/hw_submission_automation.py +++ b/hw_submission_automation/hw_submission_automation.py @@ -178,7 +178,7 @@ def _run_sdcm(self, args: List[str]) -> str: error_msg = ( f"SDCM command failed with code {e.returncode}:\n" f"Command: {' '.join(command)}\n" - f"Stdout output: {e.output.strip()}" + f"Stdout output: {e.output.strip()}\n" f"Stderr output: {e.stderr.strip()}" ) raise RuntimeError(error_msg) from e From 41aef901301a5ca194382d41142ae0504a51d9ee Mon Sep 17 00:00:00 2001 From: Kostiantyn Kostiuk Date: Fri, 19 Sep 2025 10:55:38 +0300 Subject: [PATCH 5/6] Implement wait and download action Signed-off-by: Kostiantyn Kostiuk --- .../hw_submission_automation.py | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/hw_submission_automation/hw_submission_automation.py b/hw_submission_automation/hw_submission_automation.py index 2f5a43e..e88c53a 100644 --- a/hw_submission_automation/hw_submission_automation.py +++ b/hw_submission_automation/hw_submission_automation.py @@ -5,7 +5,7 @@ Actions: submit (default) Submit a hardware package for certification - download Download submission results (not yet implemented) + wait_download Download submission results Options: -h, --help show this help message @@ -311,6 +311,19 @@ def download_results(self, product_id: str, submission_id: str, output_file: str ] ) + def download_metadata(self, product_id: str, submission_id: str, output_file: str) -> str: + """Download submission metadata""" + return self._run_sdcm( + [ + "--metadata", + output_file, + "--productid", + product_id, + "--submissionid", + submission_id, + ] + ) + def list_products(self) -> str: """List all products""" return self._run_sdcm(["--list", "product"]) @@ -371,8 +384,23 @@ def parse_arguments(): "action", nargs="?", default="submit", - choices=["submit", "download"], - help="Action to perform: submit (default) or download" + choices=["submit", "wait_download"], + help="Action to perform: submit (default) or wait_download" + ) + + parser.add_argument( + "-pid", "--product_id", + help="Parse product ID" + ) + + parser.add_argument( + "-sid", "--submission_id", + help="Parse submission ID" + ) + + parser.add_argument( + "-o", "--output_file", + help="Parse output file path (e.g., /path/to/file.signed.zip)" ) return parser.parse_args() @@ -547,9 +575,33 @@ def main_submit(args): json.dump(create_results, f, indent=4) -def main_download(args): +def main_wait_download(args): print(f"[INFO] Download action selected") - print(f"[INFO] This feature is not yet implemented") + + if not args.product_id: + print("Error: --product_id is required for download action") + sys.exit(1) + if not args.submission_id: + print("Error: --submission_id is required for download action") + sys.exit(1) + if not args.output_file: + print("Error: --output_file is required for download action") + sys.exit(1) + + output_file = os.path.abspath(args.output_file) + + wrapper = SDCMWrapper() + print(f"Waiting for submission with product ID: {args.product_id} and submission ID: {args.submission_id}") + results = wrapper.wait_for_submission(args.product_id, args.submission_id) + print(f"Submission completed") + + wrapper.download_results(args.product_id, args.submission_id, output_file) + print(f"Results downloaded to: {output_file}") + + if "> driverMetadata Url" in results: + print(f"Driver metadata URL found in submission results") + metadata_file = output_file + "_metadata.json" + wrapper.download_metadata(args.product_id, args.submission_id, metadata_file) if __name__ == "__main__": @@ -557,13 +609,13 @@ def main_download(args): args = parse_arguments() # Check that action is supported - if args.action not in ["submit", "download"]: + if args.action not in ["submit", "wait_download"]: print(f"Error: Unsupported action '{args.action}'") - print("Supported actions: submit, download") + print("Supported actions: submit, wait_download") sys.exit(1) # Call appropriate main function based on action if args.action == "submit": main_submit(args) - elif args.action == "download": - main_download(args) + elif args.action == "wait_download": + main_wait_download(args) From 9f8e8feae28efa5e706a48375165d12dcff73721 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kostiuk Date: Fri, 10 Oct 2025 11:00:45 +0300 Subject: [PATCH 6/6] Remove help message, argparse will generate it automatically Signed-off-by: Kostiantyn Kostiuk --- .../hw_submission_automation.py | 39 ++++++------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/hw_submission_automation/hw_submission_automation.py b/hw_submission_automation/hw_submission_automation.py index e88c53a..ac9c498 100644 --- a/hw_submission_automation/hw_submission_automation.py +++ b/hw_submission_automation/hw_submission_automation.py @@ -1,33 +1,6 @@ #!/usr/bin/env python3 """ -Usage: python3 hw_submission_automation.py [options] [action] - -Actions: - submit (default) Submit a hardware package for certification - wait_download Download submission results - -Options: - -h, --help show this help message - -t ..., --test_harness parse test_harness, valid value: HLK(default),Attestation - -n ..., --product_name parse product name, eg: "Red Hat VirtIO RNG Drivers for Windows 11" - -a ..., --guest_arch parse specified guest archtechure. Valid value: x86,x64(default),mixed,ARM64 - 'mixed': For Win10 packages containing both x86/x64 - -g ..., --guest_names parse specified guest platform. Valid value: - - x86: 10_1511, 10_1607, 10_1703, 10_1709, 10_1803, 10_1809, 10_19H1, 10_2004, 10.all, 10.latest - x64: 10_1511, 10_1607, 10_1703, 10_1709, 10_1803, 10_1809, 10_19H1, 10_2004, 10_21H2, 11_22H2, 11_24H2, 16, 19, 22, - 25, 10.all, 11.all, 10.latest, 11.latest - ARM64: 10_1709, 10_1803, 10_19H1, 10_2004, 10_21H2, 11_22H2, 11_24H2, 22, 25, 10.all, 11.all, 10.latest, 11.latest - Examples: - 11.latest - 10_1803,10_2004 - 10.all - - -s ..., --submission_name parse submission name, default value is the same with product_name - -p ..., --package_path parse package file path eg: /home/271_RNG_win11_unsigned.hlkx - -d ..., --announcement_date Parse announcement date (GA) in YYYY-MM-DD format (e.g., 2025-06-24) - Examples: python3 hw_submission_automation.py -n test_rng -g 11.latest -p /home/271_RNG_win11_unsigned.hlkx -d 2025-06-24 python3 hw_submission_automation.py submit -n test_rng -g 11.latest -p /home/271_RNG_win11_unsigned.hlkx -d 2025-06-24 @@ -365,7 +338,17 @@ def parse_arguments(): ) parser.add_argument( "-g", "--guest_names", - help="parse specified guest platform" + help=""" + parse specified guest platform. + Valid value: + x86: 10_1511, 10_1607, 10_1703, 10_1709, 10_1803, 10_1809, 10_19H1, 10_2004, 10.all, 10.latest + x64: 10_1511, 10_1607, 10_1703, 10_1709, 10_1803, 10_1809, 10_19H1, 10_2004, 10_21H2, 11_22H2, 11_24H2, 16, 19, 22, + 25, 10.all, 11.all, 10.latest, 11.latest + ARM64: 10_1709, 10_1803, 10_19H1, 10_2004, 10_21H2, 11_22H2, 11_24H2, 22, 25, 10.all, 11.all, 10.latest, 11.latest + Examples: + 11.latest + 10_1803,10_2004 + 10.all""" ) parser.add_argument( "-s", "--submission_name",