mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-07 21:51:20 +00:00
8be1e9b2c1
* test(scanner): merge release evidence descriptors Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(scanner): add authority and legacy release descriptors Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(scanner): add scoped ack release descriptor Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(scanner): require all release evidence fields Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(scanner): add mrf release descriptor producer Add a standalone W13 MRF descriptor producer for measured G07/G08/P4 raw artifacts, with fail-closed self-tests for missing responsibility cases, disk-full ENOSPC observations, synthetic artifacts, and short P4 cleanup soaks. Also bind G12 quota path evidence to explicit case lists so W16 descriptors cannot pass without reset and settlement coverage. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(scanner): reuse mrf release descriptor producer Route the W13 runner through the shared MRF descriptor producer so measured runs and offline re-packaging use the same fail-closed validation. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: zhi22915 <qiuzgang@gmail.com>
287 lines
12 KiB
Python
Executable File
287 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Assemble measured Scanner/Heal G01 authority release evidence.
|
|
|
|
The producer consumes operator-collected measured JSON. It packages root and
|
|
quota authority measurements into the common release-bundle descriptor shape
|
|
and lets check_test_wiring.py validate G01 without approving the full release.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from datetime import datetime, timedelta, timezone
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import check_test_wiring as wiring
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def git_head() -> str:
|
|
return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
|
|
|
|
|
|
def timestamp(value: Any, name: str) -> str:
|
|
wiring.require(isinstance(value, str) and value.strip(), f"missing {name}")
|
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
wiring.require(parsed.tzinfo is not None, f"{name} must include timezone")
|
|
return parsed.isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
def measured_json(path: Path, source_revision: str, label: str) -> dict[str, Any]:
|
|
payload = wiring.read_json(path.resolve())
|
|
wiring.require(isinstance(payload, dict), f"{label} must be a JSON object")
|
|
for marker in ("fixture", "fixture_only", "dry_run", "synthetic"):
|
|
wiring.require(payload.get(marker) is not True, f"{label} is {marker}")
|
|
wiring.require(payload.get("schema") == 1, f"{label} schema must be 1")
|
|
wiring.require(payload.get("evidence_type") == "measured", f"{label} must be measured")
|
|
wiring.require(payload.get("source_revision") == source_revision, f"{label} source revision mismatch")
|
|
return payload
|
|
|
|
|
|
def common_evidence(args: argparse.Namespace, source_revision: str) -> dict[str, Any]:
|
|
duration = wiring.evidence_integer(args.duration_seconds, "duration_seconds", 1, 86400)
|
|
started_at = timestamp(args.started_at, "started_at") if args.started_at else datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
if args.finished_at:
|
|
finished_at = timestamp(args.finished_at, "finished_at")
|
|
else:
|
|
started = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
|
|
finished_at = (started + timedelta(seconds=duration)).isoformat().replace("+00:00", "Z")
|
|
wiring.require(
|
|
datetime.fromisoformat(finished_at.replace("Z", "+00:00")) >= datetime.fromisoformat(started_at.replace("Z", "+00:00")),
|
|
"authority measurement timestamps are inverted",
|
|
)
|
|
run_id = args.run_id or f"authority-{source_revision[:12]}"
|
|
window_id = args.measurement_window_id or f"authority-window-{source_revision[:12]}"
|
|
wiring.evidence_string(run_id, "run_id", r"[A-Za-z0-9][A-Za-z0-9._:-]{7,127}")
|
|
wiring.evidence_string(window_id, "measurement_window_id", r"[A-Za-z0-9][A-Za-z0-9._:-]{7,127}")
|
|
wiring.require(run_id != window_id, "authority proof must separate run/window identities")
|
|
return {
|
|
"evidence_type": "measured",
|
|
"source_revision": source_revision,
|
|
"run_id": run_id,
|
|
"measurement_window_id": window_id,
|
|
"started_at": started_at,
|
|
"finished_at": finished_at,
|
|
"command": [
|
|
"scripts/run_scanner_heal_authority_evidence.py",
|
|
"--root-authority-json",
|
|
"<root-authority-json>",
|
|
"--quota-authority-json",
|
|
"<quota-authority-json>",
|
|
],
|
|
}
|
|
|
|
|
|
def validate_root_authority(payload: dict[str, Any]) -> None:
|
|
wiring.release_bundle_exact_strings(
|
|
payload.get("root_authority_cases"),
|
|
wiring.SCANNER_HEAL_RELEASE_G01_ROOT_AUTHORITY_CASES,
|
|
"root_authority_cases",
|
|
)
|
|
wiring.release_bundle_bool_true(payload.get("root_cas_observed"), "root_cas_observed")
|
|
wiring.release_bundle_bool_true(payload.get("root_readback_observed"), "root_readback_observed")
|
|
wiring.release_bundle_bool_true(payload.get("incomplete_root_rejected"), "incomplete_root_rejected")
|
|
wiring.release_bundle_bool_true(payload.get("stale_root_rejected"), "stale_root_rejected")
|
|
|
|
|
|
def validate_quota_authority(payload: dict[str, Any]) -> None:
|
|
wiring.release_bundle_exact_strings(
|
|
payload.get("quota_authority_cases"),
|
|
wiring.SCANNER_HEAL_RELEASE_G01_QUOTA_AUTHORITY_CASES,
|
|
"quota_authority_cases",
|
|
)
|
|
wiring.release_bundle_bool_true(payload.get("quota_floor_readback_observed"), "quota_floor_readback_observed")
|
|
wiring.release_bundle_bool_true(payload.get("over_limit_put_rejected"), "over_limit_put_rejected")
|
|
wiring.release_bundle_bool_true(payload.get("rejected_object_invisible"), "rejected_object_invisible")
|
|
wiring.release_bundle_bool_true(payload.get("quota_fails_closed_without_authority"), "quota_fails_closed_without_authority")
|
|
|
|
|
|
def write_field(out_dir: Path, field: str, common: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
|
|
evidence = {
|
|
**payload,
|
|
**common,
|
|
"evidence_type": "measured",
|
|
"summary": payload.get("summary") or f"Measured Scanner/Heal G01.{field} evidence.",
|
|
}
|
|
artifact = out_dir / "artifacts" / f"G01-{field}.json"
|
|
artifact.parent.mkdir(parents=True, exist_ok=True)
|
|
artifact_payload = {
|
|
"schema": 1,
|
|
"evidence_type": "measured",
|
|
"source_revision": evidence["source_revision"],
|
|
"run_id": evidence["run_id"],
|
|
"measurement_window_id": evidence["measurement_window_id"],
|
|
"gate": "G01",
|
|
"field": field,
|
|
}
|
|
for key, value in evidence.items():
|
|
if key not in {"artifact", "sha256", "artifact_format", "summary", "started_at", "finished_at", "command"}:
|
|
artifact_payload[key] = value
|
|
wiring.write_json(artifact, artifact_payload)
|
|
evidence["artifact"] = artifact.relative_to(out_dir).as_posix()
|
|
evidence["sha256"] = wiring.digest(artifact)
|
|
evidence["artifact_format"] = "json"
|
|
return evidence
|
|
|
|
|
|
def build_descriptor(args: argparse.Namespace) -> Path:
|
|
out_dir = args.out_dir.resolve()
|
|
wiring.require(not out_dir.exists(), "output directory must be new")
|
|
source_revision = args.source_revision or git_head()
|
|
root_authority = measured_json(args.root_authority_json, source_revision, "root authority artifact")
|
|
quota_authority = measured_json(args.quota_authority_json, source_revision, "quota authority artifact")
|
|
validate_root_authority(root_authority)
|
|
validate_quota_authority(quota_authority)
|
|
out_dir.mkdir(parents=True)
|
|
common = common_evidence(args, source_revision)
|
|
fields = {
|
|
"root_authority_evidence": write_field(out_dir, "root_authority_evidence", common, root_authority),
|
|
"quota_authority_evidence": write_field(out_dir, "quota_authority_evidence", common, quota_authority),
|
|
}
|
|
descriptor = out_dir / "release-bundle-authority.json"
|
|
wiring.write_json(descriptor, {
|
|
"schema": 1,
|
|
"evidence": "measured",
|
|
"source_revision": source_revision,
|
|
"gates": {
|
|
"G01": {
|
|
"status": "pass",
|
|
"lane": "authority-coverage",
|
|
"evidence_type": "measured",
|
|
"evidence_fields": fields,
|
|
},
|
|
},
|
|
})
|
|
subprocess.check_call([
|
|
sys.executable,
|
|
str(ROOT / "scripts/check_test_wiring.py"),
|
|
"--check-scanner-heal-release-bundle-gate",
|
|
str(descriptor),
|
|
"G01",
|
|
], cwd=ROOT)
|
|
return descriptor
|
|
|
|
|
|
def write_self_test_inputs(root: Path, source_revision: str) -> tuple[Path, Path]:
|
|
root_json = root / "root-authority.json"
|
|
quota_json = root / "quota-authority.json"
|
|
wiring.write_json(root_json, {
|
|
"schema": 1,
|
|
"evidence_type": "measured",
|
|
"source_revision": source_revision,
|
|
"root_authority_cases": list(wiring.SCANNER_HEAL_RELEASE_G01_ROOT_AUTHORITY_CASES),
|
|
"root_cas_observed": True,
|
|
"root_readback_observed": True,
|
|
"incomplete_root_rejected": True,
|
|
"stale_root_rejected": True,
|
|
})
|
|
wiring.write_json(quota_json, {
|
|
"schema": 1,
|
|
"evidence_type": "measured",
|
|
"source_revision": source_revision,
|
|
"quota_authority_cases": list(wiring.SCANNER_HEAL_RELEASE_G01_QUOTA_AUTHORITY_CASES),
|
|
"quota_floor_readback_observed": True,
|
|
"over_limit_put_rejected": True,
|
|
"rejected_object_invisible": True,
|
|
"quota_fails_closed_without_authority": True,
|
|
})
|
|
return root_json, quota_json
|
|
|
|
|
|
def run_self_test() -> None:
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
source_revision = git_head()
|
|
root_json, quota_json = write_self_test_inputs(root, source_revision)
|
|
descriptor = build_descriptor(parse_args([
|
|
"--root-authority-json", str(root_json),
|
|
"--quota-authority-json", str(quota_json),
|
|
"--out-dir", str(root / "out"),
|
|
"--duration-seconds", "60",
|
|
]))
|
|
wiring.require(descriptor.is_file(), "self-test descriptor missing")
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
source_revision = git_head()
|
|
root_json, quota_json = write_self_test_inputs(root, source_revision)
|
|
payload = wiring.read_json(root_json)
|
|
payload["synthetic"] = True
|
|
wiring.write_json(root_json, payload)
|
|
try:
|
|
build_descriptor(parse_args([
|
|
"--root-authority-json", str(root_json),
|
|
"--quota-authority-json", str(quota_json),
|
|
"--out-dir", str(root / "out"),
|
|
]))
|
|
except ValueError as err:
|
|
wiring.require("synthetic" in str(err), "wrong self-test failure for synthetic root authority")
|
|
else:
|
|
raise ValueError("self-test accepted synthetic root authority")
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
source_revision = git_head()
|
|
root_json, quota_json = write_self_test_inputs(root, source_revision)
|
|
payload = wiring.read_json(quota_json)
|
|
payload["quota_authority_cases"] = payload["quota_authority_cases"][:-1]
|
|
wiring.write_json(quota_json, payload)
|
|
try:
|
|
build_descriptor(parse_args([
|
|
"--root-authority-json", str(root_json),
|
|
"--quota-authority-json", str(quota_json),
|
|
"--out-dir", str(root / "out"),
|
|
]))
|
|
except ValueError as err:
|
|
wiring.require("missing cases" in str(err), "wrong self-test failure for incomplete quota authority")
|
|
else:
|
|
raise ValueError("self-test accepted incomplete quota authority")
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--root-authority-json", type=Path)
|
|
parser.add_argument("--quota-authority-json", type=Path)
|
|
parser.add_argument("--out-dir", type=Path)
|
|
parser.add_argument("--source-revision")
|
|
parser.add_argument("--run-id")
|
|
parser.add_argument("--measurement-window-id")
|
|
parser.add_argument("--started-at")
|
|
parser.add_argument("--finished-at")
|
|
parser.add_argument("--duration-seconds", type=int, default=60)
|
|
parser.add_argument("--self-test", action="store_true")
|
|
args = parser.parse_args(argv)
|
|
if not args.self_test:
|
|
if args.root_authority_json is None:
|
|
parser.error("--root-authority-json is required unless --self-test is used")
|
|
if args.quota_authority_json is None:
|
|
parser.error("--quota-authority-json is required unless --self-test is used")
|
|
if args.out_dir is None:
|
|
parser.error("--out-dir is required unless --self-test is used")
|
|
return args
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
args = parse_args()
|
|
if args.self_test:
|
|
run_self_test()
|
|
return 0
|
|
descriptor = build_descriptor(args)
|
|
print(f"Authority release descriptor verified: {descriptor}")
|
|
return 0
|
|
except (ValueError, OSError, json.JSONDecodeError, subprocess.SubprocessError) as err:
|
|
print(f"ERROR: {err}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|