diff --git a/Makefile b/Makefile
index 96a25f40..8321f1cd 100644
--- a/Makefile
+++ b/Makefile
@@ -91,7 +91,7 @@ pck-register: ## Register PCK certificates with Intel PCS (requires INTEL_PCS_AP
##@ Reference Value Collection
.PHONY: collect-firmware-refvals
-collect-firmware-refvals: ## Collect firmware reference values (bare metal, default)
+collect-firmware-refvals: ## Collect bare-metal firmware values; set OCP_VERSION=x.y.z if oc is unavailable (e.g. airgap)
@scripts/collect_firmware_refvals.py --platform baremetal
.PHONY: collect-azure-refvals
diff --git a/README.md b/README.md
index 46a9b388..7edb0f6b 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,7 @@ For air-gapped bare metal environments, see [`airgap/DEPLOY-RUNBOOK.md`](airgap/
**Common:**
- Tools on your workstation: `podman`, `yq`, `jq`, `skopeo`
+- Python 3.10+ with the shared script dependencies: `python3 -m pip install -r requirements.txt`
- OpenShift pull secret saved at `~/pull-secret.json` (download from [console.redhat.com](https://console.redhat.com/openshift/downloads)), or point elsewhere via the `PULL_SECRET` environment variable
- Fork the repository — ArgoCD reconciles cluster state against your fork, so changes must be pushed to your remote
@@ -97,9 +98,9 @@ For air-gapped bare metal environments, see [`airgap/DEPLOY-RUNBOOK.md`](airgap/
These scripts generate the cryptographic material and attestation reference values needed by Trustee. Run them once before your first deployment.
1. `make gen-secrets` — generates KBS key pairs, sealed-secrets signing keys, and copies `values-secret.yaml.template` to `~/values-secret-coco-pattern.yaml`
-2. Collect attestation reference values (requires `veritas` — `pip install "osc-veritas[snp]==0.1.3rc1"` —, `cosign` >= 2.0 for Azure, and `~/pull-secret.json` or `PULL_SECRET`). The OSC operator version is read from the pattern's own pinned values file (`clusterGroup.subscriptions.sandbox.csv`), not auto-detected from a live cluster — see `docs/firmware-reference-values.md`. By default this collects and merges reference values for **both TDX and SNP**:
+2. Collect attestation reference values (requires the shared Python dependencies above, `cosign` >= 2.0 for Azure, and `~/pull-secret.json` or `PULL_SECRET`). The OSC operator version is read from the pattern's own pinned values file (`clusterGroup.subscriptions.sandbox.csv`), not auto-detected from a live cluster — see `docs/firmware-reference-values.md`. By default this collects and merges reference values for **both TDX and SNP**:
- **Azure:** `make collect-azure-refvals` — pulls PCR measurements from the dm-verity image via veritas. Saves to `~/.coco-pattern/measurements.json`.
- - **Bare metal:** `make collect-firmware-refvals` — computes firmware measurements from OCP release artifacts via veritas. Saves to `~/.coco-pattern/firmware-reference-values.json`. `pcrStash` and `firmwareReferenceValues` are both enabled by default in `~/values-secret-coco-pattern.yaml`, so nothing needs to be uncommented — the collection script automatically writes an empty `{}` placeholder for the platform you're not using.
+ - **Bare metal:** `make collect-firmware-refvals` — computes firmware measurements from OCP release artifacts via veritas. Saves to `~/.coco-pattern/firmware-reference-values.json`. When collecting before cluster access is available, for example from a connected staging host preparing a disconnected deployment, set the target release explicitly: `OCP_VERSION=4.22.8 make collect-firmware-refvals`. `pcrStash` and `firmwareReferenceValues` are both enabled by default in `~/values-secret-coco-pattern.yaml`, so nothing needs to be uncommented — the collection script automatically writes an empty `{}` placeholder for the platform you're not using.
- See [docs/firmware-reference-values.md](docs/firmware-reference-values.md) for detailed workflow and options.
3. Review and customise `~/values-secret-coco-pattern.yaml` — this file is loaded into Vault and provides secrets to the pattern.
diff --git a/airgap/DEPLOY-RUNBOOK.md b/airgap/DEPLOY-RUNBOOK.md
index b3e8ea2c..e4b242b5 100644
--- a/airgap/DEPLOY-RUNBOOK.md
+++ b/airgap/DEPLOY-RUNBOOK.md
@@ -27,8 +27,16 @@
> **Skip this phase on repeat runs.** These steps configure the jump host infrastructure
> (mirror registry, git server) that persists across deployments. Run once per jump host.
>
-> **Prerequisites:** Internet access on the jump host, `podman` and `openssl`
-> installed, and `python3-passlib` or `httpd-tools` for `htpasswd`.
+> **Prerequisites:** Internet access on the jump host, Python 3.10+, `podman`
+> and `openssl` installed, and `python3-passlib` or `httpd-tools` for
+> `htpasswd`.
+
+Install the shared Python dependencies before starting the smart Git HTTP
+server or collecting reference values:
+
+```bash
+python3 -m pip install -r requirements.txt
+```
### 0-0: Set Site Variables
diff --git a/docs/firmware-reference-values.md b/docs/firmware-reference-values.md
index 09cf2762..6b0a925e 100644
--- a/docs/firmware-reference-values.md
+++ b/docs/firmware-reference-values.md
@@ -19,11 +19,10 @@ By default, `collect_firmware_refvals.py` collects reference values for **both T
## Prerequisites
-- `veritas` installed on the host: `pip install "osc-veritas[snp]==0.1.3rc1"`
-- Python 3 with PyYAML: `pip3 install pyyaml`
+- Python 3.10+ with the shared script dependencies: `python3 -m pip install -r requirements.txt` (includes `veritas`, Typer, Rich, and PyYAML)
- `cosign` >= 2.0 — Azure only, used by veritas to verify the Red Hat dm-verity image signature:
- OpenShift pull secret at `~/pull-secret.json` (override the location with the `PULL_SECRET` environment variable or `--pull-secret`)
-- For bare metal: OCP version of your cluster (auto-detected if `oc` is logged in, or pass `--ocp-version` explicitly)
+- For bare metal: OCP version of your cluster (auto-detected if `oc` is logged in, set `OCP_VERSION`, or pass `--ocp-version` explicitly)
- For bare metal TDX: `tdx-measure` (`cargo install --git https://github.com/virtee/tdx-measure tdx-measure-cli`) — collection continues with a warning if absent, but TDX RTMR values will be incomplete
**Why host-installed instead of the `coco-tools` container**: the container image (`quay.io/openshift_sandboxed_containers/coco-tools:0.5.1`) is pinned to an older veritas release that lacks `--skip-tlog`, which is needed to avoid the Azure verification failures described below. This is a deliberate, temporary deviation — see the tracking issue referenced in [Known Limitations](#known-limitations) for moving back to the container once a `coco-tools` release ships with a newer veritas.
@@ -58,8 +57,16 @@ base64-encoded map), not just a version stamp, and is now always passed to
veritas explicitly.
OCP version (bare metal only) is unaffected by this — there is no
-values-file pin for the exact OCP patch, so `--ocp-version` still falls
-back to live-cluster auto-detection, or can be passed explicitly.
+values-file pin for the exact OCP patch. It resolves in this order:
+
+1. `--ocp-version` (repeatable), if supplied.
+2. `OCP_VERSION`, if set.
+3. Live-cluster auto-detection through an authenticated `oc` session.
+
+Use `OCP_VERSION` when reference values are collected before a cluster is
+available, such as from a connected staging host preparing artifacts for a
+fully air-gapped environment. The variable accepts one OCP version; use the
+repeatable `--ocp-version` option when collecting for multiple versions.
## Collecting Reference Values
@@ -86,6 +93,10 @@ Veritas pulls the `osc-dm-verity-image` from the Red Hat registry, verifies its
# Collect firmware values from OCP release artifacts
make collect-firmware-refvals
+# Collect without an authenticated cluster connection (for example, from a
+# connected staging host preparing artifacts for a fully disconnected environment)
+OCP_VERSION=4.22.8 make collect-firmware-refvals
+
# Or with explicit OCP version:
./scripts/collect_firmware_refvals.py --platform baremetal --ocp-version 4.20.18
@@ -107,8 +118,8 @@ Options:
-o, --output Override output path
-p, --pull-secret Pull secret file (default: ~/pull-secret.json,
override via PULL_SECRET env var)
- -v, --ocp-version OCP version (bare metal; repeatable; default:
- auto-detect from a live cluster)
+ -v, --ocp-version OCP version (bare metal; repeatable; takes
+ precedence over OCP_VERSION and auto-detection)
--osc-version OSC operator version (repeatable; default: read
from --values-file's pinned subscription CSV)
--values-file Values file to read the pinned OSC version from
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..97793fa8
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,7 @@
+# Shared dependencies for all Python utilities in this repository.
+Jinja2
+osc-veritas[snp]==0.1.3rc1
+PyYAML
+pytest
+rich
+typer
diff --git a/rhdp/README.md b/rhdp/README.md
index 01ec5b2e..7c3961f2 100644
--- a/rhdp/README.md
+++ b/rhdp/README.md
@@ -6,7 +6,7 @@ The scripts in this directory help users of that platform automate deployments.
## Prerequisites
- `podman` installed and running (used by `pattern.sh` itself)
-- `veritas` installed on the host (used for reference value collection): `pip install "osc-veritas[snp]==0.1.3rc1"`
+- Python 3.10+ with the shared script dependencies: `python3 -m pip install -r requirements.txt`
- `cosign` >= 2.0 (used by veritas for Azure image signature verification)
- `yq`, `jq` installed
- OpenShift pull secret (default: `~/pull-secret.json`, override with `PULL_SECRET` — see below)
@@ -40,7 +40,7 @@ export RESOURCEGROUP=
3. The wrapper script **requires** an azure region code. This code SHOULD be the same as what was selected in RHDP.
4. Optionally use `--prefix` for custom cluster naming: `bash ./rhdp/wrapper.sh --prefix dev1 eastasia`
-The wrapper handles: cluster provisioning, secret generation, PCR reference value collection (via veritas), and pattern installation.
+The wrapper installs the root `requirements.txt` with its selected Python interpreter, then handles cluster provisioning, secret generation, PCR reference value collection (via veritas), and pattern installation.
### Multi-Cluster Deployment (Hub and Spoke)
diff --git a/rhdp/requirements.txt b/rhdp/requirements.txt
deleted file mode 100644
index 3a98310e..00000000
--- a/rhdp/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-typer
-rich
-Jinja2
-typing_extensions
\ No newline at end of file
diff --git a/rhdp/rhdp-cluster-define.py b/rhdp/rhdp-cluster-define.py
index 9971969a..ac1ea5a3 100644
--- a/rhdp/rhdp-cluster-define.py
+++ b/rhdp/rhdp-cluster-define.py
@@ -5,12 +5,11 @@
import os
import pathlib
import shutil
-from typing import Dict, List, Optional
+from typing import Annotated, Dict, List, Optional
import typer
from jinja2 import Environment, FileSystemLoader, select_autoescape
from rich import print as rprint
-from typing_extensions import Annotated
def get_default_cluster_configs(prefix: str = "") -> List[Dict]:
diff --git a/rhdp/wrapper-cluster-only.sh b/rhdp/wrapper-cluster-only.sh
index 5b799b74..f247a1f7 100755
--- a/rhdp/wrapper-cluster-only.sh
+++ b/rhdp/wrapper-cluster-only.sh
@@ -2,16 +2,23 @@
#!/usr/bin/env bash
set -e
-# Function to detect available python binary
+# Function to detect available Python 3 binary.
get_python_cmd() {
- if command -v python &> /dev/null; then
- echo "python"
- elif command -v python3 &> /dev/null; then
- echo "python3"
+ local python_cmd
+ if command -v python3 &> /dev/null; then
+ python_cmd="python3"
+ elif command -v python &> /dev/null; then
+ python_cmd="python"
else
echo "ERROR: Neither python3 nor python is available" >&2
exit 1
fi
+
+ if ! "$python_cmd" -c 'import sys; raise SystemExit(sys.version_info < (3, 10))'; then
+ echo "ERROR: Python 3.10 or later is required" >&2
+ exit 1
+ fi
+ echo "$python_cmd"
}
# Parse arguments
@@ -102,7 +109,8 @@ sleep 10
echo "---------------------"
echo "Installing python dependencies"
echo "---------------------"
-pip install -r rhdp/requirements.txt
+PYTHON_CMD=$(get_python_cmd)
+"$PYTHON_CMD" -m pip install -r requirements.txt
echo "---------------------"
echo "requirements installed"
echo "---------------------"
@@ -116,7 +124,6 @@ sleep 5
echo "---------------------"
echo "defining cluster"
echo "---------------------"
-PYTHON_CMD=$(get_python_cmd)
DEFINE_ARGS=()
if [ "$RECREATE" = true ]; then
DEFINE_ARGS+=(--recreate)
diff --git a/rhdp/wrapper-multicluster.sh b/rhdp/wrapper-multicluster.sh
index fd1854d3..9ce50eaf 100755
--- a/rhdp/wrapper-multicluster.sh
+++ b/rhdp/wrapper-multicluster.sh
@@ -1,16 +1,23 @@
#!/usr/bin/env bash
set -e
-# Function to detect available python binary
+# Function to detect available Python 3 binary.
get_python_cmd() {
- if command -v python &> /dev/null; then
- echo "python"
- elif command -v python3 &> /dev/null; then
- echo "python3"
+ local python_cmd
+ if command -v python3 &> /dev/null; then
+ python_cmd="python3"
+ elif command -v python &> /dev/null; then
+ python_cmd="python"
else
echo "ERROR: Neither python3 nor python is available" >&2
exit 1
fi
+
+ if ! "$python_cmd" -c 'import sys; raise SystemExit(sys.version_info < (3, 10))'; then
+ echo "ERROR: Python 3.10 or later is required" >&2
+ exit 1
+ fi
+ echo "$python_cmd"
}
# Parse arguments
@@ -134,7 +141,8 @@ sleep 10
echo "---------------------"
echo "Installing python dependencies"
echo "---------------------"
-pip install -r rhdp/requirements.txt
+PYTHON_CMD=$(get_python_cmd)
+"$PYTHON_CMD" -m pip install -r requirements.txt
echo "---------------------"
echo "requirements installed"
echo "---------------------"
@@ -148,7 +156,6 @@ sleep 5
echo "---------------------"
echo "defining both clusters (hub and spoke)"
echo "---------------------"
-PYTHON_CMD=$(get_python_cmd)
DEFINE_ARGS=(--multicluster)
if [ "$RECREATE" = true ]; then
DEFINE_ARGS+=(--recreate)
@@ -447,4 +454,4 @@ fi
echo "---------------------"
echo "done"
-echo "---------------------"
\ No newline at end of file
+echo "---------------------"
diff --git a/rhdp/wrapper.sh b/rhdp/wrapper.sh
index 6dda942f..f2733134 100755
--- a/rhdp/wrapper.sh
+++ b/rhdp/wrapper.sh
@@ -2,16 +2,23 @@
#!/usr/bin/env bash
set -e
-# Function to detect available python binary
+# Function to detect available Python 3 binary.
get_python_cmd() {
- if command -v python &> /dev/null; then
- echo "python"
- elif command -v python3 &> /dev/null; then
- echo "python3"
+ local python_cmd
+ if command -v python3 &> /dev/null; then
+ python_cmd="python3"
+ elif command -v python &> /dev/null; then
+ python_cmd="python"
else
echo "ERROR: Neither python3 nor python is available" >&2
exit 1
fi
+
+ if ! "$python_cmd" -c 'import sys; raise SystemExit(sys.version_info < (3, 10))'; then
+ echo "ERROR: Python 3.10 or later is required" >&2
+ exit 1
+ fi
+ echo "$python_cmd"
}
# Parse arguments
@@ -149,7 +156,8 @@ sleep 10
echo "---------------------"
echo "Installing python dependencies"
echo "---------------------"
-pip install -r rhdp/requirements.txt
+PYTHON_CMD=$(get_python_cmd)
+"$PYTHON_CMD" -m pip install -r requirements.txt
echo "---------------------"
echo "requirements installed"
echo "---------------------"
@@ -163,7 +171,6 @@ sleep 5
echo "---------------------"
echo "defining cluster"
echo "---------------------"
-PYTHON_CMD=$(get_python_cmd)
DEFINE_ARGS=()
if [ -n "$PREFIX" ]; then
DEFINE_ARGS+=(--prefix "${PREFIX}")
@@ -207,4 +214,3 @@ export KUBECONFIG="$(pwd)/${INSTALL_DIR}/auth/kubeconfig"
echo "---------------------"
echo "pattern install done"
echo "---------------------"
-
diff --git a/scripts/collect_firmware_refvals.py b/scripts/collect_firmware_refvals.py
index 5444ed33..bcac296a 100755
--- a/scripts/collect_firmware_refvals.py
+++ b/scripts/collect_firmware_refvals.py
@@ -19,8 +19,7 @@
a coco-tools release ships with a newer veritas.
Prerequisites:
- pip install "osc-veritas[snp]==0.1.3rc1"
- PyYAML (pip install pyyaml)
+ python3 -m pip install -r requirements.txt
cosign >= 2.0 (Azure only;
https://docs.sigstore.dev/cosign/system_config/installation/)
tdx-measure (bare metal TDX only; cargo install --git
@@ -44,97 +43,40 @@
"1.2" default).
Version resolution (OCP version, bare metal only):
- --ocp-version (repeatable) wins if given. Otherwise this script
- auto-detects from a live cluster (`oc version`). Unlike OSC, there is no
- values-file pin for the exact OCP patch version -- it's genuine live
- cluster state, not something coco-pattern declares.
+ --ocp-version (repeatable) wins if given, followed by the OCP_VERSION
+ environment variable. Otherwise this script auto-detects from a live
+ cluster (`oc version`). Unlike OSC, there is no values-file pin for the
+ exact OCP patch version -- it's genuine live cluster state, not something
+ coco-pattern declares.
"""
-import argparse
import json
+import os
import re
import shutil
import subprocess
-import sys
import tempfile
from pathlib import Path
+from types import SimpleNamespace
+from typing import Annotated, Literal, Optional
+
+import typer
+from rich.console import Console
try:
import yaml
except ImportError: # pragma: no cover - checked explicitly in main()
yaml = None # type: ignore[assignment]
-VERITAS_PIP_SPEC = "osc-veritas[snp]==0.1.3rc1"
RVPS_FILENAME = "rvps-reference-values.yaml"
+console = Console()
+error_console = Console(stderr=True)
class CollectionError(Exception):
"""Raised for any unrecoverable error; caught in main() for a clean exit."""
-def parse_args(argv=None):
- parser = argparse.ArgumentParser(
- description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter,
- )
- parser.add_argument(
- "--platform",
- required=True,
- choices=["baremetal", "azure"],
- help="Platform to collect reference values for",
- )
- parser.add_argument(
- "-o",
- "--output",
- help="Override output path",
- )
- parser.add_argument(
- "-p",
- "--pull-secret",
- default=None,
- help="Pull secret file (default: ~/pull-secret.json, override via "
- "the PULL_SECRET environment variable)",
- )
- parser.add_argument(
- "-v",
- "--ocp-version",
- action="append",
- dest="ocp_versions",
- metavar="VER",
- help="OCP version (bare metal; repeatable; default: auto-detect "
- "from a live cluster)",
- )
- parser.add_argument(
- "--osc-version",
- action="append",
- dest="osc_versions",
- metavar="VER",
- help="OSC operator version (repeatable; default: read from "
- "--values-file's pinned subscription CSV)",
- )
- parser.add_argument(
- "--values-file",
- help="Values file to read the pinned OSC operator version from "
- "(default: values-azure.yaml or values-baremetal.yaml, "
- "matching --platform)",
- )
- parser.add_argument(
- "-t",
- "--tee",
- default="both",
- choices=["tdx", "snp", "both"],
- help="TEE type (default: both -- collects and merges both)",
- )
- parser.add_argument(
- "--verify-tlog",
- action="store_true",
- help="Azure only: verify against the Rekor transparency log "
- "instead of the default --skip-tlog. Only the signature check "
- "is skipped by default, not overall image verification.",
- )
- return parser.parse_args(argv)
-
-
# --------------------------------------------------------------------------
# Prerequisite checks
# --------------------------------------------------------------------------
@@ -144,15 +86,16 @@ def check_veritas():
if shutil.which("veritas") is None:
raise CollectionError(
"veritas is required but not installed.\n"
- f' Install with: pip install "{VERITAS_PIP_SPEC}"'
+ " Install shared dependencies with: python3 -m pip install -r "
+ "requirements.txt"
)
def check_pyyaml():
if yaml is None:
raise CollectionError(
- "python3 with PyYAML module is required. "
- "Install with: pip3 install pyyaml"
+ "python3 with PyYAML module is required. Install shared "
+ "dependencies with: python3 -m pip install -r requirements.txt"
)
@@ -173,10 +116,9 @@ def check_cosign():
)
match = re.search(r"GitVersion:\s*v?(\d+)\.(\d+)", result.stdout)
if not match:
- print(
+ error_console.print(
"WARNING: could not determine cosign version; veritas requires "
"cosign >= 2.0",
- file=sys.stderr,
)
return
major = int(match.group(1))
@@ -187,8 +129,6 @@ def check_cosign():
def resolve_pull_secret(cli_value):
- import os
-
path = Path(
cli_value or os.environ.get("PULL_SECRET") or Path.home() / "pull-secret.json"
)
@@ -257,19 +197,22 @@ def resolve_osc_versions(args):
def resolve_ocp_versions(args):
- """Resolve OCP version(s) for bare metal: CLI override, else live-cluster.
+ """Resolve OCP version(s): CLI override, environment override, then cluster.
Unlike OSC, there is no values-file pin for the exact OCP patch version.
"""
if args.ocp_versions:
return list(dict.fromkeys(args.ocp_versions)), "--ocp-version"
+ if ocp_version := os.environ.get("OCP_VERSION"):
+ return [ocp_version], "OCP_VERSION environment variable"
+
if shutil.which("oc") is not None:
whoami = subprocess.run(
["oc", "whoami"], capture_output=True, text=True, check=False
)
if whoami.returncode == 0:
- print("Detecting OCP version from cluster...")
+ console.print("Detecting OCP version from cluster...")
result = subprocess.run(
["oc", "version", "-o", "json"],
capture_output=True,
@@ -282,7 +225,7 @@ def resolve_ocp_versions(args):
except json.JSONDecodeError:
version = None
if version:
- print(f"Detected OCP version: {version}")
+ console.print(f"Detected OCP version: {version}")
return [version], "live cluster"
raise CollectionError(
@@ -307,10 +250,9 @@ def bucket(version):
buckets = {bucket(v) for v in osc_versions}
if len(buckets) > 1:
- print(
+ error_console.print(
f"WARNING: OSC versions {osc_versions} straddle the 1.13 "
"bot-version boundary; using the newer format (1.2)",
- file=sys.stderr,
)
return "1.2"
return buckets.pop()
@@ -366,10 +308,10 @@ def run_veritas(
args.extend(["-o", str(output_dir)])
- print(f"Running veritas (tee={tee})...")
- print("(This may take 2-3 minutes to download and process artifacts)")
+ console.print(f"Running veritas (tee={tee})...")
+ console.print("(This may take 2-3 minutes to download and process artifacts)")
result = subprocess.run(args, check=False)
- print()
+ console.print()
if result.returncode != 0:
raise CollectionError(
f"veritas failed (tee={tee}), exit code {result.returncode}"
@@ -421,10 +363,9 @@ def merge_reference_values(dicts):
for data in dicts:
for key, value in data.items():
if key in result and result[key] != value:
- print(
+ error_console.print(
f"WARNING: key '{key}' differs between TEE runs; "
"keeping the first value seen",
- file=sys.stderr,
)
continue
result[key] = value
@@ -467,17 +408,14 @@ def run(args):
# latter so the pull secret authenticates the registry.redhat.io pull
# cosign does internally; otherwise it silently falls back to
# anonymous auth and fails with a confusing UNAUTHORIZED error.
- import os
-
os.environ["REGISTRY_AUTH_FILE"] = str(pull_secret)
if (Path.home() / ".docker" / "config.json").is_file():
- print(
+ error_console.print(
"WARNING: ~/.docker/config.json exists and takes precedence "
"over REGISTRY_AUTH_FILE for cosign's registry auth. If it "
"lacks registry.redhat.io credentials, cosign verification "
"will still fail with UNAUTHORIZED regardless of "
"--pull-secret/PULL_SECRET.",
- file=sys.stderr,
)
osc_versions, osc_source = resolve_osc_versions(args)
@@ -496,16 +434,16 @@ def run(args):
tees_to_run = ["tdx", "snp"] if args.tee == "both" else [args.tee]
skip_tlog = not args.verify_tlog
- print("==========================================")
- print("Firmware Reference Value Collection")
- print("==========================================")
- print(f"Platform: {args.platform}")
- print(f"Version: {version_display} (source: {version_source})")
- print(f"OSC version: {', '.join(osc_versions)} (source: {osc_source})")
- print(f"Bot version: {bot_version}")
- print(f"TEE Type(s): {' '.join(tees_to_run)}")
- print(f"Output file: {output_file}")
- print()
+ console.print("==========================================")
+ console.print("Firmware Reference Value Collection")
+ console.print("==========================================")
+ console.print(f"Platform: {args.platform}")
+ console.print(f"Version: {version_display} (source: {version_source})")
+ console.print(f"OSC version: {', '.join(osc_versions)} (source: {osc_source})")
+ console.print(f"Bot version: {bot_version}")
+ console.print(f"TEE Type(s): {' '.join(tees_to_run)}")
+ console.print(f"Output file: {output_file}")
+ console.print()
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
@@ -524,18 +462,18 @@ def run(args):
)
per_tee_values.append(extract_reference_values(out_dir / RVPS_FILENAME))
- print(f"Merging reference values from: {' '.join(tees_to_run)}...")
+ console.print(f"Merging reference values from: {' '.join(tees_to_run)}...")
merged = merge_reference_values(per_tee_values)
output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_text(json.dumps(merged, indent=2) + "\n")
- print()
- print("Collected firmware reference values:")
- print(json.dumps(merged, indent=2))
- print()
- print(f"Saved to: {output_file}")
- print()
+ console.print()
+ console.print("Collected firmware reference values:")
+ console.print(json.dumps(merged, indent=2))
+ console.print()
+ console.print(f"Saved to: {output_file}")
+ console.print()
vault_key = "pcrStash" if args.platform == "azure" else "firmwareReferenceValues"
@@ -549,25 +487,80 @@ def run(args):
if not sibling.is_file():
sibling.parent.mkdir(parents=True, exist_ok=True)
sibling.write_text("{}\n")
- print(f"Created empty placeholder for the other platform: {sibling}")
- print()
+ console.print(f"Created empty placeholder for the other platform: {sibling}")
+ console.print()
- print("Next steps:")
- print(f"1. Review the collected values: cat {output_file}")
- print(f"2. Ensure '{vault_key}' is configured in ~/values-secret-coco-pattern.yaml")
- print("3. Run: make load-secrets")
- print()
-
-
-def main(argv=None):
- args = parse_args(argv)
+ console.print("Next steps:")
+ console.print(f"1. Review the collected values: cat {output_file}")
+ console.print(
+ f"2. Ensure '{vault_key}' is configured in ~/values-secret-coco-pattern.yaml"
+ )
+ console.print("3. Run: make load-secrets")
+ console.print()
+
+
+def main(
+ platform: Annotated[
+ Literal["baremetal", "azure"],
+ typer.Option("--platform", help="Platform to collect reference values for"),
+ ],
+ output: Annotated[
+ Optional[Path], typer.Option("-o", "--output", help="Override output path")
+ ] = None,
+ pull_secret: Annotated[
+ Optional[Path],
+ typer.Option(
+ "-p",
+ "--pull-secret",
+ help="Pull secret file (default: ~/pull-secret.json; PULL_SECRET overrides it)",
+ ),
+ ] = None,
+ ocp_versions: Annotated[
+ Optional[list[str]],
+ typer.Option(
+ "-v",
+ "--ocp-version",
+ help="OCP version (bare metal; repeatable; overrides OCP_VERSION)",
+ ),
+ ] = None,
+ osc_versions: Annotated[
+ Optional[list[str]],
+ typer.Option("--osc-version", help="OSC operator version (repeatable)"),
+ ] = None,
+ values_file: Annotated[
+ Optional[Path],
+ typer.Option(
+ "--values-file", help="Values file containing the pinned OSC version"
+ ),
+ ] = None,
+ tee: Annotated[
+ Literal["tdx", "snp", "both"],
+ typer.Option("-t", "--tee", help="TEE type (default: both)"),
+ ] = "both",
+ verify_tlog: Annotated[
+ bool,
+ typer.Option(
+ "--verify-tlog",
+ help="Azure only: verify against the Rekor transparency log",
+ ),
+ ] = False,
+):
+ args = SimpleNamespace(
+ platform=platform,
+ output=output,
+ pull_secret=pull_secret,
+ ocp_versions=ocp_versions,
+ osc_versions=osc_versions,
+ values_file=values_file,
+ tee=tee,
+ verify_tlog=verify_tlog,
+ )
try:
run(args)
except CollectionError as e:
- print(f"Error: {e}", file=sys.stderr)
- return 1
- return 0
+ error_console.print(f"Error: {e}")
+ raise typer.Exit(code=1)
if __name__ == "__main__":
- sys.exit(main())
+ typer.run(main)
diff --git a/scripts/git-http-server.py b/scripts/git-http-server.py
index 0f952449..bae25252 100755
--- a/scripts/git-http-server.py
+++ b/scripts/git-http-server.py
@@ -17,14 +17,16 @@
import os
import socketserver
import subprocess
-import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
+from typing import Annotated
from urllib.parse import urlparse
-GIT_PROJECT_ROOT = os.path.expanduser(
- sys.argv[2] if len(sys.argv) > 2 else "~/public_html/git"
-)
+import typer
+from rich.console import Console
+
+GIT_PROJECT_ROOT = ""
GIT_HTTP_BACKEND = "/usr/libexec/git-core/git-http-backend"
+console = Console()
class GitHTTPHandler(BaseHTTPRequestHandler):
@@ -98,8 +100,18 @@ class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True
-if __name__ == "__main__":
- port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
+def main(
+ port: Annotated[int, typer.Argument(help="TCP port to listen on")] = 8080,
+ git_project_root: Annotated[
+ str, typer.Argument(help="Directory containing exported Git repositories")
+ ] = "~/public_html/git",
+):
+ global GIT_PROJECT_ROOT
+ GIT_PROJECT_ROOT = os.path.expanduser(git_project_root)
srv = ThreadedHTTPServer(("0.0.0.0", port), GitHTTPHandler)
- print(f"Git HTTP server on port {port}, root={GIT_PROJECT_ROOT}", flush=True)
+ console.print(f"Git HTTP server on port {port}, root={GIT_PROJECT_ROOT}")
srv.serve_forever()
+
+
+if __name__ == "__main__":
+ typer.run(main)
diff --git a/tests/test_python_clis.py b/tests/test_python_clis.py
new file mode 100644
index 00000000..bb92f598
--- /dev/null
+++ b/tests/test_python_clis.py
@@ -0,0 +1,74 @@
+"""Smoke tests for Python command-line interfaces without external tooling."""
+
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
+ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*m")
+
+
+def run_cli(*arguments: str) -> subprocess.CompletedProcess[str]:
+ return subprocess.run(
+ [sys.executable, *arguments],
+ cwd=REPOSITORY_ROOT,
+ text=True,
+ capture_output=True,
+ check=False,
+ )
+
+
+def output(result: subprocess.CompletedProcess[str]) -> str:
+ return ANSI_ESCAPE.sub("", result.stdout + result.stderr)
+
+
+def test_help_for_all_python_entry_points():
+ commands = {
+ "scripts/collect_firmware_refvals.py": "--platform",
+ "scripts/git-http-server.py": "GIT_PROJECT_ROOT",
+ "rhdp/rhdp-cluster-define.py": "REGION",
+ }
+ for script, expected_option in commands.items():
+ result = run_cli(script, "--help")
+ assert result.returncode == 0, output(result)
+ assert "Usage:" in output(result)
+ assert expected_option in output(result)
+
+
+def test_collector_rejects_missing_or_invalid_options():
+ cases = (
+ (),
+ ("--platform", "invalid"),
+ ("--platform", "baremetal", "--tee", "invalid"),
+ )
+ for arguments in cases:
+ result = run_cli("scripts/collect_firmware_refvals.py", *arguments)
+ assert result.returncode != 0
+ assert "Error" in output(result)
+
+
+def test_collector_help_preserves_option_contract():
+ result = run_cli("scripts/collect_firmware_refvals.py", "--help")
+ assert result.returncode == 0, output(result)
+ help_output = output(result)
+ for option in (
+ "[baremetal|azure]",
+ "--output",
+ "--pull-secret",
+ "--ocp-version",
+ "--osc-version",
+ "[tdx|snp|both]",
+ "--verify-tlog",
+ ):
+ assert option in help_output
+
+
+def test_git_http_server_help_preserves_positional_defaults():
+ result = run_cli("scripts/git-http-server.py", "--help")
+ assert result.returncode == 0, output(result)
+ help_output = output(result)
+ assert "[port]" in help_output
+ assert "[default: 8080]" in help_output
+ assert "[git_project_root]" in help_output
+ assert "[default: ~/public_html/git]" in help_output