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
10 changes: 6 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ before doing expensive work or acquiring a guest.
`release-clean` resolves VM lanes to `clean` and the physical Mac to `ready`;
the Mac consumer then requests the stronger `runtime-ready` cleanup contract on
its lease. `dev-fast` never silently falls back from its declared mapping.
Every accepted clean or named baseline has a SHA-256 provenance manifest under
`golden/manifests/`. Provenance binds the selected VM's canonical image and
provisioning contract. Profiles, retention policy, physical hosts, and other VM
definitions do not invalidate an unchanged baseline.
Every accepted clean or named baseline has a SHA-256 fingerprint manifest under
`golden/manifests/`. The record describes the baseline disk, UEFI state, TPM
state, and QEMU image metadata; it is not coupled to the mutable controller
manifest. `doctor` validates the current controller and provisioning contract
separately, while `doctor --deep` re-hashes accepted clean images when byte-level
integrity must be re-established.
9 changes: 5 additions & 4 deletions docs/checkpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public snapshot interface.

Test the checkpoint first with an explicit `preflight --baseline NAME`. If it is
then selected by a shared profile, update `lab-manifest.json` and run `install.sh`
against the deployed lab. Recapture provenance only when that VM's image or
provisioning contract changes. Profile-only changes do not invalidate baseline
provenance. Then run `lab.sh doctor --strict` and the exact consumer/profile
`lab.sh preflight`.
against the deployed lab. Recapture its fingerprint only when the checkpoint's
disk, UEFI, or TPM state changes. Controller, profile, and provisioning-source
changes do not rewrite or invalidate an unchanged checkpoint record. Validate
those current inputs with `lab.sh doctor --strict`, then run the exact
consumer/profile `lab.sh preflight`.
9 changes: 5 additions & 4 deletions docs/rebuild.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ Capture the controller-owned manifest immediately after accepting a baseline:
./lab.sh doctor --strict
```

The capture records the whole-manifest hash for diagnostics and a canonical
hash of the selected VM's image and provisioning contract for validation.
Changing profiles, retention, hosts, or another VM does not make this baseline
stale. Changing this VM's contract does.
The capture records baseline disk, UEFI, TPM, and QEMU image metadata. A
controller-manifest hash may be retained as capture-time diagnostics, but it is
not a validity gate: changing controller configuration cannot change the bytes
of an already captured baseline. `doctor` validates current controller and
provisioning inputs independently.

`doctor --deep` additionally re-hashes the clean golden disks. Run it after a
rebuild or suspected storage corruption; routine consumers use `--strict` and
Expand Down
44 changes: 10 additions & 34 deletions lab.sh
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ lease_command() {
if [[ -n "$lease_signal" && "$result" == 0 ]]; then
case "$lease_signal" in INT) result=130 ;; TERM) result=143 ;; HUP) result=129 ;; esac
fi
if [[ "$kind" == vm && -n "$cleanup_baseline" && "$result" -ge 128 ]]; then
if [[ "$kind" == vm && -n "$cleanup_baseline" ]]; then
if "$ENGINE" status "$vm" | grep -Eq "^${vm} running "; then
"$ENGINE" stop "$vm" || result=1
fi
Expand Down Expand Up @@ -717,25 +717,8 @@ tree_digest() {
done | sha256sum | awk '{print $1}'
}

provenance_contract_sha() {
local vm="$1"
python3 - "$MANIFEST" "$vm" <<'PY'
import hashlib, json, sys
path, vm = sys.argv[1:]
with open(path) as handle:
manifest = json.load(handle)
contract = {
"schemaVersion": 1,
"vm": vm,
"vmSpec": manifest["vms"][vm],
}
encoded = json.dumps(contract, sort_keys=True, separators=(",", ":")).encode()
print(hashlib.sha256(encoded).hexdigest())
PY
}

provenance_capture_one() {
local vm="$1" name="$2" destination disk_sha vars_sha tpm_sha contract_sha
local vm="$1" name="$2" destination disk_sha vars_sha tpm_sha
golden_paths "$vm" "$name"
[[ -f "$GOLDEN_CAPTURE_DISK" ]] || { printf 'Missing golden disk: %s\n' "$GOLDEN_CAPTURE_DISK" >&2; return 1; }
[[ -f "$GOLDEN_CAPTURE_VARS" ]] || { printf 'Missing golden UEFI state: %s\n' "$GOLDEN_CAPTURE_VARS" >&2; return 1; }
Expand All @@ -747,12 +730,11 @@ provenance_capture_one() {
disk_sha="$(sha256sum "$GOLDEN_CAPTURE_DISK" | awk '{print $1}')"
vars_sha="$(sha256sum "$GOLDEN_CAPTURE_VARS" | awk '{print $1}')"
tpm_sha="$(tree_digest "$GOLDEN_CAPTURE_TPM")"
contract_sha="$(provenance_contract_sha "$vm")"
mkdir -p "$LAB_ROOT/golden/manifests"
destination="$LAB_ROOT/golden/manifests/${vm}-${name}.json"
python3 - "$destination" "$vm" "$(distro_name "$vm")" "$name" "$disk_sha" "$vars_sha" "$tpm_sha" "$LAB_VERSION" "$MANIFEST" "$GOLDEN_CAPTURE_DISK" "$contract_sha" <<'PY'
python3 - "$destination" "$vm" "$(distro_name "$vm")" "$name" "$disk_sha" "$vars_sha" "$tpm_sha" "$LAB_VERSION" "$MANIFEST" "$GOLDEN_CAPTURE_DISK" <<'PY'
import datetime, hashlib, json, os, subprocess, sys, tempfile
path, vm, distro, name, disk_sha, vars_sha, tpm_sha, version, config_path, disk_path, contract_sha = sys.argv[1:]
path, vm, distro, name, disk_sha, vars_sha, tpm_sha, version, config_path, disk_path = sys.argv[1:]
config_sha = hashlib.sha256(open(config_path, "rb").read()).hexdigest() if os.path.exists(config_path) else None
qemu_info = json.loads(subprocess.check_output(["qemu-img", "info", "--output=json", disk_path]))
record = {
Expand All @@ -764,7 +746,6 @@ record = {
"provenance": "captured-current-state",
"controllerVersion": version,
"labManifestSha256": config_sha,
"provenanceContractSha256": contract_sha,
"diskSha256": disk_sha,
"uefiVarsSha256": vars_sha,
"tpmTreeSha256": tpm_sha,
Expand Down Expand Up @@ -804,24 +785,19 @@ provenance_command() {

provenance_metadata_valid() {
local path="$1" vm="$2" baseline="$3"
local contract_sha
contract_sha="$(provenance_contract_sha "$vm")"
python3 - "$path" "$MANIFEST" "$vm" "$baseline" "$contract_sha" <<'PY'
import hashlib, json, re, sys
path, config_path, vm, baseline, contract_sha = sys.argv[1:]
python3 - "$path" "$vm" "$baseline" <<'PY'
import json, re, sys
path, vm, baseline = sys.argv[1:]
try:
with open(path) as handle:
record = json.load(handle)
with open(config_path, "rb") as handle:
config_sha = hashlib.sha256(handle.read()).hexdigest()
assert record["schemaVersion"] == 1
assert record["vm"] == vm
assert record["baseline"] == baseline
if "labManifestSha256" in record:
assert re.fullmatch(r"[0-9a-f]{64}", record["labManifestSha256"])
if "provenanceContractSha256" in record:
assert record["provenanceContractSha256"] == contract_sha
else:
# Compatibility for provenance captured before contract-scoped hashes.
assert record["labManifestSha256"] == config_sha
assert re.fullmatch(r"[0-9a-f]{64}", record["provenanceContractSha256"])
assert re.fullmatch(r"[0-9a-f]{64}", record["diskSha256"])
assert re.fullmatch(r"[0-9a-f]{64}", record["uefiVarsSha256"])
assert record["tpmTreeSha256"] == "none" or re.fullmatch(r"[0-9a-f]{64}", record["tpmTreeSha256"])
Expand Down
7 changes: 7 additions & 0 deletions release-notes.d/baseline-fingerprints-and-lease-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: fixed
area: controller
---

Keep unchanged VM baselines usable when controller configuration changes, and
honor `--cleanup-baseline` after successful, failed, and interrupted VM leases.
50 changes: 19 additions & 31 deletions tests/lab_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,10 @@ python3 - "$test_root/lab-manifest.json" "$test_root/golden/manifests/appimage-d
import hashlib, json, sys
with open(sys.argv[1], "rb") as handle:
config_sha = hashlib.sha256(handle.read()).hexdigest()
with open(sys.argv[1]) as handle:
manifest = json.load(handle)
contract = {"schemaVersion": 1, "vm": "appimage", "vmSpec": manifest["vms"]["appimage"]}
contract_sha = hashlib.sha256(json.dumps(contract, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
record = {
"schemaVersion": 1,
"vm": "appimage",
"labManifestSha256": config_sha,
"provenanceContractSha256": contract_sha,
"diskSha256": "a" * 64,
"uefiVarsSha256": "b" * 64,
"tpmTreeSha256": "none",
Expand All @@ -59,55 +54,44 @@ PY
python3 -c 'import json,sys; assert json.load(sys.stdin)["ready"] is True'
"$test_root/lab.sh" preflight cli release-clean --lanes appimage --json |
python3 -c 'import json,sys; assert json.load(sys.stdin)["ready"] is True'
python3 - "$test_root/golden/manifests/appimage-clean.json" <<'PY'
python3 - "$test_root/lab-manifest.json" <<'PY'
import json, sys
path = sys.argv[1]
with open(path) as handle:
record = json.load(handle)
record.pop("provenanceContractSha256")
manifest = json.load(handle)
manifest["profiles"]["dev-fast"]["deb"] = "desktop-e2e-v4"
with open(path, "w") as handle:
json.dump(record, handle)
json.dump(manifest, handle, indent=2, sort_keys=True)
handle.write("\n")
PY
"$test_root/lab.sh" preflight cli release-clean --lanes appimage --json |
"$test_root/lab.sh" preflight desktop dev-fast --lanes appimage --json |
python3 -c 'import json,sys; assert json.load(sys.stdin)["ready"] is True'
python3 - "$test_root/golden/manifests/appimage-clean.json" "$test_root/golden/manifests/appimage-desktop-e2e-v2.json" <<'PY'
import json, sys
path, source = sys.argv[1:]
with open(path) as handle:
record = json.load(handle)
with open(source) as handle:
current = json.load(handle)
record["provenanceContractSha256"] = current["provenanceContractSha256"]
with open(path, "w") as handle:
json.dump(record, handle)
PY
python3 - "$test_root/lab-manifest.json" <<'PY'
import json, sys
path = sys.argv[1]
with open(path) as handle:
manifest = json.load(handle)
manifest["profiles"]["dev-fast"]["deb"] = "desktop-e2e-v4"
manifest["vms"]["appimage"]["memoryMiB"] += 1
with open(path, "w") as handle:
json.dump(manifest, handle, indent=2, sort_keys=True)
handle.write("\n")
PY
"$test_root/lab.sh" preflight cli release-clean --lanes appimage --json |
python3 -c 'import json,sys; assert json.load(sys.stdin)["ready"] is True'
python3 - "$test_root/lab-manifest.json" <<'PY'
python3 - "$test_root/golden/manifests/appimage-clean.json" <<'PY'
import json, sys
path = sys.argv[1]
with open(path) as handle:
manifest = json.load(handle)
manifest["vms"]["appimage"]["memoryMiB"] += 1
record = json.load(handle)
record["diskSha256"] = "not-a-digest"
with open(path, "w") as handle:
json.dump(manifest, handle, indent=2, sort_keys=True)
handle.write("\n")
json.dump(record, handle)
PY
if changed_contract_result="$("$test_root/lab.sh" preflight cli release-clean --lanes appimage --json)"; then
printf 'VM contract change unexpectedly preserved provenance\n' >&2
if malformed_fingerprint_result="$("$test_root/lab.sh" preflight cli release-clean --lanes appimage --json)"; then
printf 'Malformed baseline fingerprint unexpectedly passed preflight\n' >&2
exit 1
fi
python3 -c 'import json,sys; assert json.load(sys.stdin)["ready"] is False' <<<"$changed_contract_result"
python3 -c 'import json,sys; assert json.load(sys.stdin)["ready"] is False' <<<"$malformed_fingerprint_result"
"$test_root/lab.sh" preflight cli release-clean --lanes macos-arm64 --json |
python3 -c 'import json,sys; assert json.load(sys.stdin)["ready"] is True'
if "$test_root/lab.sh" start ubuntu >/dev/null 2>&1; then
Expand All @@ -116,17 +100,21 @@ if "$test_root/lab.sh" start ubuntu >/dev/null 2>&1; then
fi
"$test_root/lab.sh" lease ubuntu test successful -- bash -c 'test "$OMNIDECK_VM_LAB_VM" = appimage'
[[ ! -e "$test_root/discarded/runs/successful-appimage" ]]
"$test_root/lab.sh" lease ubuntu test successful-cleanup --cleanup-baseline clean -- true
grep -Fxq 'reset appimage clean' "$test_root/runtime/fake-actions.log"
[[ ! -e "$test_root/discarded/runs/successful-cleanup-appimage" ]]
"$test_root/lab.sh" lease macos test host-successful -- "$test_root/lab.sh" run macos true
[[ ! -e "$test_root/discarded/runs/host-successful-macos-arm64" ]]
if "$test_root/lab.sh" lease macos test host-cleanup-failed --cleanup-baseline runtime-ready -- bash -c 'exit 7'; then
printf 'failing physical-host lease unexpectedly succeeded\n' >&2
exit 1
fi
grep -Fxq 'reset macos-arm64 runtime-ready' "$test_root/runtime/fake-actions.log"
if "$test_root/lab.sh" lease debian test failed -- bash -c 'exit 9'; then
if "$test_root/lab.sh" lease debian test failed --cleanup-baseline clean -- bash -c 'exit 9'; then
printf 'failing leased command unexpectedly succeeded\n' >&2
exit 1
fi
grep -Fxq 'reset deb clean' "$test_root/runtime/fake-actions.log"
[[ -f "$test_root/discarded/runs/failed-deb/metadata.json" ]]

"$test_root/lab.sh" lease fedora test held -- bash -c 'sleep 2' &
Expand Down