Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions hw_submission_automation/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,6 @@ __marimo__/

# Streamlit
.streamlit/secrets.toml

# SDCM credentials
authconfig.json
256 changes: 162 additions & 94 deletions hw_submission_automation/hw_submission_automation.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,12 @@
#!/usr/bin/env python3

"""
Usage: python3 hw_submission_automation.py [options] [string...]

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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had better keep the available "guest_names" values on the argparse part to make the help message more user-friendly.

eg:

x86: 10_1511, 10_1607, 10_1703, 10_1709, 10_1803, 10_1809, 10_19H1, 10_2004, 10.all, 10.lates


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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we can keep this example to have a clear usage.

python3 hw_submission_automation.py submit -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
Expand Down Expand Up @@ -173,7 +151,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
Expand Down Expand Up @@ -306,6 +284,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"])
Expand All @@ -324,8 +315,77 @@ 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",
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",
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",
help="parse submission name, default value is the same with product_name"
)
parser.add_argument(
"-p", "--package_path",
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)"
)
parser.add_argument(
"action",
nargs="?",
default="submit",
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()


def gen_guest_mapping():
Expand Down Expand Up @@ -402,54 +462,19 @@ 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_submit(args):
# Validate required arguments for submit action
if not args.product_name:
print("Error: --product_name is required for submit action")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using argparse.ArgumentParser.error() for consistent error reporting.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be complicated because "product_name" is not a mandatory option for argparse, because it is required only for the commit action. So, from the argparse point of view, there is no error.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, from this point the view, it is reasonable.

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)
Comment on lines +466 to +475

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The manual validation of required arguments for the 'submit' action (and similarly in 'main_wait_download') is functional but can be improved. Using argparse subparsers is the idiomatic way to handle different sets of arguments for different actions. Each subparser can define its own required arguments, which simplifies the main functions, removes code duplication, and leverages argparse to provide standard, helpful error messages to the user. Consider refactoring to use subparsers for better maintainability.


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()

Expand All @@ -460,11 +485,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:
Expand All @@ -474,63 +499,106 @@ 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)
if not sid:
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)


def main_wait_download(args):
print(f"[INFO] Download action selected")

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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Relying on the string "> driverMetadata Url" from the stdout of an external tool is brittle. Any change in the tool's output formatting (e.g., spacing, capitalization) could break this logic silently. A more robust solution would be to have wait_for_submission return a status, or to call another method after it completes to programmatically check for the availability of the metadata URL from a structured response rather than parsing plain text.

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__":
main(sys.argv[1:])
# Parse command line arguments
args = parse_arguments()

# Check that action is supported
if args.action not in ["submit", "wait_download"]:
print(f"Error: Unsupported action '{args.action}'")
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 == "wait_download":
main_wait_download(args)
Loading