#!/usr/bin/env python3
"""Build/provision/test disposable systemd containers without host data mounts."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import time
HERE = Path(__file__).resolve().parent
PLATFORMS = {
"ubuntu": {"image": "sdp-assurance-ubuntu:26.04", "base": "ubuntu:26.04", "dockerfile": "Dockerfile.ubuntu"},
"rhel": {"image": "sdp-assurance-rhel:10.2", "base": "registry.access.redhat.com/ubi10/ubi-init:10.2", "dockerfile": "Dockerfile.rhel"},
"rhel8": {"image": "sdp-assurance-rhel8:8.10", "base": "registry.access.redhat.com/ubi8/ubi-init:8.10", "dockerfile": "Dockerfile.rhel8"},
"rhel9": {"image": "sdp-assurance-rhel9:9.8", "base": "registry.access.redhat.com/ubi9/ubi-init:9.8", "dockerfile": "Dockerfile.rhel9"},
}
LABEL = "io.perforce.sdp.disposable"
PRIMARY_STAGES = ("offline", "integration", "runtime", "maintenance", "baseline-comparison", "python-installer")
DISTRO_STAGES = tuple("distro-" + stage for stage in PRIMARY_STAGES if stage != "python-installer")
HOST_STAGE_FILES = tuple(stage + ".log" for stage in PRIMARY_STAGES + DISTRO_STAGES) + ("test-outcomes.json",)
# python-installer.log inside the container is the original compilation log,
# not a copy of the host verification-stage output. Preserve it for reuse.
CONTAINER_STAGE_FILES = tuple(name for name in HOST_STAGE_FILES if name != "python-installer.log") + ("baseline-comparison.json", "distro-baseline-comparison.json", "coverage.json")
class Matrix:
def __init__(self, args):
self.args = args
self.docker = args.docker
self.results = args.results.resolve()
self.results.mkdir(parents=True, exist_ok=True)
def command(self, arguments, logfile=None, check=True):
command = [self.docker, *arguments]
print("docker " + " ".join(arguments), flush=True)
if logfile:
with logfile.open("w") as output:
result = subprocess.run(command, stdout=output, stderr=subprocess.STDOUT)
else:
result = subprocess.run(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if check and result.returncode:
detail = "see " + str(logfile) if logfile else result.stderr
raise RuntimeError("Docker command failed: " + detail)
return result
def name(self, platform):
return self.args.name_prefix + "-" + platform
def inspect_owned(self, platform):
result = self.command(["inspect", self.name(platform)], check=False)
if result.returncode:
return None
data = json.loads(result.stdout)[0]
if data.get("Config", {}).get("Labels", {}).get(LABEL) != "true":
raise RuntimeError("Refusing to use an existing container without the disposable label")
return data
def build(self, platform):
folder = self.results / platform
folder.mkdir(exist_ok=True)
definition = HERE / PLATFORMS[platform]["dockerfile"]
self.command(["build", "--pull", "-f", str(definition), "-t", PLATFORMS[platform]["image"], str(HERE)], folder / "image-build.log")
info = self.command(["image", "inspect", PLATFORMS[platform]["image"]])
(folder / "image-inspect.json").write_text(info.stdout)
(folder / "image-build-context.json").write_text(json.dumps({
"dockerfile": definition.name, "dockerfile_sha256": hashlib.sha256(definition.read_bytes()).hexdigest(),
"image": PLATFORMS[platform]["image"], "base": PLATFORMS[platform]["base"],
"image_id": json.loads(info.stdout)[0]["Id"], "build_exit_code": 0,
}, indent=2) + "\n")
def start(self, platform):
name = self.name(platform)
folder = self.results / platform
image_record = folder / "image-inspect.json"
if not image_record.is_file():
raise RuntimeError("Build this platform first; its recorded image identity is required")
expected_image = json.loads(image_record.read_text())[0]["Id"]
data = self.inspect_owned(platform)
if data:
if data.get("Image") != expected_image:
raise RuntimeError("Existing container does not use the recorded built image; use a new name prefix")
if not data["State"]["Running"]:
self.command(["start", name])
else:
# Privileged systemd runs in the dedicated disposable Docker VM/container.
# No host directory, credentials, Docker socket or network port is mounted/published.
self.command(["run", "--detach", "--name", name, "--label", LABEL + "=true",
"--privileged", "--cgroupns=private", "--tmpfs", "/run", "--tmpfs", "/run/lock", "--tmpfs", "/tmp",
expected_image])
data = self.inspect_owned(platform)
if not data or data.get("Image") != expected_image:
raise RuntimeError("Running container image does not match the recorded build")
(folder / "image-reproducibility.json").write_text(json.dumps({
"built_image_id": expected_image, "container_image_id": data["Image"],
"matches": True, "container": name,
}, indent=2) + "\n")
for _ in range(60):
status = self.command(["exec", name, "systemctl", "is-system-running"], check=False)
if status.stdout.strip() in ("running", "degraded"):
self.command(["exec", name, "touch", "/run/sdp-container-test"])
return
time.sleep(1)
raise RuntimeError("systemd did not reach running/degraded state: " + name)
def bootstrap(self, platform):
if self.inspect_owned(platform) is None:
raise RuntimeError("Start the disposable container before bootstrapping")
source = self.args.source
if source is None or not (source / ".export-manifest.json").is_file():
raise RuntimeError("--source must be a clean export containing .export-manifest.json")
baseline = getattr(self.args, "baseline_source", None)
if baseline is None or not (baseline / ".export-manifest.json").is_file() or not (baseline / ".baseline-info.json").is_file():
raise RuntimeError("--baseline-source must be an immutable baseline export with manifest and provenance information")
folder = self.results / platform
folder.mkdir(exist_ok=True)
previous = [folder / name for name in HOST_STAGE_FILES if (folder / name).is_file()]
if previous:
archive = folder / ("previous-validation-" + str(time.time_ns()))
archive.mkdir()
for path in previous:
shutil.move(str(path), archive / path.name)
for name in ("source-manifest.json", "baseline-source-manifest.json", "coverage.json", "baseline-comparison.json", "distro-baseline-comparison.json"):
if (folder / name).is_file():
shutil.copy2(folder / name, archive / name)
name = self.name(platform)
# A reset invalidates behavioral evidence from the previous fixture.
# The completed Python installer receipt remains valid only when its
# installer hash passes python_runtime.sh's independent verification.
self.command(["exec", name, "rm", "-f", *["/results/" + item for item in CONTAINER_STAGE_FILES]])
self.command(["exec", name, "rm", "-rf", "/inputs/sdp"])
self.command(["exec", name, "mkdir", "-p", "/inputs/sdp", "/results"])
self.command(["cp", str(source.resolve()) + "/.", name + ":/inputs/sdp"])
self.command(["exec", name, "rm", "-rf", "/inputs/baseline-sdp"])
self.command(["exec", name, "mkdir", "-p", "/inputs/baseline-sdp"])
self.command(["cp", str(baseline.resolve()) + "/.", name + ":/inputs/baseline-sdp"])
self.command(["cp", str(HERE / "bootstrap.sh"), name + ":/inputs/bootstrap.sh"])
command = ["exec", name, "bash", "/inputs/bootstrap.sh", self.args.release]
if self.args.resume:
command.append("--resume")
self.command(command, folder / "bootstrap-console.log")
def select_python(self, platform, variant):
# Only generated prefixes in our labelled disposable container are selectable.
self.command(["exec", self.name(platform), "bash", "/inputs/sdp/test/containers/python_runtime.sh", "select", variant])
def test(self, platform):
if self.inspect_owned(platform) is None:
raise RuntimeError("Missing disposable container")
name = self.name(platform)
folder = self.results / platform
folder.mkdir(exist_ok=True)
root_exec = ["exec", "--env", "PATH=/p4/common/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"--env", "SDP_BASELINE_SOURCE=/inputs/baseline-sdp", "--env", "SDP_CANDIDATE_SOURCE=/inputs/sdp"]
user_exec = root_exec + ["--user", "perforce", "--workdir", "/p4/1"]
commands = {
"offline": root_exec + ["--env", "TMPDIR=/var/tmp", name, "/p4/common/python/bin/python3", "-m", "unittest", "discover", "-s", "/inputs/sdp/test", "-p", "test_*.py", "-v"],
"integration": user_exec + [name, "bash", "/p4/sdp/test/sdp_test_harness.sh", "--root", "/srv/sdp-test-integration", "--instance", "1", "--port", "19999", "--os-user", "perforce", "--confirm-disposable"],
"runtime": user_exec + [name, "/p4/common/python/bin/python3", "/inputs/sdp/test/containers/runtime_integration.py", "--root", "/srv/sdp-test-integration", "--instance", "1", "--port", "19999", "--os-user", "perforce", "--confirm-disposable"],
"maintenance": user_exec + [name, "/p4/common/python/bin/python3", "/inputs/sdp/test/containers/maintenance_integration.py", "--root", "/srv/sdp-test-integration", "--instance", "1", "--port", "19999", "--os-user", "perforce", "--confirm-disposable"],
"baseline-comparison": root_exec + ["--env", "TMPDIR=/var/tmp", name, "/p4/common/python/bin/python3", "/inputs/sdp/test/containers/baseline_comparison.py", "--baseline", "/inputs/baseline-sdp", "--candidate", "/inputs/sdp", "--output", "/results/baseline-comparison.json"],
"python-installer": root_exec + [name, "bash", "/inputs/sdp/test/containers/python_runtime.sh", "verify"],
}
outcome_file = folder / "test-outcomes.json"
outcomes = json.loads(outcome_file.read_text()) if outcome_file.is_file() else {}
selected = getattr(self.args, "test_stage", None) or list(PRIMARY_STAGES + DISTRO_STAGES)
try:
for kind in selected:
distro = kind.startswith("distro-")
outcomes[kind] = 1
self.select_python(platform, "distro" if distro else "baseline")
command = list(commands[kind.removeprefix("distro-")])
if distro and kind == "distro-baseline-comparison":
command[-1] = "/results/distro-baseline-comparison.json"
outcomes[kind] = self.command(command, folder / (kind + ".log"), check=False).returncode
finally:
try:
self.select_python(platform, "baseline")
outcomes.pop("python-runtime-restore", None)
except (OSError, RuntimeError) as error:
outcomes["python-runtime-restore"] = 1
raise RuntimeError("Failed to restore the primary Python runtime") from error
finally:
(folder / "test-outcomes.json").write_text(json.dumps(outcomes, indent=2) + "\n")
return any(outcomes.values())
def collect(self, platform):
data = self.inspect_owned(platform)
if not data:
raise RuntimeError("Missing disposable container")
folder = self.results / platform
folder.mkdir(exist_ok=True)
(folder / "container-inspect.json").write_text(json.dumps(data, indent=2) + "\n")
# Stage output and exit codes belong to the host runner. A resumed or
# manually inspected container may still contain older copies of them.
saved_stage_files = {name: (folder / name).read_bytes() if (folder / name).is_file() else None
for name in HOST_STAGE_FILES}
self.command(["cp", self.name(platform) + ":/results/.", str(folder)], check=False)
for name, contents in saved_stage_files.items():
path = folder / name
if contents is not None:
path.write_bytes(contents)
elif path.is_file():
path.unlink() # Never manufacture an unrun stage from stale container output.
self.command(["exec", self.name(platform), "journalctl", "-u", "p4d_1.service", "--no-pager"], folder / "service-journal.log", check=False)
self.command(["exec", self.name(platform), "systemctl", "show", "p4d_1.service", "-p", "ActiveState", "-p", "SubState", "-p", "MainPID"], folder / "service-final-state.txt", check=False)
coverage = HERE / "coverage.py"
if coverage.is_file() and self.args.source:
result = subprocess.run([os.sys.executable, str(coverage), "--source", str(self.args.source), "--results", str(folder), "--output", str(folder / "coverage.json")])
if result.returncode:
raise RuntimeError("Per-script coverage report failed")
def cleanup(self, platform):
if self.inspect_owned(platform):
self.command(["rm", "--force", self.name(platform)])
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("stage", choices=("build", "start", "bootstrap", "test", "collect", "cleanup", "all"))
parser.add_argument("--platform", choices=(*PLATFORMS, "both", "all"), default="all")
parser.add_argument("--source", type=Path, help="Clean export from export_source.py")
parser.add_argument("--baseline-source", type=Path, help="Immutable deployed-archive or submitted baseline export, copied without host mounts")
parser.add_argument("--results", type=Path, required=True)
parser.add_argument("--release", default="r26.1")
parser.add_argument("--test-stage", action="append", choices=PRIMARY_STAGES + DISTRO_STAGES, help="Select one test stage; repeat to select several. Unselected outcomes are preserved.")
parser.add_argument("--resume", action="store_true", help="Explicitly reset an existing marked disposable deployment during bootstrap")
parser.add_argument("--name-prefix", default="sdp-assurance-matrix")
parser.add_argument("--docker", default=shutil.which("docker") or "docker")
args = parser.parse_args()
if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.-]*", args.name_prefix):
parser.error("invalid name prefix")
matrix = Matrix(args)
platforms = list(PLATFORMS) if args.platform == "all" else (["ubuntu", "rhel"] if args.platform == "both" else [args.platform])
failed = False
for platform in platforms:
try:
if args.stage == "all":
for stage in ("build", "start", "bootstrap", "test", "collect"):
failed = bool(getattr(matrix, stage)(platform)) or failed
else:
failed = bool(getattr(matrix, args.stage)(platform)) or failed
except (OSError, RuntimeError, ValueError) as error:
print("ERROR [%s]: %s" % (platform, error), flush=True)
failed = True
if args.stage not in ("build", "cleanup"):
try:
matrix.collect(platform)
except (OSError, RuntimeError, ValueError):
pass
return int(failed)
if __name__ == "__main__":
raise SystemExit(main())
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 34004 | Russell C. Jackson (Rusty) |
Improve SDP backup/recovery, Maintenance, logging and cloud operation. Add owned checkpoint locks, recovery-aware retention, preserved database switches, bounded cleanup and coordinated AWS/Azure snapshots. Fix service-user password initialization, edge staging, standby promotion state parsing, proxy cache setup, typed depot verification and historical-server configuration/error handling. Remove unused OTP programs; keep Windows removed, server.locks cleanup and the journal-count condition unchanged. Add no global shell strict modes. Update operational documentation and add reproducible mkdirs.sh/mkdirs.cfg container assurance. Validate Ubuntu 26.04 and UBI 8.10/9.8/10.2 across both Python runtimes: 44 stages; 428 offline tests per runtime/OS, zero offline skips; 19 deployed-archive comparisons per combination. Three six-node topology runs pass all 30 recovery/failure scenarios. P4D 2023.1 through 2026.1 on UBI 8/9 pass all 14 cells and 98 scenarios. Static, documentation and provenance checks pass. Containers do not certify every fleet kernel, patch build or integration. Tested source manifest SHA-256: e1a23e17b8ced8c1148d9a250589977c6f6e98606bf2a81dc3ff5d7c909e3695 Deployed comparison archive SHA-256: 1bd23217356ee3808e040d39aea5be9519bda7c8f2b4924656a70aff8ad7293a |