diff --git a/dag/extdeps/os/ubuntu_autoinstall.dag b/dag/extdeps/os/ubuntu_autoinstall.dag index f2d0577feb0..1d31ca8f949 100644 --- a/dag/extdeps/os/ubuntu_autoinstall.dag +++ b/dag/extdeps/os/ubuntu_autoinstall.dag @@ -54,3 +54,14 @@ type UbuntuAutoinstallPayload { ssh_password_auth: Bool storage_policy: AutoinstallStoragePolicy } + +// A LIVE-SESSION autoinstall: upstream's `early-commands` run in the live installer environment +// before any other autoinstall step, and `interactive-sections: ["*"]` hands every section after them +// to a human. So the commands are the whole unattended content of the seed, and nothing that writes +// storage runs unless someone at the console drives it. Both keys and their semantics are the +// autoinstall reference's (module anchor); the type carries no identity or storage because an +// all-interactive session asks for them at the console instead. +type UbuntuAutoinstallLiveCommands { + autoinstall_version: Int + early_commands: List +} diff --git a/dag/extdeps/provisioning/ubuntu_seeded_install_media.dag b/dag/extdeps/provisioning/ubuntu_seeded_install_media.dag index e20bd5a5630..ca4c5029133 100644 --- a/dag/extdeps/provisioning/ubuntu_seeded_install_media.dag +++ b/dag/extdeps/provisioning/ubuntu_seeded_install_media.dag @@ -1,13 +1,8 @@ module extdeps.provisioning.ubuntu_seeded_install_media -import std.types { NonEmptyStr, String } -import std.content_hash { ContentHash } -import std.content_hash { content_hash_atom, content_hash_combine_structural, content_hash_tagged } -import extdeps.provisioning.ubuntu_install_media { - UbuntuInstallMediaArtifactRow, - ubuntu_install_media_artifacts_dir, -} -import extdeps.os.ubuntu_autoinstall { UbuntuAutoinstallPayload } +import std.types { Int, List, NonEmptyStr, String } +import std.content_hash { Fnv1a64Structural, content_hash_atom, content_hash_combine_structural, content_hash_tagged_structural } +import extdeps.provisioning.ubuntu_install_media { UbuntuInstallMediaArtifactRow } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } @@ -18,15 +13,62 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } -type UbuntuSeededInstallMediaFlavor = | UbuntuLiveServerSeeded +// A file the NoCloud seed directory carries beside user-data and meta-data. The seed directory is +// the medium's only payload channel that needs no network at runtime, so a program the seed runs +// rides here rather than being fetched. +type NoCloudSeedFile { + name: NonEmptyStr + content: NonEmptyStr +} -type UbuntuSeededInstallMediaArtifactRow { +// WHAT A DERIVED SEEDED IMAGE IS BUILT FROM, and nothing else: every field below reaches the output +// bytes, so each one is in the derivation key. The payload is a PARAMETER -- an install autoinstall +// (srv3) and a live-session census program (Mt. Collins) are both a rendered user-data body plus seed +// files, rendered by their own layer; this module does not know which it is carrying. The volume id +// is a parameter because it decides the block-device label the booted host reports, so a consumer +// that checks the boot medium must expect the derived label, never the stock one. +type UbuntuSeededInstallMediaBuildInput { stock_artifact: UbuntuInstallMediaArtifactRow - autoinstall: UbuntuAutoinstallPayload - autoinstall_user_data_body: NonEmptyStr - grub_kernel_cmdline: NonEmptyStr nocloud_dir_on_iso: NonEmptyStr - content_hash: ContentHash + user_data_body: NonEmptyStr + seed_files: List + grub_kernel_cmdline: NonEmptyStr + volume_id: NonEmptyStr + pinned_dates: SeededInstallMediaPinnedDates + image_stem: NonEmptyStr + builder_revision: NonEmptyStr +} + +// THE DATES ARE PINNED SO THAT IDENTICAL INPUTS CAN REPRODUCE IDENTICAL BYTES, AND THAT IS A BET +// UNTIL A BUILD RUNS (DESIGN §4d: an inference is typed as a bet, not promoted). What is established: +// the replay stamps the build instant into the volume descriptors and into mapped files, so WITHOUT +// pinning a rebuild of the same inputs measures a different digest. What is NOT established: that +// pinning these two dates is SUFFICIENT for byte-identical output -- xorriso may carry other +// build-varying state, and the ordering these commands rely on (both -volume_date commands after the +// -map commands, so freshly mapped files are covered) is read off the tool's left-to-right execution +// rather than measured. WHAT WOULD SETTLE IT: the srv2 wet control named in +// gunbc.machine_intake_mtcollins1_census_image -- build twice from one input, compare the two measured +// digests, and change one seed byte and compare again. +// +// NOTHING SILENTLY DEPENDS ON THE BET. If a rebuild is not byte-identical it produces a DIFFERENT +// digest, so it publishes under a different name; create-only publication refuses nothing and +// replaces nothing, and the derivation record names whichever bytes were actually built. The cost of +// the bet being wrong is a second published image, not a wrong one. +// +// uuid is xorriso's 16-digit volume date (it is also the medium's blkid UUID); file_date is the +// -alter_date timestring applied to every file. +type SeededInstallMediaPinnedDates { + volume_uuid: NonEmptyStr + file_date: NonEmptyStr +} + +// THE IDENTITY IS THE MEASURED OUTPUT, not this. The build key names the derivation (which inputs, +// which builder) so a later reader can find what was built from them; the published name carries the +// sha256 of the bytes the replay actually wrote, read after the build. +type UbuntuSeededInstallMediaBuilt { + build_key: NonEmptyStr + output_digest: NonEmptyStr + image_name: NonEmptyStr } // The ds= NoCloud seedfrom carries a ';' (e.g. ds=nocloud;s=). In a grub.cfg linux line ';' is grub's command separator, so the arg MUST be double-quoted or grub truncates the cmdline at ';' and the kernel never sees s= — autoinstall then falls to interactive. These builders emit grub cmdline strings (injected by the remaster sed), so they quote the ds arg here; the List grub path quotes structurally in grub_cmdline_arg_render. @@ -42,46 +84,80 @@ fn ubuntu_seeded_install_media_default_nocloud_dir(hostname: NonEmptyStr) -> Non concat("nocloud/", hostname as String) as NonEmptyStr } -fn seeded_install_media_content_hash( - stock_content_sha256: NonEmptyStr, - autoinstall_user_data_body: NonEmptyStr, - grub_kernel_cmdline: NonEmptyStr, - nocloud_dir_on_iso: NonEmptyStr, -) -> ContentHash { - content_hash_tagged( - tag: "ubuntu-seeded-install-media/v2", +fn seeded_install_media_seed_file_key(file: NoCloudSeedFile) -> Fnv1a64Structural { + content_hash_combine_structural( + content_hash_atom(value: file.name), + content_hash_atom(value: file.content), + ) +} + +fn seeded_install_media_build_key(input: UbuntuSeededInstallMediaBuildInput) -> NonEmptyStr { + let seed = fold( + input.seed_files, + init: content_hash_combine_structural( + content_hash_atom(value: input.nocloud_dir_on_iso), + content_hash_atom(value: input.user_data_body), + ), + f: (acc, file) => content_hash_combine_structural(acc, seeded_install_media_seed_file_key(file: file)), + ) + let boot = content_hash_combine_structural( + content_hash_combine_structural( + content_hash_atom(value: input.grub_kernel_cmdline), + content_hash_atom(value: input.volume_id), + ), + content_hash_combine_structural( + content_hash_atom(value: input.pinned_dates.volume_uuid), + content_hash_atom(value: input.pinned_dates.file_date), + ), + ) + content_hash_tagged_structural( + tag: "ubuntu-seeded-install-media/v3", payload: content_hash_combine_structural( content_hash_combine_structural( - content_hash_combine_structural( - content_hash_atom(value: stock_content_sha256), - content_hash_atom(value: autoinstall_user_data_body), - ), - content_hash_atom(value: grub_kernel_cmdline), + content_hash_atom(value: input.stock_artifact.content_sha256 as NonEmptyStr), + content_hash_combine_structural(seed, boot), + ), + content_hash_combine_structural( + content_hash_atom(value: input.image_stem), + content_hash_atom(value: input.builder_revision), ), - content_hash_atom(value: nocloud_dir_on_iso), ), - ) + ).digest as NonEmptyStr } -fn ubuntu_seeded_install_media_filename(artifact: UbuntuSeededInstallMediaArtifactRow) -> NonEmptyStr { +// Sixteen hex digits of the output sha256 name the image: enough to be unique across every image +// this fleet will ever build, short enough to read on a BMC's virtual-media page. The full digest +// travels in UbuntuSeededInstallMediaBuilt and is what every read-back compares. +data seeded_install_media_name_digest_width: Int = 16 + +fn ubuntu_seeded_install_media_image_name(image_stem: NonEmptyStr, output_digest: NonEmptyStr) -> NonEmptyStr { concat( - concat( - concat("ubuntu-", artifact.stock_artifact.point_release as String), - "-live-server-", - ), - concat( - concat(artifact.autoinstall.identity.hostname as String, "-seeded.iso"), - ), + image_stem as String, + "-", + substring(s: output_digest as String, start: 0, end: seeded_install_media_name_digest_width), + ".iso", ) as NonEmptyStr } -fn ubuntu_seeded_install_media_install_path(artifact: UbuntuSeededInstallMediaArtifactRow) -> NonEmptyStr { +fn ubuntu_seeded_install_media_built(input: UbuntuSeededInstallMediaBuildInput, output_digest: NonEmptyStr) -> UbuntuSeededInstallMediaBuilt { + UbuntuSeededInstallMediaBuilt { + build_key: seeded_install_media_build_key(input: input), + output_digest: output_digest, + image_name: ubuntu_seeded_install_media_image_name(image_stem: input.image_stem, output_digest: output_digest), + } +} + +// The derivation record: build key -> measured output digest. It is how a consumer that did not run +// the build (an actuator in another process) finds the image the inputs it holds produced, and every +// consumer re-measures the named file against the recorded digest before trusting it. +fn ubuntu_seeded_install_media_record_path(dir: NonEmptyStr, input: UbuntuSeededInstallMediaBuildInput) -> NonEmptyStr { concat( - concat(ubuntu_install_media_artifacts_dir() as String, "/"), - ubuntu_seeded_install_media_filename(artifact: artifact) as String, + dir as String, "/.", input.image_stem as String, ".", seeded_install_media_build_key(input: input) as String, ".built", ) as NonEmptyStr } -fn ubuntu_seeded_install_media_part_path(install_path: NonEmptyStr) -> NonEmptyStr { - concat(install_path as String, ".part") as NonEmptyStr +fn ubuntu_seeded_install_media_staging_path(dir: NonEmptyStr, input: UbuntuSeededInstallMediaBuildInput) -> NonEmptyStr { + concat( + dir as String, "/.", input.image_stem as String, ".", seeded_install_media_build_key(input: input) as String, ".staging", + ) as NonEmptyStr } diff --git a/dag/extdeps/provisioning/ubuntu_seeded_install_media_remaster.dag b/dag/extdeps/provisioning/ubuntu_seeded_install_media_remaster.dag index 643f7f39f20..15ea6dce742 100644 --- a/dag/extdeps/provisioning/ubuntu_seeded_install_media_remaster.dag +++ b/dag/extdeps/provisioning/ubuntu_seeded_install_media_remaster.dag @@ -8,17 +8,20 @@ import extdeps.filesystem.filesystem_io { Filesystem } import extdeps.tools.xorriso import extdeps.tools.grep import extdeps.tools.sed -import extdeps.provisioning.ubuntu_seeded_install_media { - UbuntuSeededInstallMediaArtifactRow, - ubuntu_seeded_install_media_install_path, - ubuntu_seeded_install_media_part_path, +import extdeps.tools.sha256sum { + Sha256FileDigest, + Sha256FileDigestUnavailable, + Sha256Matches, + Sha256Mismatch, + Sha256VerifyInfraFailure, + sha256sum_file_digest_via_shell, + sha256sum_verify_via_shell, } -import extdeps.provisioning.ubuntu_install_media { - ubuntu_install_media_install_path, - ubuntu_install_media_artifacts_dir, +import extdeps.provisioning.ubuntu_seeded_install_media { + NoCloudSeedFile, + UbuntuSeededInstallMediaBuildInput, } -import std.types { String, NonEmptyStr, FilePath } -import std.disposition { Disposition, Terminal } +import std.types { Bool, List, String, NonEmptyStr, FilePath } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -27,28 +30,6 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } -data install_media_remaster_bash_emit_dissolution_trigger: Disposition = Terminal { - reason: "in-process typed reconcile (shell.Test.IsFile, xorriso.Iso.*, grep.Grep.MatchesFixedString, sed.Sed.InPlaceSubstitute, Filesystem.Write, shell.Move.File) replaces ShellProgram/serialize_bash - no shell text is built or serialized" -} - -data install_media_remaster_classification_scaffold: Disposition = Terminal { - reason: "InstallMediaRemasterObservation (Absent|Valid|Stale|VerifyInfraFailure) is the single classification authority, derived by observe_install_media_remaster - no parallel ad hoc staleness check remains" -} - -data install_media_remaster_classification_dissolution_trigger: Disposition = Terminal { - reason: "install_media_remaster_reconcile dispatches on InstallMediaRemasterObservation directly, not on inline shell text" -} - -data install_media_remaster_chmod_access_upsert_scaffold: Disposition = Terminal { - reason: "shell.Chmod.RecursiveWritable is the single typed authority for making the extracted work tree writable - no inline chmod RawLine remains" -} - -data install_media_remaster_chmod_access_upsert_dissolution_trigger: Disposition = Terminal { - reason: "install_media_remaster_build calls shell.Chmod.RecursiveWritable directly" -} - -data install_media_remaster_probe_refusal_shape_doc: String = "DESIGN §5 refusal-shape fix (cursor review, PR #6596): install_media_remaster_probe_verdict now returns a 3-way InstallMediaRemasterProbeVerdict (Valid|Stale|InfraFailure\{reason\}) instead of collapsing the mktemp probe into a Bool, so a shell.Mktemp.Dir failure stays distinguishable from a genuine content/grub/seed mismatch (InstallMediaRemasterVerifyInfraFailure vs InstallMediaRemasterStale). observe_install_media_remaster matches this verdict directly, and install_media_remaster_reconcile refuses fail-closed on infra failure instead of silently treating an indeterminate probe as stale-and-rebuild." - // xorriso -boot_image any replay modifies the stock ISO by mapping in only the NoCloud seed dir and // grub.cfg, so the arm64 El Torito/ESP boot structure survives; an -as mkisofs rebuild destroyed it // and the media stopped booting (root-caused live on srv4, PR #7027); instance-id derives from the @@ -68,82 +49,22 @@ data esp_boot_structure_authority: ExternalAuthority = ExternalAuthority { } } -// The boot structure the replay preserves is owned by two specs the fidelity doc names in prose: -// the El Torito Bootable CD-ROM Format Specification 1.0 (Phoenix/IBM 1995; anchor is a hosted copy -// of the original) and the UEFI spec's EFI System Partition / media-access section (UEFI 2.10 §13). -// These are the entities the mkisofs rebuild violated. -type InstallMediaRemasterProbeVerdict - = InstallMediaRemasterProbeValid - | InstallMediaRemasterProbeStale - | InstallMediaRemasterProbeInfraFailure { reason: NonEmptyStr } - -type InstallMediaRemasterObservation - = InstallMediaRemasterAbsent - | InstallMediaRemasterValid - | InstallMediaRemasterStale - | InstallMediaRemasterVerifyInfraFailure { reason: NonEmptyStr } - -type InstallMediaRemasterOutcome - = InstallMediaRemasterAlreadyPresent { path: NonEmptyStr } - | InstallMediaRemasterRemastered { path: NonEmptyStr } - | InstallMediaRemasterRefused { reason: NonEmptyStr } - -fn install_media_remaster_sidecar_path(final_path: NonEmptyStr) -> NonEmptyStr { - concat(final_path as String, ".gunbc-content-hash") as NonEmptyStr -} - -fn install_media_remaster_sidecar_matches(final_path: NonEmptyStr, expected_hash: NonEmptyStr) -> Bool { - let read = Filesystem.Read(path: install_media_remaster_sidecar_path(final_path: final_path) as String) - read.success && (read.content == expected_hash as String) -} +// THE BUILDER STOPS AT A STAGED IMAGE AND ITS MEASURED DIGEST. Where the image is published, under +// which name, and who may write there are the publishing layer's decisions; this module's answer is +// only "these inputs produced bytes whose sha256 is D, and they are at the staging path". The digest +// is READ from the written file, never computed from the inputs, so a builder that wrote something +// other than what its inputs describe cannot report the inputs' identity for it. +type SeededInstallMediaStageOutcome + = SeededInstallMediaStaged { staging_path: NonEmptyStr, output_digest: NonEmptyStr } + | SeededInstallMediaStageRefused { reason: NonEmptyStr } -fn install_media_remaster_probe_verdict(artifact: UbuntuSeededInstallMediaArtifactRow, final_path: NonEmptyStr) -> InstallMediaRemasterProbeVerdict { - let probe = shell.Mktemp.Dir() - if probe.success == false { - InstallMediaRemasterProbeInfraFailure { reason: "mktemp -d failed for staleness probe" as NonEmptyStr } - } else { - let grub_probe = concat(probe.path as String, "/grub.cfg") as NonEmptyStr - let user_probe = concat(probe.path as String, "/user-data") as NonEmptyStr - let user_internal = concat("/", concat(artifact.nocloud_dir_on_iso as String, "/user-data")) as NonEmptyStr - let grub_extracted = xorriso.Iso.ExtractPathBestEffort( - iso_path: final_path, - iso_internal_path: "/boot/grub/grub.cfg" as NonEmptyStr, - destination: grub_probe, - ) - let user_extracted = xorriso.Iso.ExtractPathBestEffort( - iso_path: final_path, - iso_internal_path: user_internal, - destination: user_probe, - ) - let sidecar_matches = install_media_remaster_sidecar_matches( - final_path: final_path, - expected_hash: artifact.content_hash as NonEmptyStr, - ) - let grub_present = shell.Test.IsFile(path: grub_probe as FilePath) - let grub_has_cmdline = grub_present.is_file - && grep.Grep.MatchesFixedString(pattern: artifact.grub_kernel_cmdline, path: grub_probe).matches - let user_nonempty = shell.Test.IsNonEmpty(path: user_probe as FilePath).nonempty - let cleaned = shell.Remove.RecursiveForce(path: probe.path) - if sidecar_matches && grub_has_cmdline && user_nonempty { - InstallMediaRemasterProbeValid - } else { - InstallMediaRemasterProbeStale - } - } -} +// WHERE THE LIVE-SERVER MEDIUM KEEPS ITS BOOT CONFIGURATION. This is a fact about the Ubuntu medium, +// not about xorriso, so it is authored here and passed to the replay as an ordinary map destination; +// the extracted tree's copy is the same path under the work dir. +data install_media_remaster_grub_cfg_on_iso: NonEmptyStr = "/boot/grub/grub.cfg" -fn observe_install_media_remaster(artifact: UbuntuSeededInstallMediaArtifactRow) -> InstallMediaRemasterObservation { - let final_path = ubuntu_seeded_install_media_install_path(artifact: artifact) - let present = shell.Test.IsFile(path: final_path as FilePath) - if present.is_file == false { - InstallMediaRemasterAbsent - } else { - match install_media_remaster_probe_verdict(artifact: artifact, final_path: final_path) { - InstallMediaRemasterProbeValid => InstallMediaRemasterValid - InstallMediaRemasterProbeStale => InstallMediaRemasterStale - InstallMediaRemasterProbeInfraFailure { reason: r } => InstallMediaRemasterVerifyInfraFailure { reason: r } - } - } +fn install_media_remaster_grub_cfg_path(work_dir: NonEmptyStr) -> NonEmptyStr { + concat(work_dir as String, install_media_remaster_grub_cfg_on_iso as String) as NonEmptyStr } // grep exits non-zero when the pattern is ABSENT, and absence is precisely the question this fn @@ -174,151 +95,138 @@ fn install_media_remaster_instance_seed_name(nocloud_dir_on_iso: NonEmptyStr) -> ) } -fn install_media_remaster_write_nocloud_seed( - work_dir: NonEmptyStr, - nocloud_dir_on_iso: NonEmptyStr, - user_data_body: NonEmptyStr, -) -> Bool { - let nocloud_dir = concat(work_dir as String, concat("/", nocloud_dir_on_iso as String)) as NonEmptyStr +fn install_media_remaster_write_seed_file(nocloud_dir: NonEmptyStr, file: NoCloudSeedFile) -> Bool { + Filesystem.Write( + path: concat(nocloud_dir as String, "/", file.name as String), + content: file.content as String, + ).success +} + +fn install_media_remaster_write_nocloud_seed(work_dir: NonEmptyStr, input: UbuntuSeededInstallMediaBuildInput) -> Bool { + let nocloud_dir = concat(work_dir as String, "/", input.nocloud_dir_on_iso as String) as NonEmptyStr let mkdir_nocloud = shell.Mkdir.Parents(path: nocloud_dir as FilePath) if mkdir_nocloud.success == false { false } else { let wrote_user_data = Filesystem.Write( path: concat(nocloud_dir as String, "/user-data"), - content: user_data_body as String, + content: input.user_data_body as String, ) let wrote_meta_data = Filesystem.Write( path: concat(nocloud_dir as String, "/meta-data"), content: concat( "instance-id: gunbc-", - concat(install_media_remaster_instance_seed_name(nocloud_dir_on_iso: nocloud_dir_on_iso), "-seeded\n"), + install_media_remaster_instance_seed_name(nocloud_dir_on_iso: input.nocloud_dir_on_iso), + "-seeded\n", ), ) - wrote_user_data.success && wrote_meta_data.success + let wrote_files = fold( + input.seed_files, + init: true, + f: (acc, file) => acc && install_media_remaster_write_seed_file(nocloud_dir: nocloud_dir, file: file), + ) + wrote_user_data.success && wrote_meta_data.success && wrote_files + } +} + +fn install_media_remaster_measure_staged(staging_path: NonEmptyStr) -> SeededInstallMediaStageOutcome { + match sha256sum_file_digest_via_shell(path: staging_path) { + Sha256FileDigestUnavailable { path: _, reason: r } => + SeededInstallMediaStageRefused { reason: concat("the staged image's digest could not be read: ", r) as NonEmptyStr } + Sha256FileDigest { digest: d } => + SeededInstallMediaStaged { staging_path: staging_path, output_digest: d.hex } } } -fn install_media_remaster_build_iso( +fn install_media_remaster_replay( + input: UbuntuSeededInstallMediaBuildInput, stock_path: NonEmptyStr, - final_path: NonEmptyStr, - part_path: NonEmptyStr, - content_hash: NonEmptyStr, + staging_path: NonEmptyStr, work_dir: NonEmptyStr, - nocloud_dir_on_iso: NonEmptyStr, -) -> InstallMediaRemasterOutcome { - let built = xorriso.Iso.ReplayBuildWithTwoMaps( +) -> SeededInstallMediaStageOutcome { + let built = xorriso.Iso.ReplayBuildPinnedDates( stock_iso_path: stock_path, - output_path: part_path, - volume_id: "UBUNTU_SEEDED" as NonEmptyStr, - map_one_source: concat(work_dir as String, concat("/", nocloud_dir_on_iso as String)) as NonEmptyStr, - map_one_iso_path: concat("/", nocloud_dir_on_iso as String) as NonEmptyStr, - map_two_source: concat(work_dir as String, "/boot/grub/grub.cfg") as NonEmptyStr, - map_two_iso_path: "/boot/grub/grub.cfg" as NonEmptyStr, + output_path: staging_path, + volume_id: input.volume_id, + volume_uuid: input.pinned_dates.volume_uuid, + file_date: input.pinned_dates.file_date, + map_one_source: concat(work_dir as String, "/", input.nocloud_dir_on_iso as String) as NonEmptyStr, + map_one_iso_path: concat("/", input.nocloud_dir_on_iso as String) as NonEmptyStr, + map_two_source: install_media_remaster_grub_cfg_path(work_dir: work_dir), + map_two_iso_path: install_media_remaster_grub_cfg_on_iso, ) if built.success == false { - InstallMediaRemasterRefused { reason: "xorriso -boot_image any replay build failed" as NonEmptyStr } + SeededInstallMediaStageRefused { reason: "xorriso -boot_image any replay build failed" as NonEmptyStr } } else { - let moved = shell.Move.File(source: part_path as FilePath, destination: final_path as FilePath) - if moved.success == false { - InstallMediaRemasterRefused { reason: "mv failed after replay build" as NonEmptyStr } - } else { - let wrote_sidecar = Filesystem.Write( - path: install_media_remaster_sidecar_path(final_path: final_path) as String, - content: content_hash as String, - ) - if wrote_sidecar.success == false { - InstallMediaRemasterRefused { reason: "writing content-hash sidecar failed" as NonEmptyStr } - } else { - InstallMediaRemasterRemastered { path: final_path } - } - } + install_media_remaster_measure_staged(staging_path: staging_path) } } -fn install_media_remaster_build( - artifact: UbuntuSeededInstallMediaArtifactRow, +fn install_media_remaster_from_tree( + input: UbuntuSeededInstallMediaBuildInput, stock_path: NonEmptyStr, - final_path: NonEmptyStr, - part_path: NonEmptyStr, + staging_path: NonEmptyStr, work_dir: NonEmptyStr, -) -> InstallMediaRemasterOutcome { +) -> SeededInstallMediaStageOutcome { let extracted = xorriso.Iso.ExtractTree(iso_path: stock_path, destination: work_dir) if extracted.success == false { - InstallMediaRemasterRefused { reason: "xorriso -osirrox extract failed" as NonEmptyStr } + SeededInstallMediaStageRefused { reason: "xorriso -osirrox extract failed" as NonEmptyStr } + } else if shell.Chmod.RecursiveWritable(path: work_dir as FilePath).success == false { + SeededInstallMediaStageRefused { reason: "chmod -R u+w failed" as NonEmptyStr } + } else if install_media_remaster_write_nocloud_seed(work_dir: work_dir, input: input) == false { + SeededInstallMediaStageRefused { reason: "writing NoCloud seed files failed" as NonEmptyStr } } else { - let chmod = shell.Chmod.RecursiveWritable(path: work_dir as FilePath) - if chmod.success == false { - InstallMediaRemasterRefused { reason: "chmod -R u+w failed" as NonEmptyStr } - } else if install_media_remaster_write_nocloud_seed( - work_dir: work_dir, - nocloud_dir_on_iso: artifact.nocloud_dir_on_iso, - user_data_body: artifact.autoinstall_user_data_body, - ) == false { - InstallMediaRemasterRefused { reason: "writing NoCloud seed files failed" as NonEmptyStr } + let grub_cfg = install_media_remaster_grub_cfg_path(work_dir: work_dir) + if shell.Test.IsFile(path: grub_cfg as FilePath).is_file == false { + SeededInstallMediaStageRefused { reason: concat("missing grub.cfg at ", grub_cfg as String) as NonEmptyStr } + } else if install_media_remaster_ensure_grub_cmdline(grub_cfg: grub_cfg, cmdline: input.grub_kernel_cmdline) == false { + SeededInstallMediaStageRefused { reason: "grub kernel cmdline not applied" as NonEmptyStr } } else { - let grub_cfg = concat(work_dir as String, "/boot/grub/grub.cfg") as NonEmptyStr - let grub_present = shell.Test.IsFile(path: grub_cfg as FilePath) - if grub_present.is_file == false { - InstallMediaRemasterRefused { - reason: concat("InstallMediaRemasterRefusal: missing grub.cfg at ", grub_cfg as String) as NonEmptyStr - } - } else if install_media_remaster_ensure_grub_cmdline(grub_cfg: grub_cfg, cmdline: artifact.grub_kernel_cmdline) == false { - InstallMediaRemasterRefused { reason: "InstallMediaRemasterRefusal: grub kernel cmdline not applied" as NonEmptyStr } - } else { - install_media_remaster_build_iso( - stock_path: stock_path, - final_path: final_path, - part_path: part_path, - content_hash: artifact.content_hash as NonEmptyStr, - work_dir: work_dir, - nocloud_dir_on_iso: artifact.nocloud_dir_on_iso, - ) - } + install_media_remaster_replay(input: input, stock_path: stock_path, staging_path: staging_path, work_dir: work_dir) } } } -fn install_media_remaster_apply(artifact: UbuntuSeededInstallMediaArtifactRow) -> InstallMediaRemasterOutcome { - let stock_path = ubuntu_install_media_install_path(artifact: artifact.stock_artifact) - let final_path = ubuntu_seeded_install_media_install_path(artifact: artifact) - let part_path = ubuntu_seeded_install_media_part_path(install_path: final_path) - let stock_present = shell.Test.IsFile(path: stock_path as FilePath) - if stock_present.is_file == false { - InstallMediaRemasterRefused { - reason: concat("InstallMediaRemasterRefusal: stock ISO missing at ", stock_path as String) as NonEmptyStr - } +fn install_media_remaster_in_scratch( + input: UbuntuSeededInstallMediaBuildInput, + stock_path: NonEmptyStr, + staging_path: NonEmptyStr, +) -> SeededInstallMediaStageOutcome { + let work = shell.Mktemp.Dir() + if work.success == false { + SeededInstallMediaStageRefused { reason: "mktemp -d failed" as NonEmptyStr } } else { - let mkdir = shell.Mkdir.Parents(path: ubuntu_install_media_artifacts_dir() as FilePath) - if mkdir.success == false { - InstallMediaRemasterRefused { reason: "mkdir -p failed" as NonEmptyStr } - } else { - let work = shell.Mktemp.Dir() - if work.success == false { - InstallMediaRemasterRefused { reason: "mktemp -d failed" as NonEmptyStr } - } else { - let outcome = install_media_remaster_build( - artifact: artifact, - stock_path: stock_path, - final_path: final_path, - part_path: part_path, - work_dir: work.path as NonEmptyStr, - ) - let cleaned = shell.Remove.RecursiveForce(path: work.path) - outcome - } - } + let outcome = install_media_remaster_from_tree( + input: input, + stock_path: stock_path, + staging_path: staging_path, + work_dir: work.path as NonEmptyStr, + ) + let cleaned = shell.Remove.RecursiveForce(path: work.path) + outcome } } -fn install_media_remaster_reconcile(artifact: UbuntuSeededInstallMediaArtifactRow) -> InstallMediaRemasterOutcome { - let final_path = ubuntu_seeded_install_media_install_path(artifact: artifact) - match observe_install_media_remaster(artifact: artifact) { - InstallMediaRemasterValid => InstallMediaRemasterAlreadyPresent { path: final_path } - InstallMediaRemasterAbsent => install_media_remaster_apply(artifact: artifact) - InstallMediaRemasterStale => install_media_remaster_apply(artifact: artifact) - InstallMediaRemasterVerifyInfraFailure { reason: r } => InstallMediaRemasterRefused { - reason: concat("VerifyInfraFailureRefusal: ", r as String) as NonEmptyStr +// THE UPSTREAM BYTES ARE VERIFIED BEFORE THEY ARE BUILT FROM. The derivation key names the stock +// image by its pinned sha256; a stock file at the expected path whose bytes differ would otherwise +// produce an image whose recorded derivation is false. A mismatch and an unverifiable stock are +// separate refusals -- the second says nothing about the file. +fn seeded_install_media_stage( + input: UbuntuSeededInstallMediaBuildInput, + stock_path: NonEmptyStr, + staging_path: NonEmptyStr, +) -> SeededInstallMediaStageOutcome { + if shell.Test.IsFile(path: stock_path as FilePath).is_file == false { + SeededInstallMediaStageRefused { reason: concat("stock ISO missing at ", stock_path as String) as NonEmptyStr } + } else { + match sha256sum_verify_via_shell(path: stock_path, expected_digest: input.stock_artifact.content_sha256 as NonEmptyStr) { + Sha256VerifyInfraFailure { reason: r } => + SeededInstallMediaStageRefused { reason: concat("stock ISO digest could not be verified: ", r as String) as NonEmptyStr } + Sha256Mismatch => + SeededInstallMediaStageRefused { + reason: concat("stock ISO at ", stock_path as String, " does not hash to the pinned ", input.stock_artifact.content_sha256 as String) as NonEmptyStr + } + Sha256Matches => install_media_remaster_in_scratch(input: input, stock_path: stock_path, staging_path: staging_path) } } } diff --git a/dag/extdeps/tools/xorriso.dag b/dag/extdeps/tools/xorriso.dag index 5d59f89b347..09fa8bbc983 100644 --- a/dag/extdeps/tools/xorriso.dag +++ b/dag/extdeps/tools/xorriso.dag @@ -62,11 +62,13 @@ service xorriso.Iso { } } - operation ReplayBuildWithTwoMaps { + operation ReplayBuildPinnedDates { input { stock_iso_path: NonEmptyStr, output_path: NonEmptyStr, volume_id: NonEmptyStr, + volume_uuid: NonEmptyStr, + file_date: NonEmptyStr, map_one_source: NonEmptyStr, map_one_iso_path: NonEmptyStr, map_two_source: NonEmptyStr, @@ -74,10 +76,10 @@ service xorriso.Iso { } output { success: Bool from "exit_success" } transport shell { - argv: ["xorriso", "-indev", "{stock_iso_path}", "-outdev", "{output_path}", "-boot_image", "any", "replay", "-volid", "{volume_id}", "-map", "{map_one_source}", "{map_one_iso_path}", "-map", "{map_two_source}", "{map_two_iso_path}", "-commit"] + argv: ["xorriso", "-indev", "{stock_iso_path}", "-outdev", "{output_path}", "-boot_image", "any", "replay", "-volid", "{volume_id}", "-map", "{map_one_source}", "{map_one_iso_path}", "-map", "{map_two_source}", "{map_two_iso_path}", "-volume_date", "uuid", "{volume_uuid}", "-volume_date", "all_file_dates", "{file_date}", "-commit"] } exit { - 0 => Unit + 0 => Unit "ORDER IS LOAD-BEARING: xorriso executes left to right and -volume_date all_file_dates applies to the files then in the image, so both date commands follow the -map commands -- issued before them, the freshly mapped files would keep the build instant. -volume_date uuid fixes the volume dates and the medium's UUID" nonzero => String "xorriso -boot_image any replay build failed" } } diff --git a/dag/gunbc/ci/ci_layer_roots.dag b/dag/gunbc/ci/ci_layer_roots.dag index 90559b076d3..82c0ade5378 100644 --- a/dag/gunbc/ci/ci_layer_roots.dag +++ b/dag/gunbc/ci/ci_layer_roots.dag @@ -867,6 +867,11 @@ data witness_exclusion_frontier: List = [ classification: BinWitnessWet, reason: excl_run_verdict_exit_status_reason, dissolution: excl_run_verdict_exit_status_dissolve}, + WitnessExclusionRow { + pattern: "mtcollins1_census_image_local_wet_test.dag", + classification: LocalRepoWetLane, + reason: "SUBSTANTIATED per-row: one fn reads a committed artifact through Filesystem.Read, one reaches shell.Exec.RunArgv through gunbc.command_runner.run_shell_command_observe, and one drives gunbc.seeded_install_media_publish resolve_seeded_image against a shell.Mktemp.Dir directory; none carries a mock_response, so hermetic discovery REFUSES rather than skipping. Their subjects are a real file's bytes and a real process's output: the attempt=3 host capture parsed under the reconstructed envelope grammar, and the capture program the renderer writes executed under `sh -c` and parsed back. A mocked read or a mocked stdout would re-check the parser against authored data, which is the supplied-input half test.claim.mtcollins1_census_image already covers; this file is the inhabitance half DESIGN section 3 requires beside it. THE EXECUTION IS PURELY LOCAL -- one file read inside the checkout and `sh -c` over echo/printf with a halt of `true`, no network, no remote host, no Cargo -- which is why it belongs on the local-repo wet lane." as String, + dissolution: unbound_dissolution(description: "Filesystem.Read and shell.Exec.RunArgv gain input-varying mock_response coverage over a checkout file and a process's stdout -- at which point these fns re-enrol as ordinary hermetic discovery rows and this row deletes. A fixed-output mock does NOT discharge it: the parser would be re-checked against the fixture rather than against what the renderer's program prints")}, WitnessExclusionRow { pattern: "spark_snapshot_verify_wrapper_local_wet_test.dag", classification: LocalRepoWetLane, diff --git a/dag/gunbc/host/host_effect_realize.dag b/dag/gunbc/host/host_effect_realize.dag index 5a5883c9deb..0fe2d0677f6 100644 --- a/dag/gunbc/host/host_effect_realize.dag +++ b/dag/gunbc/host/host_effect_realize.dag @@ -346,18 +346,17 @@ import gunbc.srv3_boot_once_cd { import extdeps.filesystem.filesystem_io { Filesystem } import std.measure { second_count, ByteSize, byte_size_count } import gunbc.srv3_ubuntu_install_media_artifact { srv3_ubuntu_install_media_artifact } -import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_install_media_artifact } +import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_install_media_publish } import extdeps.provisioning.ubuntu_install_media_fetch { install_media_fetch_reconcile, } import gunbc.artifact_acquisition { AcquisitionAlreadyPresent, AcquisitionFetched, AcquisitionRefused, acquisition_refusal_text, } -import extdeps.provisioning.ubuntu_seeded_install_media_remaster { - install_media_remaster_reconcile, - InstallMediaRemasterAlreadyPresent, - InstallMediaRemasterRemastered, - InstallMediaRemasterRefused, +import gunbc.seeded_install_media_publish { + SeededImagePublished, + SeededImageAlreadyPublished, + SeededImagePublicationRefused, } import gunbc.hostname_read { hostname_short_read, @@ -3355,31 +3354,31 @@ fn realize_install_media_fetch_in_process(intent: HostEffectIntent) -> Reconcili } fn realize_install_media_remaster_in_process(intent: HostEffectIntent) -> Reconciliation { - match install_media_remaster_reconcile(artifact: srv3_seeded_install_media_artifact) { - InstallMediaRemasterAlreadyPresent { path: p } => + match srv3_seeded_install_media_publish() { + SeededImageAlreadyPublished { built: b, path: p } => reconcile_grounded( apply_effect: intent, show_read: ReadObserved { value: p as String }, grounding: Grounds { evidence: ConvergePlanApplied { exit_code: 0, - stdout: concat("install-media-remaster: already present at ", p as String), + stdout: concat("install-media-remaster: already published at ", p as String, " sha256 ", b.output_digest as String), } } ) - InstallMediaRemasterRemastered { path: p } => + SeededImagePublished { built: b, path: p } => reconcile_grounded( apply_effect: intent, show_read: ReadObserved { value: p as String }, grounding: Grounds { evidence: ConvergePlanApplied { exit_code: 0, - stdout: concat("install-media-remaster: remastered at ", p as String), + stdout: concat("install-media-remaster: built and published at ", p as String, " sha256 ", b.output_digest as String), } } ) - InstallMediaRemasterRefused { reason: why } => - NotConverged { reason: concat("install-media-remaster: ", why as String), applied: intent } + SeededImagePublicationRefused { reason: why } => + NotConverged { reason: concat("install-media-remaster: ", why), applied: intent } } } diff --git a/dag/gunbc/machine_intake/boot_image_fetch.dag b/dag/gunbc/machine_intake/boot_image_fetch.dag index 5e5ce54cdba..4bc5aa6b0f9 100644 --- a/dag/gunbc/machine_intake/boot_image_fetch.dag +++ b/dag/gunbc/machine_intake/boot_image_fetch.dag @@ -41,6 +41,7 @@ type BootImageFetchOutcome | BootImagePublishFailed { staged: NonEmptyStr, destination: NonEmptyStr } | BootImagePublishedDigestMismatch { path: NonEmptyStr, expected: NonEmptyStr, observed: NonEmptyStr } | BootImagePublicationNotAdmitted { export_dir: NonEmptyStr, reason: String } + | BootImagePresentWithDifferentDigest { path: NonEmptyStr, expected: NonEmptyStr, observed: NonEmptyStr } // WHETHER A MEASURED DIGEST AGREES WITH THE PIN. Separated from the effect sequence so the judgment // is nameable, and returning the OBSERVED value on disagreement rather than a Bool: a refusal that @@ -63,7 +64,13 @@ fn boot_image_observed_hex(observed: Sha256FileDigest) -> NonEmptyStr { } } -// THE SEQUENCE, AND ITS ORDER IS THE GUARANTEE. Fetch to a staging path, measure there, and move +// The one composition of an export directory and an image name. Every route that names an image in +// an export -- the stock fetch, a derived image built from it -- composes through here. +fn boot_image_published_path(export_dir: NonEmptyStr, image_name: NonEmptyStr) -> NonEmptyStr { + join([export_dir as String, "/", image_name as String], "") as NonEmptyStr +} + +// THE SEQUENCE, AND ITS ORDER IS THE GUARANTEE. Fetch to a staging path, measure there, and link // into the published path ONLY on agreement. Fetching straight into the export and checking // afterwards would leave an interval in which a truncated or substituted image is mountable, and the // controller re-scans that share on its own schedule rather than this program's, so the interval is @@ -86,7 +93,8 @@ fn boot_image_observed_hex(observed: Sha256FileDigest) -> NonEmptyStr { // path constant with no diagnostic anywhere. // // DERIVING THE STAGING PATH FROM THE DESTINATION MAKES THE TWO PATHS UNABLE TO DIVERGE. They share a -// directory by construction, so they share a filesystem, so the rename is atomic — and there is no +// directory by construction, so they share a filesystem, so the hard link that publishes is atomic, and +// a staging path on another filesystem would fail the link (EXDEV) rather than copy — and there is no // longer an input whose editing could break it. This is the §5 move: prefer the single authority the // realization is derived from over a check that the two agree. // @@ -111,7 +119,7 @@ fn fetch_verified_boot_image( ) -> BootImageFetchOutcome uses net: std.resources.Network { - let published_path = join([export_dir as String, "/", image_name as String], "") as NonEmptyStr + let published_path = boot_image_published_path(export_dir: export_dir, image_name: image_name) let staging_path = boot_image_staging_path(published_path: published_path) let fetched = run_shell_command( command: curl_download_command(url: url as String, destination: staging_path as String), @@ -181,7 +189,6 @@ fn admit_then_publish_boot_image( export_dir: NonEmptyStr, digest: NonEmptyStr, ) -> BootImageFetchOutcome - uses net: std.resources.Network { let standing = observed_export_ownership_standing(path: export_dir) return if export_ownership_admits_publication(standing: standing) { @@ -215,19 +222,57 @@ fn publish_verified_boot_image( published_path: NonEmptyStr, digest: NonEmptyStr, ) -> BootImageFetchOutcome - uses net: std.resources.Network { - moved = shell.Move.File( - source: staging_path as FilePath, - destination: published_path as FilePath, - ) - return if moved.success { + return if shell.Test.IsFile(path: published_path as FilePath).is_file { + reconcile_present_boot_image(staging_path: staging_path, published_path: published_path, digest: digest) + } else { + link_verified_boot_image(staging_path: staging_path, published_path: published_path, digest: digest) + } +} + +// CREATE-ONLY. A hard link fails when the destination exists, so publication can add a name and can +// never replace the bytes behind one: an image already served under a name keeps those bytes for as +// long as the name exists. The staging name is then removed; the published name holds the inode. +fn link_verified_boot_image( + staging_path: NonEmptyStr, + published_path: NonEmptyStr, + digest: NonEmptyStr, +) -> BootImageFetchOutcome { + let linked = shell.Link.Hard(source: staging_path as FilePath, destination: published_path as FilePath) + return if linked.success { + let unstaged = shell.Remove.FileForce(path: staging_path as FilePath) confirm_published_boot_image(published_path: published_path, digest: digest) } else { BootImagePublishFailed { staged: staging_path, destination: published_path } } } +// THE NAME IS ALREADY TAKEN. If it holds the verified bytes this is the idempotent re-run and it is +// confirmed by the same read-back as a fresh publish; if it holds anything else the publish REFUSES +// and nothing is replaced -- the staged file is left beside it as the evidence of what was verified. +fn reconcile_present_boot_image( + staging_path: NonEmptyStr, + published_path: NonEmptyStr, + digest: NonEmptyStr, +) -> BootImageFetchOutcome { + let present = sha256sum_file_digest_via_shell(path: published_path) + return match present { + Sha256FileDigestUnavailable { path: p, reason: r } => + BootImageDigestUnreadable { path: p, reason: r } + Sha256FileDigest { digest: _ } => + if boot_image_digest_agrees(observed: present, expected: digest) { + let unstaged = shell.Remove.FileForce(path: staging_path as FilePath) + BootImageAdmitted { path: published_path, digest: digest } + } else { + BootImagePresentWithDifferentDigest { + path: published_path, + expected: digest, + observed: boot_image_observed_hex(observed: present), + } + } + } +} + // A SEPARATE FUNC so the admission is reachable only through a successful move, in the same shape // the rest of this lane uses to keep a consequential step behind its precondition. // @@ -248,7 +293,6 @@ fn confirm_published_boot_image( published_path: NonEmptyStr, digest: NonEmptyStr, ) -> BootImageFetchOutcome - uses net: std.resources.Network { let republished = sha256sum_file_digest_via_shell(path: published_path) return match republished { diff --git a/dag/gunbc/machine_intake/host_capture_envelope.dag b/dag/gunbc/machine_intake/host_capture_envelope.dag new file mode 100644 index 00000000000..ee972025dee --- /dev/null +++ b/dag/gunbc/machine_intake/host_capture_envelope.dag @@ -0,0 +1,563 @@ +module gunbc.machine_intake_host_capture_envelope + +import std.types { Bool, Int, List, NonEmptyStr, String } +import std.nat { Nat } +import std.measure { + Gibibyte, + Mebibyte, + gibibyte, + gibibyte_to_byte_size, + measure_fit_count_floor, + mebibyte, + mebibyte_count, + mebibyte_to_byte_size, +} +import v2.std.optional { Present } +import std.dissolution { DissolutionCondition, unbound_dissolution } +import extdeps.posix.shell_command_language { posix_single_quote } + +// ONE GRAMMAR, READ IN BOTH DIRECTIONS (DESIGN §4). The census program that runs on the host EMITS +// this envelope and the boot run's verdict PARSES it. Before this module the two sides were spelled +// independently -- the parser as literals in gunbc.machine_intake_mtcollins1_boot_run, the emitter as +// a program that was never committed at all -- so a renamed section or a changed separator would +// have left a capture the verdict could not read, reported as a hardware refusal. Every token below +// is consumed by a renderer AND by a parser; neither side authors a spelling of its own. +// +// THE SHAPE IS A RECONSTRUCTION, and it says what from. The only surviving evidence of the earlier +// census is its output, artifacts/bmc/mtcollins1-host-capture.txt (attempt=3, committed in #10965); +// the seed and program that produced it are in no commit. The framing, section names and argv here +// are read off that file's ENVELOPE, ----SECTION, ----ARGV and ----EXIT lines, so a capture from the +// new image parses the same way that one does. + +data host_capture_envelope_tag: NonEmptyStr = "GUNBC-HOST-CAPTURE" + +data host_capture_envelope_rule: String = "=====" + +data host_capture_section_open: String = "----SECTION " + +data host_capture_argv_open: String = "----ARGV " + +data host_capture_exit_open: String = "----EXIT " + +data host_capture_line_close: String = "----" + +// The edac stages print one ` == ` line per counter file, which is what lets the +// verdict read mc0's ce_count and ue_count out of a section without trusting a whole-tree summary. +data host_capture_counter_separator: String = " == " + +data host_capture_edac_root: String = "/sys/devices/system/edac" + +data host_capture_edac_mc0: String = "/sys/devices/system/edac/mc/mc0/" + +data host_capture_workload_write_rc_token: String = "workload-write-rc=" + +data host_capture_workload_stable_token: String = "workload-digest-stable=yes" + +// WHICH RUN THIS ENVELOPE IS, named for what the medium can actually know. attempt=3's envelope +// carried an ATTEMPT ORDINAL, which a host counting its own boots cannot produce: the census image is +// static media with no state across boots, so the only run identity available to it is the kernel's +// boot id. Reusing `attempt=` for that would be DESIGN §3's meaning fork -- one spelling, two +// materially different meanings, with nothing in the parser to discriminate -- so the field is named +// for the value it carries. The boot-id stage prints the same value inside the envelope; here it +// identifies the run even when every section fails. +data host_capture_run_token: String = "boot-id=" + +// attempt=3's spelling, carried so the historical form PARSES AS ITSELF rather than being skipped to +// make both captures read. It is never rendered: a static medium cannot count its own boots. +data host_capture_historical_attempt_token: String = "attempt=" + +type HostCaptureEnvelopeMarker = HostCaptureBegin | HostCaptureEnd + +fn host_capture_marker_word(marker: HostCaptureEnvelopeMarker) -> String { + match marker { + HostCaptureBegin => "begin" + HostCaptureEnd => "end" + } +} + +// A stage is one section of the envelope: the name the verdict looks it up by, and the command the +// program runs. The command is printed VERBATIM on the ARGV line, so what a reader sees is what ran. +type HostCaptureStage { + name: NonEmptyStr + command: NonEmptyStr +} + +fn host_capture_edac_counters_command() -> NonEmptyStr { + concat( + "find ", host_capture_edac_root, + " -type f -name '*count' | sort | while read -r f; do echo \"$f", + host_capture_counter_separator, + "$(cat \"$f\")\"; done", + ) as NonEmptyStr +} + +// THE WORKLOAD MUST NOT BE ABLE TO MINT A CLEAN RESULT. Comparing two digest lists is not enough: if +// both hash passes FAIL the same way -- both empty, or both missing the same unreadable chunk -- the +// two lists are equal and a naive body prints the stable token over a workload that hashed nothing. +// So the token requires, and the exit status carries, four independent facts: every write succeeded, +// BOTH passes exited zero, EACH pass covered exactly the expected four chunks, and the two manifests +// agree. The chunks are named explicitly rather than globbed, so a missing chunk is a sha256sum +// failure rather than a silently shorter list. +data host_capture_workload_chunk_dir: String = "/dev/shm/gunbc" + +// THE CHUNKS ARE NAMED, NOT GLOBBED, and the expected count is DERIVED from the same row: a glob +// silently shortens when a chunk is missing, and a separately authored count could disagree with the +// list it counts. ONLY THE STEMS ARE AUTHORED -- the directory is host_capture_workload_chunk_dir and +// nothing else, for the reason the tty row states one screen up. Authored twice, an edit to the dir +// row alone would leave mkdir and rm acting on one directory while dd and sha256sum act on another: +// the census would write its whole footprint somewhere it never cleans and STILL emit the stable +// token, because every fact the token requires would hold about the other directory. +data host_capture_workload_chunk_stems: String = "chunk0 chunk1 chunk2 chunk3" + +fn host_capture_workload_chunk_paths() -> String { + let stems = split(s: host_capture_workload_chunk_stems, delimiter: " ") + fold(stems, init: "", f: (acc, stem) => match acc == "" { + true => concat(host_capture_workload_chunk_dir, "/", stem) + false => concat(acc, " ", host_capture_workload_chunk_dir, "/", stem) + }) +} + +fn host_capture_workload_chunk_count() -> Int { + split(s: host_capture_workload_chunk_stems, delimiter: " ").length() +} + +// THE FOOTPRINT IS A MEASURE, not digits inside a shell string. dd's operands are rendered from +// these rows, so the size written per chunk and the block size are one fact each and the count +// operand is DERIVED by fitting one into the other -- a footprint edit cannot leave a stale +// count= behind, and the total this stage exercises is readable without parsing shell text. +data host_capture_workload_chunk_size: Gibibyte = gibibyte(16) + +data host_capture_workload_dd_block: Mebibyte = mebibyte(1) + +fn host_capture_workload_dd_count() -> Nat { + measure_fit_count_floor( + capacity: gibibyte_to_byte_size(g: host_capture_workload_chunk_size), + each: mebibyte_to_byte_size(m: host_capture_workload_dd_block), + ) +} + + +fn host_capture_workload_command() -> NonEmptyStr { + let dir = host_capture_workload_chunk_dir + let paths = host_capture_workload_chunk_paths() + let want = host_capture_workload_chunk_count() as String + let bs = concat(mebibyte_count(m: host_capture_workload_dd_block) as String, "M") + let count = host_capture_workload_dd_count() as String + concat( + "mkdir -p ", dir, " && rc=0; for f in ", paths, "; do ", + "dd if=/dev/urandom of=$f bs=", bs, " count=", count, " status=none || rc=1; done; ", + "echo ", host_capture_workload_write_rc_token, "$rc; ", + "a=$(sha256sum ", paths, "); sa=$?; b=$(sha256sum ", paths, "); sb=$?; ", + "na=$(printf '%s\\n' \"$a\" | grep -c .); nb=$(printf '%s\\n' \"$b\" | grep -c .); ", + "printf '%s\\n' \"$a\"; ", + "ok=0; [ \"$rc\" = 0 ] && [ \"$sa\" = 0 ] && [ \"$sb\" = 0 ] && [ \"$na\" = ", want, " ] && [ \"$nb\" = ", want, " ] && [ \"$a\" = \"$b\" ] && ok=1; ", + "[ \"$ok\" = 1 ] && echo ", host_capture_workload_stable_token, "; ", + "rm -rf ", dir, "; [ \"$ok\" = 1 ]", + ) as NonEmptyStr +} + +// The census roster, in run order: attempt=3's sections with attempt=3's argv, so the two captures +// answer the same questions about the same machine -- including the DIMM inventory (dmidecode types +// 16 and 17) and meminfo, which the memory intake this census serves reads. ONE SECTION IS +// DELIBERATELY NOT CARRIED: attempt=3's `dmesg-full`. The image boots with console=ttyAMA0, so the +// kernel ring buffer is already on the very serial stream the envelope is written to, and printing +// it again inside the envelope made it most of attempt=3's capture without adding a fact. Re-adding it is one row here if a verdict ever needs the ring buffer inside the envelope. +// The envelope's END marker follows the last stage and is the only thing that may precede the halt; +// it is not a section, because a section can fail and still be followed by more output, whereas END +// is the program's statement that it wrote everything. +data host_capture_census_stages: List = [ + HostCaptureStage { name: "subject-system", command: "dmidecode --type 1" }, + HostCaptureStage { name: "subject-baseboard", command: "dmidecode --type 2" }, + HostCaptureStage { name: "subject-chassis", command: "dmidecode --type 3" }, + HostCaptureStage { name: "boot-id", command: "cat /proc/sys/kernel/random/boot_id" }, + HostCaptureStage { name: "boot-cmdline", command: "cat /proc/cmdline" }, + HostCaptureStage { name: "kernel", command: "uname -a" }, + HostCaptureStage { name: "boot-media", command: "ls -l /dev/disk/by-label; blkid" }, + HostCaptureStage { name: "nproc", command: "nproc" }, + HostCaptureStage { name: "lscpu", command: "lscpu" }, + HostCaptureStage { name: "numa", command: "cat /sys/devices/system/node/online; numactl -H" }, + HostCaptureStage { name: "meminfo", command: "grep -E 'MemTotal|MemAvailable' /proc/meminfo" }, + HostCaptureStage { name: "dmidecode-type16", command: "dmidecode --type 16" }, + HostCaptureStage { name: "dmidecode-type17", command: "dmidecode --type 17" }, + HostCaptureStage { + name: "edac-mc-name", + command: "for d in /sys/devices/system/edac/mc/mc*; do echo \"$d/mc_name = $(cat $d/mc_name)\"; done", + }, + HostCaptureStage { name: "edac-find", command: "find /sys/devices/system/edac | sort" }, + HostCaptureStage { name: "edac-before", command: host_capture_edac_counters_command() }, + HostCaptureStage { name: "workload", command: host_capture_workload_command() }, + HostCaptureStage { name: "edac-after", command: host_capture_edac_counters_command() }, +] + +fn host_capture_marker_line(machine: NonEmptyStr, marker: HostCaptureEnvelopeMarker) -> String { + concat( + host_capture_envelope_rule, host_capture_envelope_tag as String, " ", machine as String, + " ", host_capture_run_token, "$GUNBC_BOOT_ID ", host_capture_marker_word(marker: marker), + " $(date -u +%Y-%m-%dT%H:%M:%SZ)", host_capture_envelope_rule, + ) +} + +// THE CAPTURE PROGRAM IS SHELL TEXT BUILT BY CONCATENATION, and that carrier half is declared rather +// than left implicit: what it runs and how it is framed are modeled (the roster and the envelope +// tokens above, read by the parser too), but the program that sequences them is a string. +data host_capture_program_shell_emit_dissolution_trigger: DissolutionCondition = unbound_dissolution(description: "dissolve-on: gunbc.machine_intake_host_capture_envelope host_capture_program / host_capture_stage_program / host_capture_edac_counters_command / host_capture_workload_command -- concat-built POSIX sh for the live-session census (exec to the console, per-stage section/argv/exit framing, sync, halt), quoting through extdeps.posix.shell_command_language posix_single_quote. DISSOLVES WHEN bash-emit (#5828 / ROADMAP 6-shell-slice0 / shell-to-intent Phase 2) renders a stage roster through orchestration emit without a medium-as-string scaffold, at which point these renderers become one emitted program derived from the same roster rows.") + +fn host_capture_stage_program(stage: HostCaptureStage) -> String { + let name = stage.name as String + concat( + "echo ", posix_single_quote(spelling: concat(host_capture_section_open, name, host_capture_line_close)), "\n", + "echo ", posix_single_quote(spelling: concat(host_capture_argv_open, name, ": ", stage.command as String)), "\n", + "sh -c ", posix_single_quote(spelling: stage.command as String), " 2>&1\n", + "echo \"", host_capture_exit_open, name, ": $?\"\n", + ) +} + +// The whole program. Output goes to the console device ONLY, so the envelope is what the BMC's SOL +// collector records; `sync` then the halt run only after END is echoed. The halt is a parameter +// because it is the one line whose effect is on the machine rather than the envelope: the census +// image halts with a forced power-off, and an execution witness renders the same program with a +// halt that returns. +fn host_capture_program( + machine: NonEmptyStr, + console_device: NonEmptyStr, + stages: List, + halt_command: NonEmptyStr, +) -> NonEmptyStr { + concat( + "#!/bin/sh\n", + "exec >", console_device as String, " 2>&1\n", + "GUNBC_BOOT_ID=$(cat /proc/sys/kernel/random/boot_id)\n", + "echo \"", host_capture_marker_line(machine: machine, marker: HostCaptureBegin), "\"\n", + join(map(stages, s => host_capture_stage_program(stage: s)), ""), + "echo \"", host_capture_marker_line(machine: machine, marker: HostCaptureEnd), "\"\n", + "sync\n", + halt_command as String, "\n", + ) as NonEmptyStr +} + +// --------------------------------------------------------------------------------------------- +// ONE BOUND ENVELOPE, NOT A SEARCH OVER THE STREAM. +// +// The console this is read from is a CONTINUOUS STREAM across boots: a BMC's SOL capture holds every +// run the unit has made. Searching it for "a begin line", "an end line" and "a section" independently +// lets a verdict assemble a pass out of three different runs -- begin from the attempt that hung, +// sections from the attempt that was interrupted, end from the attempt that halted -- and every one +// of those reads is individually true. So the stream is BOUND FIRST: one begin, the matching end for +// the same machine AND the same run, and only the text between them is a capture at all. +// +// THE RUN IDENTITY IS TYPED RATHER THAN IGNORED. attempt=3 carried an ordinal; the census image +// carries a kernel boot id, because static media cannot count its own boots. Those are different +// identity forms and the parser keeps them apart -- an unrecognized spelling REFUSES rather than +// being skipped to make both parse, which would restore exactly the cross-run join this exists to +// prevent. + +type HostCaptureRunIdentity + = HistoricalAttemptOrdinal { ordinal: NonEmptyStr } + | KernelBootId { boot_id: NonEmptyStr } + +fn host_capture_run_identity_value(run: HostCaptureRunIdentity) -> String { + match run { + HistoricalAttemptOrdinal { ordinal: o } => o as String + KernelBootId { boot_id: b } => b as String + } +} + +fn host_capture_run_identity_of(field: String) -> HostCaptureRunIdentity? { + if starts_with(s: field, prefix: host_capture_historical_attempt_token) { + let rest = substring(s: field, start: host_capture_historical_attempt_token.length(), end: field.length()) + if rest.length() == 0 { none } else { Present { value: HistoricalAttemptOrdinal { ordinal: rest as NonEmptyStr } } } + } else if starts_with(s: field, prefix: host_capture_run_token) { + let rest = substring(s: field, start: host_capture_run_token.length(), end: field.length()) + if rest.length() == 0 { none } else { Present { value: KernelBootId { boot_id: rest as NonEmptyStr } } } + } else { + none + } +} + +type HostCaptureMarker { + machine: NonEmptyStr + run: HostCaptureRunIdentity + kind: HostCaptureEnvelopeMarker +} + +// A marker line is `=====GUNBC-HOST-CAPTURE =====`. Every +// field is required: a line missing the run field is not a marker, because a marker with no run +// identity is precisely what would let two runs be joined. +fn host_capture_marker_of(line: String) -> HostCaptureMarker? { + if string_contains(s: line, pattern: concat(host_capture_envelope_rule, host_capture_envelope_tag as String, " ")) == false { + none + } else { + match get(xs: split(s: line, delimiter: concat(host_capture_envelope_tag as String, " ")), index: 1) { + Absent => none + Present { value: after_tag } => { + let fields = split(s: after_tag, delimiter: " ") + match get(xs: fields, index: 0) { + Absent => none + Present { value: machine } => + match get(xs: fields, index: 1) { + Absent => none + Present { value: run_field } => + match get(xs: fields, index: 2) { + Absent => none + Present { value: word } => + match host_capture_run_identity_of(field: run_field) { + Absent => none + Present { value: run } => + if machine.length() == 0 { + none + } else if word == host_capture_marker_word(marker: HostCaptureBegin) { + Present { value: HostCaptureMarker { machine: machine as NonEmptyStr, run: run, kind: HostCaptureBegin } } + } else if word == host_capture_marker_word(marker: HostCaptureEnd) { + Present { value: HostCaptureMarker { machine: machine as NonEmptyStr, run: run, kind: HostCaptureEnd } } + } else { + none + } + } + } + } + } + } + } + } +} + +type HostCaptureBoundSection { + name: NonEmptyStr + body: String +} + +type BoundHostCaptureEnvelope { + machine: NonEmptyStr + run: HostCaptureRunIdentity + sections: List +} + +type HostCaptureBinding + = HostCaptureBound { envelope: BoundHostCaptureEnvelope } + | HostCaptureUnbound { reason: String } + +// TWO RUN IDENTITIES ARE THE SAME RUN ONLY IF THEY ARE THE SAME KIND OF IDENTITY. Comparing the +// projected TEXT alone made attempt=3 and boot-id=3 the same run, so an envelope begun by the +// historical program and ended by a kernel boot could close as one -- the exact confusion the two +// forms were introduced to end. A historical attempt ordinal and a kernel boot id are drawn from +// different spaces and a shared spelling between them means nothing. +fn host_capture_run_identity_same(a: HostCaptureRunIdentity, b: HostCaptureRunIdentity) -> Bool { + match a { + HistoricalAttemptOrdinal { ordinal: x } => + match b { + HistoricalAttemptOrdinal { ordinal: y } => (x as String) == (y as String) + KernelBootId { boot_id: _ } => false + } + KernelBootId { boot_id: x } => + match b { + KernelBootId { boot_id: y } => (x as String) == (y as String) + HistoricalAttemptOrdinal { ordinal: _ } => false + } + } +} + +fn host_capture_marker_matches(a: HostCaptureMarker, machine: NonEmptyStr, run: HostCaptureRunIdentity) -> Bool { + (a.machine as String) == (machine as String) + && host_capture_run_identity_same(a: a.run, b: run) +} + +type HostCaptureScan { + begun: Bool + closed: Bool + machine: NonEmptyStr + run: HostCaptureRunIdentity + body: List +} + +// The first begin for the machine opens the envelope; the first end for the SAME machine and run +// closes it. Lines before the begin and after the end are other runs' and are not read. An end that +// precedes the begin is never reached, so a reversed pair leaves the envelope unclosed. +fn host_capture_scan_line(acc: HostCaptureScan, line: String, machine: NonEmptyStr) -> HostCaptureScan { + if acc.closed { + acc + } else { + match host_capture_marker_of(line: line) { + Absent => if acc.begun { HostCaptureScan { begun: true, closed: false, machine: acc.machine, run: acc.run, body: append(acc.body, items: [line]) } } else { acc } + Present { value: m } => + match m.kind { + HostCaptureBegin => + if acc.begun == false && (m.machine as String) == (machine as String) { + HostCaptureScan { begun: true, closed: false, machine: m.machine, run: m.run, body: [] } + } else { + acc + } + HostCaptureEnd => + if acc.begun && host_capture_marker_matches(a: m, machine: acc.machine, run: acc.run) { + HostCaptureScan { begun: true, closed: true, machine: acc.machine, run: acc.run, body: acc.body } + } else { + acc + } + } + } + } +} + +fn host_capture_section_name_of(line: String) -> NonEmptyStr? { + if starts_with(s: line, prefix: host_capture_section_open) == false { + none + } else { + let rest = substring(s: line, start: host_capture_section_open.length(), end: line.length()) + match get(xs: split(s: rest, delimiter: host_capture_line_close), index: 0) { + Absent => none + Present { value: name } => if name.length() == 0 { none } else { Present { value: name as NonEmptyStr } } + } + } +} + +type HostCaptureSectionScan { + sections: List + open_name: NonEmptyStr? + open_body: List + duplicate: Bool +} + +fn host_capture_section_seen(sections: List, name: NonEmptyStr) -> Bool { + fold(sections, init: false, f: (acc, s) => acc || (s.name as String) == (name as String)) +} + +fn host_capture_close_open_section(acc: HostCaptureSectionScan) -> List { + match acc.open_name { + Absent => acc.sections + Present { value: n } => append(acc.sections, items: [HostCaptureBoundSection { name: n, body: join(acc.open_body, "\n") }]) + } +} + +// A section runs from its opener to the next opener or to the envelope's end. A name that opens +// TWICE inside one envelope refuses the whole capture: with two candidates there is no fact about +// which one the verdict read, and picking either is the silent choice this refuses to make. +fn host_capture_section_scan_line(acc: HostCaptureSectionScan, line: String) -> HostCaptureSectionScan { + match host_capture_section_name_of(line: line) { + Absent => + match acc.open_name { + Absent => acc + Present { value: _ } => HostCaptureSectionScan { sections: acc.sections, open_name: acc.open_name, open_body: append(acc.open_body, items: [line]), duplicate: acc.duplicate } + } + Present { value: n } => { + let closed = host_capture_close_open_section(acc: acc) + HostCaptureSectionScan { + sections: closed, + open_name: Present { value: n }, + open_body: [], + duplicate: acc.duplicate || host_capture_section_seen(sections: closed, name: n), + } + } + } +} + +fn host_capture_bind(capture: String, machine: NonEmptyStr) -> HostCaptureBinding { + let scan = fold( + split(s: capture, delimiter: "\n"), + init: HostCaptureScan { begun: false, closed: false, machine: machine, run: KernelBootId { boot_id: "unbound" }, body: [] }, + f: (acc, line) => host_capture_scan_line(acc: acc, line: line, machine: machine), + ) + if scan.begun == false { + HostCaptureUnbound { reason: concat("no begin marker for ", machine as String) } + } else if scan.closed == false { + HostCaptureUnbound { + reason: concat( + "the envelope for ", machine as String, " run ", host_capture_run_identity_value(run: scan.run), + " was opened and never closed by an end marker for that same run", + ) + } + } else { + let sections = fold( + scan.body, + init: HostCaptureSectionScan { sections: [], open_name: none, open_body: [], duplicate: false }, + f: (acc, line) => host_capture_section_scan_line(acc: acc, line: line), + ) + if sections.duplicate { + HostCaptureUnbound { + reason: concat("a section name opens twice inside the envelope for ", machine as String, "; which one the verdict would read is not a fact") + } + } else { + HostCaptureBound { + envelope: BoundHostCaptureEnvelope { + machine: scan.machine, + run: scan.run, + sections: host_capture_close_open_section(acc: sections), + } + } + } + } +} + +fn host_capture_bound_section(envelope: BoundHostCaptureEnvelope, name: String) -> HostCaptureBoundSection? { + fold( + envelope.sections, + init: none, + f: (acc, s) => match acc { + Present { value: _ } => acc + Absent => if (s.name as String) == name { Present { value: s } } else { none } + }, + ) +} + +fn host_capture_exit_zero_line(name: String) -> String { + concat(host_capture_exit_open, name, ": 0") +} + +fn host_capture_bound_section_exit_zero(envelope: BoundHostCaptureEnvelope, name: String) -> Bool { + match host_capture_bound_section(envelope: envelope, name: name) { + Absent => false + Present { value: s } => string_contains(s: s.body, pattern: host_capture_exit_zero_line(name: name)) + } +} + +// The ARGV line a bound section carries, as the command text after its own `----ARGV : ` +// prefix. The historical binding reads this to check what attempt=3 ran against what the new roster +// says it ran. +fn host_capture_bound_section_argv(envelope: BoundHostCaptureEnvelope, name: String) -> String? { + match host_capture_bound_section(envelope: envelope, name: name) { + Absent => none + Present { value: s } => { + let opener = concat(host_capture_argv_open, name, ": ") + match get(xs: split(s: s.body, delimiter: opener), index: 1) { + Absent => none + Present { value: tail } => get(xs: split(s: tail, delimiter: "\n"), index: 0) + } + } + } +} + +// The count must be the WHOLE output line of the nproc section, immediately before that section's +// zero exit: a 160 printed anywhere else in the capture, or inside nproc beside other text, is not a +// reading of nproc. +fn host_capture_bound_nproc_exact(envelope: BoundHostCaptureEnvelope, threads: String) -> Bool { + match host_capture_bound_section(envelope: envelope, name: "nproc") { + Absent => false + Present { value: s } => + string_contains(s: s.body, pattern: concat("\n", threads, "\n", host_capture_exit_zero_line(name: "nproc"))) + } +} + +fn host_capture_mc0_counter(section: String, counter: String, value: String) -> Bool { + string_contains(s: section, pattern: concat(host_capture_edac_mc0, counter, host_capture_counter_separator, value, "\n")) +} + +fn host_capture_bound_edac_clean(envelope: BoundHostCaptureEnvelope, name: String) -> Bool { + match host_capture_bound_section(envelope: envelope, name: name) { + Absent => false + Present { value: s } => + string_contains(s: s.body, pattern: host_capture_exit_zero_line(name: name)) + && host_capture_mc0_counter(section: s.body, counter: "ce_count", value: "0") + && host_capture_mc0_counter(section: s.body, counter: "ue_count", value: "0") + } +} + +// Both tokens are matched as WHOLE OUTPUT LINES. The ARGV line prints the workload command verbatim +// and that command contains both tokens, so a substring match reads the program text as its own +// result and a failed workload passes. +fn host_capture_bound_workload_stable(envelope: BoundHostCaptureEnvelope) -> Bool { + match host_capture_bound_section(envelope: envelope, name: "workload") { + Absent => false + Present { value: s } => + string_contains(s: s.body, pattern: host_capture_exit_zero_line(name: "workload")) + && string_contains(s: s.body, pattern: concat("\n", host_capture_workload_write_rc_token, "0\n")) + && string_contains(s: s.body, pattern: concat("\n", host_capture_workload_stable_token, "\n")) + } +} diff --git a/dag/gunbc/machine_intake/host_capture_historical_binding.dag b/dag/gunbc/machine_intake/host_capture_historical_binding.dag new file mode 100644 index 00000000000..6aa09585c68 --- /dev/null +++ b/dag/gunbc/machine_intake/host_capture_historical_binding.dag @@ -0,0 +1,320 @@ +module gunbc.machine_intake_host_capture_historical_binding + +import std.types { Bool, List, NonEmptyStr, String } +import std.content_hash { ContentHash } +import std.measure { ByteSize } +import v2.std.optional { Present } +import gunbc.machine_intake_host_capture_envelope { + BoundHostCaptureEnvelope, + HostCaptureStage, + host_capture_bound_section_argv, + host_capture_census_stages, +} +import gunbc.machine_intake_mtcollins1_memory_census_observation { + mtcollins1_host_capture_artifact_path, + mtcollins1_host_capture_byte_count, + mtcollins1_host_capture_digest, +} + +// WHAT THE CENSUS PROGRAM ACTUALLY IS, stated at the strength the evidence supports. It is a NEWLY +// AUTHORED PRODUCER CONSTRAINED BY attempt=3's OUTPUT -- not attempt=3's executable recovered from +// its bytes, which no commit contains. The distinction is not pedantry: a reader who believes the +// second will trust the new program wherever the old one was trusted, including for facts the new +// program computes differently. The workload is the sharpest case -- its ARGV line in the capture is +// a PROSE DESCRIPTION ("4 x 16GiB tmpfs chunks, urandom-seeded, sha256 x2 passes, compare"), so the +// implementation here was written from that description and shares no line with whatever ran. +// +// SO THE RELATION IS CARRIED ROW BY ROW rather than asserted in prose. Every section the historical +// capture contains gets exactly one disposition against the new roster, bound to the capture's own +// committed identity; a mutation to a stage the binding calls EXACT makes the map fail rather than +// passing quietly. + +type HistoricalCaptureIdentity { + path: NonEmptyStr + digest: ContentHash + byte_count: ByteSize +} + +// The capture this producer is constrained by, by its committed identity rather than by its name: +// the three rows are gunbc.machine_intake_mtcollins1_memory_census_observation's, consumed here +// rather than re-minted, so a re-captured artifact moves both readings at once. +data census_historical_capture: HistoricalCaptureIdentity = HistoricalCaptureIdentity { + path: mtcollins1_host_capture_artifact_path, + digest: mtcollins1_host_capture_digest, + byte_count: mtcollins1_host_capture_byte_count, +} + +// HOW ONE HISTORICAL SECTION RELATES TO THE NEW ROSTER. +// Exact -- the new stage's command IS the historical ARGV, byte for byte. The row +// quotes that ARGV so the pair is checkable without the file, and the wet +// claim proves the quote is what the committed capture actually carries. +// Normalized -- deliberately changed, carrying what it was, what it became and why. +// DeliberatelyOmitted -- the section is not carried at all, with its reason. +// AuthoredFromDescription -- the capture's ARGV line for this section is PROSE, not a command +// (`read /sys/...`, "4 x 16GiB tmpfs chunks, ..."), so there is nothing to +// carry over: the new command is authored to produce the output the capture +// shows, and no literal comparison to a historical command is possible. +// NewlyIntroduced -- the new roster has a stage the capture never had. +type HistoricalStageDisposition + = HistoricalStageExact { argv: NonEmptyStr } + | HistoricalStageNormalized { original: NonEmptyStr, replacement: NonEmptyStr, reason: NonEmptyStr } + | HistoricalStageAuthoredFromDescription { original: NonEmptyStr, reason: NonEmptyStr } + | HistoricalStageDeliberatelyOmitted { reason: NonEmptyStr } + | HistoricalStageNewlyIntroduced { reason: NonEmptyStr } + +type HistoricalStageBinding { + name: NonEmptyStr + disposition: HistoricalStageDisposition +} + +data census_shell_wrapper_reason: NonEmptyStr = "attempt=3 ran this stage's pipeline through an explicit `sh -c` wrapper and suppressed tool stderr with 2>/dev/null; the new program already runs every stage under `sh -c` and captures stderr into the section (2>&1), so the wrapper is redundant and the suppression would hide the reason a stage failed" as NonEmptyStr + +// ONE ROW PER SECTION THE CAPTURE CONTAINS, in the capture's own order. The ARGV strings on the +// Normalized rows are attempt=3's, quoted from the committed file; the Exact rows assert the new +// stage command IS the historical one and are checked against the file by the wet claim rather than +// by eye. +data census_historical_stage_bindings: List = [ + HistoricalStageBinding { name: "subject-system", disposition: HistoricalStageExact { argv: "dmidecode --type 1" } }, + HistoricalStageBinding { name: "subject-baseboard", disposition: HistoricalStageExact { argv: "dmidecode --type 2" } }, + HistoricalStageBinding { name: "subject-chassis", disposition: HistoricalStageExact { argv: "dmidecode --type 3" } }, + HistoricalStageBinding { name: "boot-id", disposition: HistoricalStageExact { argv: "cat /proc/sys/kernel/random/boot_id" } }, + HistoricalStageBinding { name: "boot-cmdline", disposition: HistoricalStageExact { argv: "cat /proc/cmdline" } }, + HistoricalStageBinding { name: "kernel", disposition: HistoricalStageExact { argv: "uname -a" } }, + HistoricalStageBinding { + name: "boot-media", + disposition: HistoricalStageNormalized { + original: "sh -c ls -l /dev/disk/by-label 2>/dev/null; blkid 2>/dev/null | head -20", + replacement: "ls -l /dev/disk/by-label; blkid", + reason: census_shell_wrapper_reason, + }, + }, + HistoricalStageBinding { name: "nproc", disposition: HistoricalStageExact { argv: "nproc" } }, + HistoricalStageBinding { name: "lscpu", disposition: HistoricalStageExact { argv: "lscpu" } }, + HistoricalStageBinding { + name: "numa", + disposition: HistoricalStageNormalized { + original: "sh -c cat /sys/devices/system/node/online; numactl -H 2>/dev/null", + replacement: "cat /sys/devices/system/node/online; numactl -H", + reason: census_shell_wrapper_reason, + }, + }, + HistoricalStageBinding { + name: "meminfo", + disposition: HistoricalStageNormalized { + original: "sh -c grep -E \"MemTotal|MemAvailable\" /proc/meminfo", + replacement: "grep -E 'MemTotal|MemAvailable' /proc/meminfo", + reason: "the sh -c wrapper is redundant as above, and the pattern is single-quoted so the alternation reaches grep rather than the shell" as NonEmptyStr, + }, + }, + HistoricalStageBinding { name: "dmidecode-type16", disposition: HistoricalStageExact { argv: "dmidecode --type 16" } }, + HistoricalStageBinding { name: "dmidecode-type17", disposition: HistoricalStageExact { argv: "dmidecode --type 17" } }, + HistoricalStageBinding { + name: "edac-mc-name", + disposition: HistoricalStageNormalized { + original: "sh -c for d in /sys/devices/system/edac/mc/mc*; do echo \"$d/mc_name = $(cat $d/mc_name 2>/dev/null)\"; done", + replacement: "for d in /sys/devices/system/edac/mc/mc*; do echo \"$d/mc_name = $(cat $d/mc_name)\"; done", + reason: census_shell_wrapper_reason, + }, + }, + HistoricalStageBinding { + name: "edac-find", + disposition: HistoricalStageNormalized { + original: "sh -c find /sys/devices/system/edac 2>/dev/null | sort", + replacement: "find /sys/devices/system/edac | sort", + reason: census_shell_wrapper_reason, + }, + }, + HistoricalStageBinding { + name: "edac-before", + disposition: HistoricalStageAuthoredFromDescription { + original: "read /sys/devices/system/edac", + reason: "attempt=3's ARGV line is a DESCRIPTION rather than a command -- `read ` is not what ran -- so the counter walk is newly authored to emit the ` == ` lines the capture's own output shows, which is what the verdict reads" as NonEmptyStr, + }, + }, + HistoricalStageBinding { + name: "workload", + disposition: HistoricalStageAuthoredFromDescription { + original: "4 x 16GiB tmpfs chunks, urandom-seeded, sha256 x2 passes, compare", + reason: "attempt=3's ARGV line is PROSE, not a command, so no source exists to carry over: the body in gunbc.machine_intake_host_capture_envelope host_capture_workload_command is written from that description and from the capture's output tokens, and shares no line with whatever ran. It is the stage least constrained by the historical bytes, which is why its own execution controls carry the weight" as NonEmptyStr, + }, + }, + HistoricalStageBinding { + name: "edac-after", + disposition: HistoricalStageAuthoredFromDescription { + original: "read /sys/devices/system/edac", + reason: "same as edac-before: the historical ARGV is a description, and the walk is authored to emit the counter lines the verdict reads" as NonEmptyStr, + }, + }, + HistoricalStageBinding { + name: "dmesg-full", + disposition: HistoricalStageDeliberatelyOmitted { + reason: "the image boots with console=ttyAMA0, so the kernel ring buffer is already on the serial stream the envelope is written to; printing it again inside the envelope made it most of attempt=3's capture without adding a fact" as NonEmptyStr, + }, + }, +] + +fn historical_binding_for(name: String) -> HistoricalStageBinding? { + fold( + census_historical_stage_bindings, + init: none, + f: (acc, b) => match acc { + Present { value: _ } => acc + Absent => if (b.name as String) == name { Present { value: b } } else { none } + }, + ) +} + +// What the new roster must say about a binding, so a reader can check the pair without running the +// capture: an Exact or Normalized row is carried and its command must equal the replacement (for +// Exact, the replacement IS the historical ARGV, which the wet claim reads off the file); an omitted +// row must not be carried at all. +fn historical_binding_expected_command(b: HistoricalStageBinding) -> NonEmptyStr? { + match b.disposition { + HistoricalStageExact { argv: a } => Present { value: a } + HistoricalStageNormalized { original: _, replacement: r, reason: _ } => Present { value: r } + HistoricalStageAuthoredFromDescription { original: _, reason: _ } => none + HistoricalStageDeliberatelyOmitted { reason: _ } => none + HistoricalStageNewlyIntroduced { reason: _ } => none + } +} + +fn historical_binding_is_carried(b: HistoricalStageBinding) -> Bool { + match b.disposition { + HistoricalStageExact { argv: _ } => true + HistoricalStageNormalized { original: _, replacement: _, reason: _ } => true + HistoricalStageAuthoredFromDescription { original: _, reason: _ } => true + HistoricalStageDeliberatelyOmitted { reason: _ } => false + HistoricalStageNewlyIntroduced { reason: _ } => true + } +} + +fn stage_named(stages: List, name: String) -> HostCaptureStage? { + fold( + stages, + init: none, + f: (acc, s) => match acc { + Present { value: _ } => acc + Absent => if (s.name as String) == name { Present { value: s } } else { none } + }, + ) +} + +// EVERY CARRIED BINDING IS IN THE ROSTER AND EVERY OMITTED ONE IS NOT, and a Normalized row's +// replacement must be the roster's command verbatim. This is the half that does not need the file: +// it fails the moment a stage command is edited away from what the binding says it became. +fn roster_matches_bindings(stages: List) -> Bool { + fold( + census_historical_stage_bindings, + init: true, + f: (acc, b) => acc && match stage_named(stages: stages, name: b.name as String) { + Absent => historical_binding_is_carried(b: b) == false + Present { value: stage } => + historical_binding_is_carried(b: b) && match historical_binding_expected_command(b: b) { + Absent => true + Present { value: expected } => (stage.command as String) == (expected as String) + } + }, + ) +} + +// NO ROSTER STAGE IS UNACCOUNTED FOR. A stage added to the roster without a binding row is exactly +// the drift this map exists to refuse: it would ride into the census with no statement of whether +// the historical capture had it. +fn census_roster_matches_bindings() -> Bool { + roster_matches_bindings(stages: host_capture_census_stages) +} + +fn census_roster_is_fully_bound() -> Bool { + fold( + host_capture_census_stages, + init: true, + f: (acc, s) => acc && match historical_binding_for(name: s.name as String) { + Absent => false + Present { value: _ } => true + }, + ) +} + +// WHAT THE BOUND ENVELOPE MUST SAY FOR ONE BINDING TO HOLD. Exact means the file's ARGV line IS +// the roster's command; Normalized and AuthoredFromDescription mean the file's ARGV line is the row's +// `original` (so an edit to that quote is caught against the bytes); DeliberatelyOmitted means the +// file has the section and the roster does not. The capture is SUPPLIED, so this stays a pure fold +// and the wet claim's only job is to hand it the real file. +fn binding_holds_against_capture(envelope: BoundHostCaptureEnvelope, stages: List, b: HistoricalStageBinding) -> Bool { + let historical = host_capture_bound_section_argv(envelope: envelope, name: b.name as String) + let carried = stage_named(stages: stages, name: b.name as String) + match b.disposition { + // NEWLY INTRODUCED IS THE ONE DISPOSITION THAT REQUIRES THE SECTION TO BE ABSENT, so it is + // decided before the historical section is demanded. Folded in with the others it was a trap: + // the capture has no such section, so the Absent arm answered false before this arm was + // reached, while roster_matches_bindings counted the stage as carried and passed. The first + // stage anyone added would have gone green on the dry claim and permanently red on the wet one. + // The row still has to EARN the disposition: if the capture does carry the section, the row is + // wrong about history and this fails. + HistoricalStageNewlyIntroduced { reason: _ } => + match historical { + Present { value: _ } => false + Absent => + match carried { + Absent => false + Present { value: _ } => true + } + } + _ => binding_holds_against_present_section(historical: historical, carried: carried, b: b) + } +} + +fn binding_holds_against_present_section(historical: String?, carried: HostCaptureStage?, b: HistoricalStageBinding) -> Bool { + match historical { + Absent => false + Present { value: argv } => + match b.disposition { + HistoricalStageExact { argv: quoted } => + (quoted as String) == argv + && match carried { + Absent => false + Present { value: stage } => (stage.command as String) == argv + } + HistoricalStageNormalized { original: o, replacement: r, reason: _ } => + (o as String) == argv + && match carried { + Absent => false + Present { value: stage } => (stage.command as String) == (r as String) + } + HistoricalStageAuthoredFromDescription { original: o, reason: _ } => + (o as String) == argv + && match carried { + Absent => false + Present { value: _ } => true + } + HistoricalStageDeliberatelyOmitted { reason: _ } => + match carried { + Absent => true + Present { value: _ } => false + } + HistoricalStageNewlyIntroduced { reason: _ } => false + } + } +} + +fn bindings_hold_against_capture(envelope: BoundHostCaptureEnvelope, stages: List) -> Bool { + fold( + census_historical_stage_bindings, + init: true, + f: (acc, b) => acc && binding_holds_against_capture(envelope: envelope, stages: stages, b: b), + ) +} + +// THE MUTATION THE MAP MUST REFUSE: one bound stage's command edited away from what its row says. +// A roster carrying it is not the roster this binding describes, and saying so is the whole point of +// carrying the map rather than a sentence. +fn census_stages_with_mutated_command(name: NonEmptyStr, command: NonEmptyStr) -> List { + map( + host_capture_census_stages, + s => if (s.name as String) == (name as String) { + HostCaptureStage { name: s.name, command: command } + } else { + s + }, + ) +} diff --git a/dag/gunbc/machine_intake/mtcollins1_boot_image_fetch.dag b/dag/gunbc/machine_intake/mtcollins1_boot_image_fetch.dag index bf787aeade6..c9228646441 100644 --- a/dag/gunbc/machine_intake/mtcollins1_boot_image_fetch.dag +++ b/dag/gunbc/machine_intake/mtcollins1_boot_image_fetch.dag @@ -12,6 +12,7 @@ import gunbc.machine_intake_boot_image_fetch { BootImagePublishFailed, BootImagePublishedDigestMismatch, BootImagePublicationNotAdmitted, + BootImagePresentWithDifferentDigest, fetch_verified_boot_image, } @@ -51,6 +52,8 @@ fn mtcollins1_render_fetch(outcome: BootImageFetchOutcome) -> ProcessExit { exit_failure(reason: concat("mtcollins1 boot image: digest agreed but publishing ", s as String, " to ", d as String, " failed")) BootImagePublicationNotAdmitted { export_dir: d, reason: r } => exit_failure(reason: concat("mtcollins1 boot image: the artifact verified but publication into ", d as String, " was NOT admitted: ", r)) + BootImagePresentWithDifferentDigest { path: p, expected: e, observed: o } => + exit_failure(reason: concat("mtcollins1 boot image: REFUSED, ", p as String, " already exists and holds ", o as String, " rather than the verified ", e as String, "; publication is create-only, so nothing was replaced and the verified bytes remain staged beside it")) BootImagePublishedDigestMismatch { path: p, expected: e, observed: o } => exit_failure(reason: concat("mtcollins1 boot image: the move reported success but ", p as String, " now holds ", o as String, " rather than the verified ", e as String, "; the export does NOT contain what this program published and the unit must not be booted from it until that is explained")) } diff --git a/dag/gunbc/machine_intake/mtcollins1_census_image.dag b/dag/gunbc/machine_intake/mtcollins1_census_image.dag new file mode 100644 index 00000000000..bf6580b738e --- /dev/null +++ b/dag/gunbc/machine_intake/mtcollins1_census_image.dag @@ -0,0 +1,160 @@ +module gunbc.machine_intake_mtcollins1_census_image + +import std.types { List, NonEmptyStr, String } +import std.measure { Bandwidth, bandwidth, bandwidth_count } +import extdeps.provisioning.ubuntu_install_media { noble_numbat_2404_3_live_server_arm64 } +import extdeps.provisioning.ubuntu_seeded_install_media { + NoCloudSeedFile, + UbuntuSeededInstallMediaBuildInput, + ubuntu_seeded_install_media_grub_kernel_cmdline, +} +import extdeps.os.ubuntu_autoinstall { UbuntuAutoinstallLiveCommands } +import gunbc.os_install_emit { autoinstall_live_commands_user_data } +import gunbc.seeded_install_media_policy { seeded_install_media_pinned_dates, seeded_install_media_builder_revision } +import gunbc.machine_intake_mtcollins1_boot_artifact { mtcollins1_boot_export_dir, mtcollins1_diskless_image_name } +import gunbc.machine_intake_boot_image_fetch { boot_image_published_path } +import gunbc.seeded_install_media_publish { + BmcExportBoundary, + SeededImagePublication, + SeededImageResolution, + publish_seeded_image, + resolve_seeded_image, +} +import gunbc.machine_intake_host_capture_envelope { + HostCaptureStage, + host_capture_census_stages, + host_capture_program, +} + +// THE CENSUS IMAGE FOR MT. COLLINS UNIT 1: the 24.04.3 arm64 live-server medium with a NoCloud seed +// that runs the host-capture census in the LIVE environment and powers off. It is a derived artifact +// with its own identity -- the bytes differ from stock, so it never takes the stock name and the +// stock file in the export is never touched. +// +// THE RUNTIME SHAPE, in order: UEFI boots the medium; grub passes autoinstall and the NoCloud +// seedfrom; subiquity reads the seed from the medium and runs its early-commands before any other +// step; the one early command runs the capture program off the medium, which writes the envelope to +// the serial console, syncs, and powers off. Nothing is fetched at runtime: the program rides in the +// seed directory. If the power-off does not happen, every later installer section is interactive, +// so the machine waits at the installer rather than touching storage. +// +// The unit's name in the capture envelope. The boot run's verdict matches envelope markers against +// its subject's machine, so this and that must be one value. +data mtcollins1_census_machine: NonEmptyStr = "mtcollins1" + +// The Altra host's serial console is the PL011 at ttyAMA0 -- the console the BMC's SOL carries and +// the one attempt=3's envelope arrived on. THE TTY IS ONE ROW because the program writes to the +// device and the kernel is told to console on the same tty: authored twice, an edit to one spelling +// sends the census to a device nothing is reading, and the envelope's absence would be reported as a +// boot failure -- the misattribution this lane exists to end. Both spellings derive from here. +data mtcollins1_census_console_tty: NonEmptyStr = "ttyAMA0" + +// THE RATE IS A MEASURE, not text: the corpus already models a serial console's rate as +// std.measure Bandwidth (extdeps.network.mikrotik_crs812 rj45 console data_rate), so the kernel +// argument RENDERS this row rather than restating the number. +data mtcollins1_census_console_baud: Bandwidth = bandwidth(count: 115200) + +data mtcollins1_census_console_device: NonEmptyStr = concat("/dev/", mtcollins1_census_console_tty as String) as NonEmptyStr + +data mtcollins1_census_console_arg: NonEmptyStr = concat( + "console=", mtcollins1_census_console_tty as String, ",", + bandwidth_count(b: mtcollins1_census_console_baud) as String, +) as NonEmptyStr + +// The seed directory the earlier census booted with (its /proc/cmdline in attempt=3 reads +// ds=nocloud;s=/cdrom/gunbc-census/), kept so the two captures are comparable. +data mtcollins1_census_nocloud_dir: NonEmptyStr = "gunbc-census" + +data mtcollins1_census_program_file: NonEmptyStr = "capture.sh" + +// poweroff -f does not return on success. If it fails, the installer continues into a session whose +// every section is interactive (see the seed), so no storage action is reachable without a human. +data mtcollins1_census_halt_command: NonEmptyStr = "poweroff -f" + +// ISO 9660 limits a volume id to 32 d-characters. This is the label the booted host reports for the +// medium, so the boot-media check expects it rather than the stock "Ubuntu-Server 24.04.3 LTS arm64". +data mtcollins1_census_volume_id: NonEmptyStr = "GUNBC_MTC1_CENSUS_2404_3" + +data mtcollins1_census_image_stem: NonEmptyStr = "gunbc-mtcollins1-census-ubuntu-24.04.3-arm64" + +fn mtcollins1_census_capture_program(stages: List) -> NonEmptyStr { + host_capture_program( + machine: mtcollins1_census_machine, + console_device: mtcollins1_census_console_device, + stages: stages, + halt_command: mtcollins1_census_halt_command, + ) +} + +fn mtcollins1_census_program_on_medium() -> NonEmptyStr { + concat("/cdrom/", mtcollins1_census_nocloud_dir as String, "/", mtcollins1_census_program_file as String) as NonEmptyStr +} + +// THE STAGE ROSTER IS THE PARAMETER. The census image is this function applied to the census roster; +// a roster that appends stages after the census ones yields a different image with a different +// digest and name, built by the same derivation. Nothing here may run before the census stages or +// between them and the envelope's END. +fn mtcollins1_seeded_image_input(stages: List, image_stem: NonEmptyStr, volume_id: NonEmptyStr) -> UbuntuSeededInstallMediaBuildInput { + UbuntuSeededInstallMediaBuildInput { + stock_artifact: noble_numbat_2404_3_live_server_arm64, + nocloud_dir_on_iso: mtcollins1_census_nocloud_dir, + user_data_body: autoinstall_live_commands_user_data( + payload: UbuntuAutoinstallLiveCommands { + autoinstall_version: 1, + early_commands: [concat("sh ", mtcollins1_census_program_on_medium() as String) as NonEmptyStr], + }, + ) as NonEmptyStr, + seed_files: [ + NoCloudSeedFile { + name: mtcollins1_census_program_file, + content: mtcollins1_census_capture_program(stages: stages), + }, + ], + grub_kernel_cmdline: concat( + ubuntu_seeded_install_media_grub_kernel_cmdline(nocloud_dir: mtcollins1_census_nocloud_dir) as String, + " ", + mtcollins1_census_console_arg as String, + ) as NonEmptyStr, + volume_id: volume_id, + pinned_dates: seeded_install_media_pinned_dates, + image_stem: image_stem, + builder_revision: seeded_install_media_builder_revision, + } +} + +data mtcollins1_census_image_input: UbuntuSeededInstallMediaBuildInput = mtcollins1_seeded_image_input( + stages: host_capture_census_stages, + image_stem: mtcollins1_census_image_stem, + volume_id: mtcollins1_census_volume_id, +) + +// DECLARED FRONTIER (DESIGN §3c): the two effectful entries below have NO consumer in this change, +// and each names the one that lands next. mtcollins1_census_image_publish is consumed by stage 0B/0C: +// a step in the fleet-converge workflow run on srv2 (host=srv2, the SM/WIF credential prelude, the same +// job shape as gunbc.machine_intake_mtcollins1_boot_image_fetch mtcollins1_fetch_boot_image), which +// builds and publishes inside /srv/bmc and emits the published name and digest as its receipt. +// mtcollins1_census_image_resolve is consumed by stage 0D: gunbc.machine_intake_mtcollins1_boot_run +// (#11484) resolves the census image before the approval-gated boot, and its signed boot subject binds +// the resolved image_name and output_digest instead of the stock ones. TRIGGER THAT RETIRES THIS +// FRONTIER: that fleet-converge step and that boot-run binding landing, each calling these functions; +// until both do, neither is reachable from an entry point, and nothing here claims otherwise. +// +// BUILT WHERE IT IS SERVED. The build reads the stock medium already verified into srv2's export and +// writes the derived image into the same export, admitted against that export's ownership +// requirement like every other write the controller will read -- so the image never leaves the host +// that serves it, and the stock file beside it is read and never replaced. +fn mtcollins1_census_image_stock_path() -> NonEmptyStr { + boot_image_published_path(export_dir: mtcollins1_boot_export_dir, image_name: mtcollins1_diskless_image_name) +} + +fn mtcollins1_census_image_publish() -> SeededImagePublication { + publish_seeded_image( + input: mtcollins1_census_image_input, + stock_path: mtcollins1_census_image_stock_path(), + boundary: BmcExportBoundary { export_dir: mtcollins1_boot_export_dir }, + ) +} + +fn mtcollins1_census_image_resolve() -> SeededImageResolution { + resolve_seeded_image(input: mtcollins1_census_image_input, dir: mtcollins1_boot_export_dir) +} diff --git a/dag/gunbc/nbd_proxy_virtual_media_install.dag b/dag/gunbc/nbd_proxy_virtual_media_install.dag index 9575498a2f7..8bf2bd25b21 100644 --- a/dag/gunbc/nbd_proxy_virtual_media_install.dag +++ b/dag/gunbc/nbd_proxy_virtual_media_install.dag @@ -37,7 +37,14 @@ import gunbc.os_install_actuator_selection { ActuatorSelected, ActuatorUnsatisfiable, } import gunbc.fleet_intent { srv3_host, ComputeHost } -import gunbc.srv3_os_install_actuate_scope { srv3_actuator_virtual_media_iso_install_path } +import gunbc.seeded_install_media_publish { + SeededImageResolution, + SeededImageResolved, + SeededImageNotBuilt, + SeededImageRecordedImageAbsent, + SeededImageRecordDiverged, + SeededImageUnobservable, +} import gunbc.os_install_mechanism { OsInstallMechanism, VirtualMediaInstall, @@ -156,12 +163,13 @@ type OsInstallActuatorPlanStanding fn os_install_actuator_plan_standing_for( solution: OsInstallMechanismSolution, export: NbdProxyServeInterface, + image: SeededImageResolution, ) -> OsInstallActuatorPlanStanding { match solution { OsInstallMechanismSelected { mechanism: m } => match os_install_actuator_plan(mechanism: m, export: export) { p => - match actuator_plan_realization(steps: p.steps) { + match actuator_plan_realization(steps: p.steps, image: image) { ActuatorPlanRealized { intents: xs } => ActuatorPlanReady { plan: p, intents: xs } ActuatorPlanUnrealized { refusals: rs } => ActuatorPlanRefused { cause: ActuatorStepsUnrealized { refusals: rs } } @@ -172,10 +180,13 @@ fn os_install_actuator_plan_standing_for( } } -data srv3_os_install_actuator_plan_standing: OsInstallActuatorPlanStanding = os_install_actuator_plan_standing_for( - solution: srv3_install_mechanism_solution, - export: srv3_nbd_proxy_export_placeholder -) +fn srv3_os_install_actuator_plan_standing(image: SeededImageResolution) -> OsInstallActuatorPlanStanding { + os_install_actuator_plan_standing_for( + solution: srv3_install_mechanism_solution, + export: srv3_nbd_proxy_export_placeholder, + image: image, + ) +} // An unrealized step is a REFUSAL, not an absence. The Optional form below returned Absent for // the three steps with no realization, and Absent is indistinguishable from "this step needs no @@ -186,6 +197,7 @@ data srv3_os_install_actuator_plan_standing: OsInstallActuatorPlanStanding = os_ type ActuatorStepRefusal = ActuatorStepRealizationAbsent { step: OsInstallActuatorStep } | ActuatorStepActuatorHostUnsatisfiable { step: OsInstallActuatorStep, reason: String } + | ActuatorStepImageUnresolved { step: OsInstallActuatorStep, reason: String } type ActuatorStepRealization = ActuatorStepReady { intent: HostEffectIntent } @@ -197,12 +209,12 @@ type ActuatorPlanRealization // Accumulates every unrealized step rather than stopping at the first: which effects are missing // is the actionable fact, and reporting one of three hides the size of the gap. -fn actuator_plan_realization(steps: List) -> ActuatorPlanRealization { +fn actuator_plan_realization(steps: List, image: SeededImageResolution) -> ActuatorPlanRealization { fold( steps, init: ActuatorPlanRealized { intents: [] }, f: fn(acc, step) { - match actuator_step_realization(step: step) { + match actuator_step_realization(step: step, image: image) { ActuatorStepReady { intent: i } => match acc { ActuatorPlanRealized { intents: xs } => @@ -220,13 +232,29 @@ fn actuator_plan_realization(steps: List) -> ActuatorPlan ) } -fn actuator_step_realization(step: OsInstallActuatorStep) -> ActuatorStepRealization { - actuator_step_realization_under(step: step, selection: srv3_os_install_actuator_selection) +fn actuator_step_realization(step: OsInstallActuatorStep, image: SeededImageResolution) -> ActuatorStepRealization { + actuator_step_realization_under(step: step, selection: srv3_os_install_actuator_selection, image: image) +} + +// WHICH BYTES THE NBD STEP SERVES is the seeded image the actuator host's derivation record resolves +// to, re-measured at resolution. An image that is not built, whose record disagrees with its bytes, +// or that cannot be observed is a refusal of the serve step -- never a path the step serves anyway. +fn actuator_image_unresolved_reason(image: SeededImageResolution) -> String { + match image { + SeededImageResolved { built: _, path: p } => concat("resolved at ", p as String) + SeededImageNotBuilt { record_path: r } => concat("no derivation record at ", r as String) + SeededImageRecordedImageAbsent { path: p, recorded: r } => + concat("record names ", r as String, " but ", p as String, " is absent") + SeededImageRecordDiverged { path: p, recorded: r, observed: o } => + concat("record names ", r as String, " but ", p as String, " reads ", o as String) + SeededImageUnobservable { reason: r } => r + } } fn actuator_step_realization_under( step: OsInstallActuatorStep, selection: OsInstallActuatorSelection, + image: SeededImageResolution, ) -> ActuatorStepRealization { match step { ActuatorBootOnce { target: t, enabled: e } => @@ -263,23 +291,33 @@ fn actuator_step_realization_under( } } ActuatorSelected { host: actuator_host } => - ActuatorStepReady { - intent: HostEffectIntent { - target: HostOs { node: actuator_host }, - effect: Srv3NbdProxyServe { - intent: nbd_proxy_serve_transport_intent( - bmc_host: asrock_srv3_live_bmc_host, - session_token: bmcweb_session_token_env_ref, - ws_path: bmcweb_nbd_proxy_ws_path(variant: ep, slot: s, index: i), - iso_path: srv3_actuator_virtual_media_iso_install_path, - local_nbd_port: srv3_nbd_proxy_local_port, - ), - lease_key: srv3_nbd_proxy_lease_key, - }, - evidence: host_effect_identity_evidence, - transport: LocalShell, - privilege_denial: none, - } + match image { + SeededImageResolved { built: _, path: iso_path } => + ActuatorStepReady { + intent: HostEffectIntent { + target: HostOs { node: actuator_host }, + effect: Srv3NbdProxyServe { + intent: nbd_proxy_serve_transport_intent( + bmc_host: asrock_srv3_live_bmc_host, + session_token: bmcweb_session_token_env_ref, + ws_path: bmcweb_nbd_proxy_ws_path(variant: ep, slot: s, index: i), + iso_path: iso_path, + local_nbd_port: srv3_nbd_proxy_local_port, + ), + lease_key: srv3_nbd_proxy_lease_key, + }, + evidence: host_effect_identity_evidence, + transport: LocalShell, + privilege_denial: none, + } + } + _ => + ActuatorStepRefused { + cause: ActuatorStepImageUnresolved { + step: ActuatorNbdProxyServe { endpoint: ep, slot: s, index: i, export: exp }, + reason: actuator_image_unresolved_reason(image: image), + } + } } } ActuatorRedfishInsertMedia => diff --git a/dag/gunbc/os_install_emit.dag b/dag/gunbc/os_install_emit.dag index 8c8aab8c69a..e771f46f8a7 100644 --- a/dag/gunbc/os_install_emit.dag +++ b/dag/gunbc/os_install_emit.dag @@ -8,6 +8,7 @@ import extdeps.os.ubuntu_autoinstall { AutoinstallStoragePolicy, StoragePolicyAbsent, StoragePolicyDirectLayout, + UbuntuAutoinstallLiveCommands, } import extdeps.ssh { SshPublicKey, ssh_public_key_line } import extdeps.languages.yaml.types { @@ -71,3 +72,14 @@ fn autoinstall_user_data(payload: UbuntuAutoinstallPayload) -> String { ]) concat("#cloud-config\n", serialize_yaml(v: top)) } + +fn autoinstall_live_commands_user_data(payload: UbuntuAutoinstallLiveCommands) -> String { + let top = yaml_mapping(entries: [ + kv(key: "autoinstall", value: yaml_mapping(entries: [ + kv(key: "version", value: yaml_int(n: payload.autoinstall_version)), + kv(key: "interactive-sections", value: yaml_sequence(elements: [yaml_string(s: "*")])), + kv(key: "early-commands", value: yaml_sequence(elements: map(payload.early_commands, c => yaml_string(s: c)))), + ])), + ]) + concat("#cloud-config\n", serialize_yaml(v: top)) +} diff --git a/dag/gunbc/provisioning/seeded_install_media_policy.dag b/dag/gunbc/provisioning/seeded_install_media_policy.dag new file mode 100644 index 00000000000..7e73d271539 --- /dev/null +++ b/dag/gunbc/provisioning/seeded_install_media_policy.dag @@ -0,0 +1,28 @@ +module gunbc.seeded_install_media_policy + +import std.types { NonEmptyStr } +import extdeps.provisioning.ubuntu_seeded_install_media { SeededInstallMediaPinnedDates } + +// WHICH INSTANT A DERIVED IMAGE CLAIMS, shared by every seeded image this fleet builds. A fixed value +// is what a reproducible build needs (whether it is SUFFICIENT is the bet typed on +// SeededInstallMediaPinnedDates); this one is the 24.04.3 arm64 live-server medium's own volume +// date, read off its blkid UUID ("2025-08-06-15-52-50-00") in the boot-media section of +// artifacts/bmc/mtcollins1-host-capture.txt, so a derived image reports the date of the release it +// was derived from rather than a date that means nothing. It is policy, not an upstream fact: the +// derived image's label, not its date, is what distinguishes it from stock on the booted host. +data seeded_install_media_pinned_dates: SeededInstallMediaPinnedDates = SeededInstallMediaPinnedDates { + volume_uuid: "2025080615525000", + file_date: "2025.08.06.155250", +} + +// THE BUILDER'S REVISION IN THE DERIVATION KEY. Bump it in the same change as any edit to +// extdeps.provisioning.ubuntu_seeded_install_media_remaster or the xorriso replay operation that can +// change output bytes, so a derivation record written by the old builder cannot answer for the new +// one. A bump costs one rebuild per image. A MISSED bump is not caught by anything downstream: the +// old record and the old image still agree with each other, so re-measurement passes and consumers +// keep receiving the previous builder's output under the same key. That is why the bump is owed at +// the edit rather than left to a later check -- and why this row is a declared below-ceiling class, +// gunbc.recurring_failure_mode.a_hand_bumped_builder_revision_keys_a_derivation, not a wall. NEXT-RUNG +// TRIGGER: an evaluation-time content hash of a named declaration closure, at which point this row +// is deleted and the key reads the builder closure's hash instead. +data seeded_install_media_builder_revision: NonEmptyStr = "xorriso-replay-pinned-dates/1" diff --git a/dag/gunbc/provisioning/seeded_install_media_publish.dag b/dag/gunbc/provisioning/seeded_install_media_publish.dag new file mode 100644 index 00000000000..de4f2066280 --- /dev/null +++ b/dag/gunbc/provisioning/seeded_install_media_publish.dag @@ -0,0 +1,190 @@ +module gunbc.seeded_install_media_publish + +import std.types { NonEmptyStr, String, FilePath } +import std.algebra { trim } +import extdeps.shell +import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.tools.sha256sum { Sha256FileDigest, Sha256FileDigestUnavailable, sha256sum_file_digest_via_shell } +import extdeps.provisioning.ubuntu_seeded_install_media { + UbuntuSeededInstallMediaBuildInput, + UbuntuSeededInstallMediaBuilt, + ubuntu_seeded_install_media_built, + ubuntu_seeded_install_media_record_path, + ubuntu_seeded_install_media_staging_path, +} +import extdeps.provisioning.ubuntu_seeded_install_media_remaster { + SeededInstallMediaStageOutcome, + SeededInstallMediaStaged, + SeededInstallMediaStageRefused, + seeded_install_media_stage, +} +import gunbc.machine_intake_boot_image_fetch { + BootImageFetchOutcome, + BootImageAdmitted, + BootImageDigestUnreadable, + BootImageDigestMismatchRefused, + BootImageFetchFailed, + BootImagePublishFailed, + BootImagePublishedDigestMismatch, + BootImagePublicationNotAdmitted, + BootImagePresentWithDifferentDigest, + admit_then_publish_boot_image, + boot_image_published_path, + publish_verified_boot_image, +} + +// WHERE A DERIVED IMAGE IS WRITTEN, as the two boundaries this fleet actually has. A BMC export is +// read by a controller that fetches whatever the share holds, so writing into it is admitted against +// the export's ownership requirement (gunbc.machine_intake_boot_image_export_ownership) exactly as +// the stock fetch is. A host's local artifacts directory is served by a process this fleet runs, and +// carries no such requirement today; that is its standing, named rather than defaulted. +type SeededImageWriteBoundary + = BmcExportBoundary { export_dir: NonEmptyStr } + | HostArtifactsDirectory { dir: NonEmptyStr } + +fn seeded_image_boundary_dir(boundary: SeededImageWriteBoundary) -> NonEmptyStr { + match boundary { + BmcExportBoundary { export_dir: d } => d + HostArtifactsDirectory { dir: d } => d + } +} + +// WHAT A CONSUMER THAT DID NOT RUN THE BUILD CAN KNOW. The derivation record names the digest these +// inputs produced; the image under that digest's name is measured again before it is reported, so a +// record is never taken as evidence for bytes nobody re-read. Three unresolved states stay distinct +// because they have different remedies: the named image is ABSENT (it was removed; rebuilding is +// safe because the name is free), it holds DIFFERENT bytes (someone must look; nothing is replaced), +// or it could not be READ (says nothing about the bytes; neither rebuild nor accuse them). +type SeededImageResolution + = SeededImageResolved { built: UbuntuSeededInstallMediaBuilt, path: NonEmptyStr } + | SeededImageNotBuilt { record_path: NonEmptyStr } + | SeededImageRecordedImageAbsent { path: NonEmptyStr, recorded: NonEmptyStr } + | SeededImageRecordDiverged { path: NonEmptyStr, recorded: NonEmptyStr, observed: NonEmptyStr } + | SeededImageUnobservable { reason: String } + +type SeededImagePublication + = SeededImagePublished { built: UbuntuSeededInstallMediaBuilt, path: NonEmptyStr } + | SeededImageAlreadyPublished { built: UbuntuSeededInstallMediaBuilt, path: NonEmptyStr } + | SeededImagePublicationRefused { reason: String } + +fn resolve_seeded_image_named(input: UbuntuSeededInstallMediaBuildInput, dir: NonEmptyStr, recorded: NonEmptyStr) -> SeededImageResolution { + let built = ubuntu_seeded_install_media_built(input: input, output_digest: recorded) + let path = boot_image_published_path(export_dir: dir, image_name: built.image_name) + if shell.Test.IsFile(path: path as FilePath).is_file == false { + SeededImageRecordedImageAbsent { path: path, recorded: recorded } + } else { + match sha256sum_file_digest_via_shell(path: path) { + Sha256FileDigestUnavailable { path: _, reason: r } => + SeededImageUnobservable { reason: concat("the image at ", path as String, " exists but its digest could not be read: ", r) } + Sha256FileDigest { digest: d } => + if (d.hex as String) == (recorded as String) { + SeededImageResolved { built: built, path: path } + } else { + SeededImageRecordDiverged { path: path, recorded: recorded, observed: d.hex } + } + } + } +} + +fn resolve_seeded_image(input: UbuntuSeededInstallMediaBuildInput, dir: NonEmptyStr) -> SeededImageResolution { + let record_path = ubuntu_seeded_install_media_record_path(dir: dir, input: input) + if shell.Test.IsFile(path: record_path as FilePath).is_file == false { + SeededImageNotBuilt { record_path: record_path } + } else { + let read = Filesystem.Read(path: record_path as String) + let recorded = trim(s: read.content) + if read.success == false { + SeededImageUnobservable { reason: concat("derivation record unreadable at ", record_path as String) } + } else if recorded.length() == 0 { + SeededImageUnobservable { reason: concat("derivation record is empty at ", record_path as String) } + } else { + resolve_seeded_image_named(input: input, dir: dir, recorded: recorded as NonEmptyStr) + } + } +} + +fn seeded_image_publish_refusal(outcome: BootImageFetchOutcome) -> String { + match outcome { + BootImageAdmitted { path: p, digest: _ } => concat("published at ", p as String) + BootImageFetchFailed { url: u } => concat("fetch failed for ", u as String) + BootImageDigestUnreadable { path: p, reason: r } => concat("digest unreadable at ", p as String, ": ", r) + BootImageDigestMismatchRefused { expected: e, observed: o } => concat("digest ", o as String, " is not ", e as String) + BootImagePublishFailed { staged: s, destination: d } => concat("linking ", s as String, " to ", d as String, " failed") + BootImagePublishedDigestMismatch { path: p, expected: e, observed: o } => + concat(p as String, " reads back as ", o as String, " rather than the built ", e as String) + BootImagePublicationNotAdmitted { export_dir: d, reason: r } => concat("publication into ", d as String, " not admitted: ", r) + BootImagePresentWithDifferentDigest { path: p, expected: e, observed: o } => + concat(p as String, " already holds ", o as String, " rather than the built ", e as String, "; nothing was replaced") + } +} + +fn publish_staged_seeded_image(boundary: SeededImageWriteBoundary, staging_path: NonEmptyStr, published_path: NonEmptyStr, digest: NonEmptyStr) -> BootImageFetchOutcome { + match boundary { + BmcExportBoundary { export_dir: d } => + admit_then_publish_boot_image(staging_path: staging_path, published_path: published_path, export_dir: d, digest: digest) + HostArtifactsDirectory { dir: _ } => + publish_verified_boot_image(staging_path: staging_path, published_path: published_path, digest: digest) + } +} + +// The record is written only after the image reads back at its published name, so a record never +// names bytes that are not there. If the write fails the image stays published and the refusal says +// so: a consumer will find no record and report the image as not built, which is the safe reading. +fn record_published_seeded_image(input: UbuntuSeededInstallMediaBuildInput, dir: NonEmptyStr, built: UbuntuSeededInstallMediaBuilt, path: NonEmptyStr) -> SeededImagePublication { + let wrote = Filesystem.Write( + path: ubuntu_seeded_install_media_record_path(dir: dir, input: input) as String, + content: concat(built.output_digest as String, "\n"), + ) + if wrote.success { + SeededImagePublished { built: built, path: path } + } else { + SeededImagePublicationRefused { reason: concat("image published at ", path as String, " but its derivation record could not be written") } + } +} + +fn publish_built_seeded_image(input: UbuntuSeededInstallMediaBuildInput, boundary: SeededImageWriteBoundary, staged: SeededInstallMediaStageOutcome) -> SeededImagePublication { + let dir = seeded_image_boundary_dir(boundary: boundary) + match staged { + SeededInstallMediaStageRefused { reason: r } => SeededImagePublicationRefused { reason: r as String } + SeededInstallMediaStaged { staging_path: s, output_digest: digest } => { + let built = ubuntu_seeded_install_media_built(input: input, output_digest: digest) + let path = boot_image_published_path(export_dir: dir, image_name: built.image_name) + match publish_staged_seeded_image(boundary: boundary, staging_path: s, published_path: path, digest: digest) { + BootImageAdmitted { path: _, digest: _ } => record_published_seeded_image(input: input, dir: dir, built: built, path: path) + other => SeededImagePublicationRefused { reason: seeded_image_publish_refusal(outcome: other) } + } + } + } +} + +// BUILD ONCE PER DERIVATION. An existing record whose image re-measures clean is the answer; a record +// that disagrees with its image REFUSES rather than rebuilding, because publication is create-only and +// the name it points at is already taken by bytes someone must look at. +fn publish_seeded_image(input: UbuntuSeededInstallMediaBuildInput, stock_path: NonEmptyStr, boundary: SeededImageWriteBoundary) -> SeededImagePublication { + let dir = seeded_image_boundary_dir(boundary: boundary) + match resolve_seeded_image(input: input, dir: dir) { + SeededImageResolved { built: b, path: p } => SeededImageAlreadyPublished { built: b, path: p } + SeededImageRecordDiverged { path: p, recorded: r, observed: o } => + SeededImagePublicationRefused { reason: concat("derivation record names ", r as String, " but ", p as String, " reads ", o as String) } + SeededImageUnobservable { reason: r } => SeededImagePublicationRefused { reason: r } + SeededImageRecordedImageAbsent { path: _, recorded: _ } => build_and_publish_seeded_image(input: input, stock_path: stock_path, boundary: boundary) + SeededImageNotBuilt { record_path: _ } => build_and_publish_seeded_image(input: input, stock_path: stock_path, boundary: boundary) + } +} + +// The record's image is gone or there is no record: the name is free, so the derivation is built +// and published create-only. With pinned dates a rebuild of the same inputs reproduces the recorded +// digest; if it does not, the new record names what was actually built and the earlier digest is +// simply no longer claimed. +fn build_and_publish_seeded_image(input: UbuntuSeededInstallMediaBuildInput, stock_path: NonEmptyStr, boundary: SeededImageWriteBoundary) -> SeededImagePublication { + let dir = seeded_image_boundary_dir(boundary: boundary) + publish_built_seeded_image( + input: input, + boundary: boundary, + staged: seeded_install_media_stage( + input: input, + stock_path: stock_path, + staging_path: ubuntu_seeded_install_media_staging_path(dir: dir, input: input), + ), + ) +} diff --git a/dag/gunbc/recurring_failure_mode/a_hand_bumped_builder_revision_keys_a_derivation.dag b/dag/gunbc/recurring_failure_mode/a_hand_bumped_builder_revision_keys_a_derivation.dag new file mode 100644 index 00000000000..c286d8d6cd9 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/a_hand_bumped_builder_revision_keys_a_derivation.dag @@ -0,0 +1,22 @@ +module gunbc.recurring_failure_mode.a_hand_bumped_builder_revision_keys_a_derivation + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data a_hand_bumped_builder_revision_keys_a_derivation: RecurringFailureMode = RecurringFailureMode { + identity: "a_hand_bumped_builder_revision_keys_a_derivation" as NonEmptyStr, + + receipts: [ + "INVALID STATE: a derivation key names its builder by an AUTHORED revision string rather than by the builder's own content. An edit to the builder that changes output bytes, landed without bumping the string, leaves every existing derivation record answering for inputs the NEW builder would build differently. The record and the image it names still agree with each other, so re-measurement passes and consumers keep receiving the previous builder's output under the current key.", + + "HARM: silent. Nothing refuses and nothing is mislabelled at the image level -- the bytes are a real build of those inputs -- but they are not the build the current builder would produce, so a builder fix (for example to the boot structure the replay preserves) does not reach an image that was already recorded. DESIGN section 4b places silent wrongness outside the ladder.", + + "SPECIMEN, found in review 68615 on gunbc#11731: gunbc.seeded_install_media_policy seeded_install_media_builder_revision keys extdeps.provisioning.ubuntu_seeded_install_media seeded_install_media_build_key, and the row's own annotation stated that a missed bump is caught by nothing downstream.", + + "RUNG FOUND AT: below mitigation -- review diligence only. The class is DECIDABLE and GROUNDED: the builder is a set of declarations whose Node trees already carry v2.std.node content_hash, so the revision is derivable rather than authored. ATTAINABLE CEILING: structurally impossible -- no authored revision field exists; the key's builder component IS the content hash of the builder's declaration closure.", + + "NEXT-RUNG TRIGGER, at capability grain: an evaluation-time value for the content hash of a named declaration closure (here extdeps.provisioning.ubuntu_seeded_install_media_remaster, the xorriso replay operation it calls, and the host-capture renderer the census seed carries), consumable by a data row. When that capability lands, builder_revision is deleted from UbuntuSeededInstallMediaBuildInput and the key reads the closure hash; its discriminating RED is a builder edit that changes no authored string and still moves the key.", + ], + + evidence: [], +} diff --git a/dag/gunbc/srv3/srv3_host_effect_apply.dag b/dag/gunbc/srv3/srv3_host_effect_apply.dag index 2c020b9171f..ed26cfb3a39 100644 --- a/dag/gunbc/srv3/srv3_host_effect_apply.dag +++ b/dag/gunbc/srv3/srv3_host_effect_apply.dag @@ -86,8 +86,15 @@ fn srv3_bmcweb_token_extract_apply() -> Srv3ApplyDisposition { srv3_host_effect_apply(effect: Srv3BmcwebTokenExtract) } +// The served bytes are the seeded image this host's derivation record resolves to, re-measured now; +// an unresolved image refuses the serve rather than serving whatever a path happens to hold. fn srv3_nbd_proxy_serve_apply() -> Srv3ApplyDisposition { - srv3_host_effect_apply(effect: srv3_nbd_proxy_serve_host_effect()) + match srv3_seeded_install_media_resolve() { + SeededImageResolved { built: _, path: p } => + srv3_host_effect_apply(effect: srv3_nbd_proxy_serve_host_effect(iso_path: p)) + other => + Srv3ApplyRefused { reason: concat("nbd-proxy-serve: seeded image unresolved: ", actuator_image_unresolved_reason(image: other)) } + } } fn srv3_boot_once_read_back_sleep_apply() -> Srv3ApplyDisposition { diff --git a/dag/gunbc/srv3/srv3_nbd_proxy_serve_intent.dag b/dag/gunbc/srv3/srv3_nbd_proxy_serve_intent.dag index b55c3e16f39..8c3ae48dea5 100644 --- a/dag/gunbc/srv3/srv3_nbd_proxy_serve_intent.dag +++ b/dag/gunbc/srv3/srv3_nbd_proxy_serve_intent.dag @@ -12,13 +12,14 @@ import extdeps.boards.asrock_rack { asrock_srv3_live_bmc_host, asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint, } -import gunbc.srv3_os_install_actuate_scope { srv3_actuator_virtual_media_iso_install_path } import gunbc.host_effect { HostEffect, Srv3NbdProxyServe } import std.types { NonEmptyStr } data srv3_nbd_proxy_lease_key: NonEmptyStr = "srv3-nbd-proxy-10809" as NonEmptyStr -fn srv3_nbd_proxy_serve_transport_intent() -> NbdProxyServeTransportIntent { +// The ISO path is the resolved seeded image's, supplied by the caller that resolved it; this module +// never names an image path of its own. +fn srv3_nbd_proxy_serve_transport_intent(iso_path: NonEmptyStr) -> NbdProxyServeTransportIntent { nbd_proxy_serve_transport_intent( bmc_host: asrock_srv3_live_bmc_host, session_token: concat("$", "BMCWEB_SESSION_TOKEN") as NonEmptyStr, @@ -27,18 +28,18 @@ fn srv3_nbd_proxy_serve_transport_intent() -> NbdProxyServeTransportIntent { slot: 0, index: 0, ), - iso_path: srv3_actuator_virtual_media_iso_install_path, + iso_path: iso_path, local_nbd_port: srv3_nbd_proxy_local_port, ) } -fn srv3_nbd_proxy_serve_intent() -> NbdProxyServeTransportIntent { - srv3_nbd_proxy_serve_transport_intent() +fn srv3_nbd_proxy_serve_intent(iso_path: NonEmptyStr) -> NbdProxyServeTransportIntent { + srv3_nbd_proxy_serve_transport_intent(iso_path: iso_path) } -fn srv3_nbd_proxy_serve_host_effect() -> HostEffect { +fn srv3_nbd_proxy_serve_host_effect(iso_path: NonEmptyStr) -> HostEffect { Srv3NbdProxyServe { - intent: srv3_nbd_proxy_serve_transport_intent(), + intent: srv3_nbd_proxy_serve_transport_intent(iso_path: iso_path), lease_key: srv3_nbd_proxy_lease_key, } } diff --git a/dag/gunbc/srv3/srv3_os_install_actuate.dag b/dag/gunbc/srv3/srv3_os_install_actuate.dag index dffdde8d0f7..f3262a0da98 100644 --- a/dag/gunbc/srv3/srv3_os_install_actuate.dag +++ b/dag/gunbc/srv3/srv3_os_install_actuate.dag @@ -118,10 +118,13 @@ fn srv3_nbd_proxy_serve() -> ProcessExit // The step count was never the subject — that the plan is DERIVED from the solved mechanism was. // srv3's BMC upgrade propagates through that derivation and the plan now refuses, which is what // distinguishes a derived plan from a hardcoded three-step one: a hardcoded plan would still be Ready. -fn srv3_os_install_actuator_plan_refuses_on_unestablished_path() -> Bool { - match srv3_os_install_actuator_plan_standing { - ActuatorPlanRefused { cause: _ } => true - ActuatorPlanReady { plan: _, intents: _ } => false +// THE REFUSAL MUST BE THE MECHANISM'S. The plan is also a function of the served image, and an +// unresolved image refuses the serve step on its own; a caller supplying a RESOLVED image isolates +// the mechanism as the only thing left that can refuse. +fn srv3_os_install_actuator_plan_refuses_on_unestablished_path(image: SeededImageResolution) -> Bool { + match srv3_os_install_actuator_plan_standing(image: image) { + ActuatorPlanRefused { cause: ActuatorMechanismRefused { cause: _ } } => true + _ => false } } diff --git a/dag/gunbc/srv3/srv3_os_install_actuate_scope.dag b/dag/gunbc/srv3/srv3_os_install_actuate_scope.dag index adf224a6e61..ebeb2a1c7d3 100644 --- a/dag/gunbc/srv3/srv3_os_install_actuate_scope.dag +++ b/dag/gunbc/srv3/srv3_os_install_actuate_scope.dag @@ -7,7 +7,7 @@ import gunbc.os_install { srv3_seeded_os_install_plan_standing, srv3_os_install_plan_autoinstall, } -import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_ubuntu_install_media_install_path } +import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_install_media_input } import gunbc.assimilate.bmc_wif_delegation_chain { bmc_srv3_admin_secret_ref } import gunbc.assimilate.bmc_token_federation { bmc_srv3_secret_name } import gunbc.os_install_mechanism { @@ -25,11 +25,9 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } import std.types { NonEmptyStr, Int, Bool, String } data gunbc_srv3_os_install_actuate_scope_boundary: Disposition = Terminal { - reason: "prefix:os-install-actuated boundary — actuator prep (stock ISO fetch, seeded-ISO remaster, derived actuator host, NBD serve/boot-once/SOL capture runnable entrypoints) + operator runbook ONLY; NOT srv3 OS install solved. Legacy solver plan (srv3_os_install_plan, NoCloudNet) seed gap stays fail-closed; actuator prep binds srv3_seeded_os_install_plan + srv3_actuator_virtual_media_iso_install_path (NoCloudLocal on-ISO). BMC runtime login resolves intent vs observed credential phase (gunbc.srv3_bmc_credential_resolve); destructive boot-once gated by workflow escalation (gunbc.srv3_os_install_actuate_workflow)." + reason: "prefix:os-install-actuated boundary — actuator prep (stock ISO fetch, seeded-ISO remaster, derived actuator host, NBD serve/boot-once/SOL capture runnable entrypoints) + operator runbook ONLY; NOT srv3 OS install solved. Legacy solver plan (srv3_os_install_plan, NoCloudNet) seed gap stays fail-closed; actuator prep binds srv3_seeded_os_install_plan + srv3_seeded_install_media_input (NoCloudLocal on-ISO; the served path is resolved from its derivation record at serve time). BMC runtime login resolves intent vs observed credential phase (gunbc.srv3_bmc_credential_resolve); destructive boot-once gated by workflow escalation (gunbc.srv3_os_install_actuate_workflow)." } -data srv3_actuator_virtual_media_iso_install_path: NonEmptyStr = srv3_seeded_ubuntu_install_media_install_path - type AutoinstallSeedDeliveryRefusal { seed_url: NonEmptyStr refusal: NonEmptyStr @@ -113,11 +111,8 @@ fn srv3_os_install_actuate_credential_debt_grounds_bmc_secret_ref() -> Bool { fn srv3_actuator_prep_binds_seeded_iso_path() -> Bool { - srv3_actuator_virtual_media_iso_install_path == srv3_seeded_ubuntu_install_media_install_path - && string_contains( - s: srv3_actuator_virtual_media_iso_install_path as String, - pattern: "srv3-seeded.iso", - ) + string_contains(s: srv3_seeded_install_media_input.image_stem as String, pattern: "srv3-seeded") + && string_contains(s: srv3_seeded_install_media_input.user_data_body as String, pattern: "hostname: srv3") } // PREP IS NOT INSTALL — still the claim, and now demonstrated more sharply. The diff --git a/dag/gunbc/srv3/srv3_os_install_actuate_workflow.dag b/dag/gunbc/srv3/srv3_os_install_actuate_workflow.dag index d062d79126f..b707522c06d 100644 --- a/dag/gunbc/srv3/srv3_os_install_actuate_workflow.dag +++ b/dag/gunbc/srv3/srv3_os_install_actuate_workflow.dag @@ -37,11 +37,6 @@ import gunbc.workflow_escalation { workflow_step_disposition_for_authorization, workflow_step_is_runnable, } -import gunbc.nbd_proxy_virtual_media_install { - ActuatorPlanReady, - ActuatorPlanRefused, - srv3_os_install_actuator_plan_standing, -} import gunbc.srv3_bmc_credential_resolve { srv3_os_install_actuate_credential_intent } type OsInstallActuateWorkflowStep @@ -255,10 +250,3 @@ fn srv3_os_install_actuate_workflow_includes_operator_approval_before_boot() -> fn srv3_os_install_actuate_workflow_credential_intent_is_declared() -> Bool { srv3_os_install_actuate_credential_intent().rotated_secret_ref.secret == "bmc-srv3-admin" as NonEmptyStr } - -fn srv3_os_install_actuator_plan_step_count_matches_workflow_tail() -> Bool { - match srv3_os_install_actuator_plan_standing { - ActuatorPlanReady { plan: p, intents: _ } => p.steps.length() == 3 - ActuatorPlanRefused { cause: _ } => false - } -} diff --git a/dag/gunbc/srv3/srv3_os_install_diagnostic.dag b/dag/gunbc/srv3/srv3_os_install_diagnostic.dag index 23579462a71..30824dad85d 100644 --- a/dag/gunbc/srv3/srv3_os_install_diagnostic.dag +++ b/dag/gunbc/srv3/srv3_os_install_diagnostic.dag @@ -1,11 +1,6 @@ module gunbc.srv3_os_install_diagnostic import std.types { NonEmptyStr, Bool, Int, String, Milliseconds, List } -import std.content_hash { - ContentHash, - as_content_hash_structural, - fnv1a64_structural_hex_digest, -} import std.measure { ByteSize, byte_size, Millisecond } import std.disposition { Disposition, Terminal } import extdeps.bmc.types { @@ -43,8 +38,7 @@ import gunbc.network_identity_subsumption { srv3_post_install_lease_table_fixture, srv3_bmc_static_lease_row, } -import gunbc.srv3_os_install_actuate_scope { srv3_actuator_virtual_media_iso_install_path } -import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_install_media_content_hash } +import extdeps.provisioning.ubuntu_seeded_install_media { UbuntuSeededInstallMediaBuilt } import product.placement_supply { HostIdentity } import gunbc.srv3_actuate_shell_paths { srv3_bmcweb_token_path, @@ -120,23 +114,27 @@ type OsInstallAttemptIntent { actuator_host: HostIdentity bmc_endpoint: NonEmptyStr iso_path: NonEmptyStr - iso_content_hash: ContentHash + iso_sha256: NonEmptyStr expected_hostname: NonEmptyStr boot_source: RedfishBootSourceOverrideTarget boot_override: RedfishBootSourceOverrideEnabled serve_port: Int } -data srv3_os_install_attempt_intent: OsInstallAttemptIntent = OsInstallAttemptIntent { - target_host: operator_host_srv3, - actuator_host: operator_host_srv1, - bmc_endpoint: asrock_srv3_live_bmc_host, - iso_path: srv3_actuator_virtual_media_iso_install_path, - iso_content_hash: srv3_seeded_install_media_content_hash, - expected_hostname: "srv3" as NonEmptyStr, - boot_source: BootTargetCd, - boot_override: BootOverrideOnce, - serve_port: srv3_nbd_proxy_local_port, +// The attempt serves the image the actuator resolved, so the intent is a function of that image: +// the expected digest is the built image's measured sha256, never a hash of the build's inputs. +fn srv3_os_install_attempt_intent_for(image: UbuntuSeededInstallMediaBuilt, iso_path: NonEmptyStr) -> OsInstallAttemptIntent { + OsInstallAttemptIntent { + target_host: operator_host_srv3, + actuator_host: operator_host_srv1, + bmc_endpoint: asrock_srv3_live_bmc_host, + iso_path: iso_path, + iso_sha256: image.output_digest, + expected_hostname: "srv3" as NonEmptyStr, + boot_source: BootTargetCd, + boot_override: BootOverrideOnce, + serve_port: srv3_nbd_proxy_local_port, + } } type NbdServeProcessObservation @@ -155,7 +153,7 @@ type BmcSessionTokenObservation type IsoServeReadinessObservation = IsoMissing - | IsoPresent { observed_content_hash: ContentHash } + | IsoPresent { observed_sha256: NonEmptyStr } type NbdServeReadinessVerdict = ServeAbsent @@ -337,8 +335,8 @@ fn classify_nbd_serve_readiness( TokenPresent => match iso { IsoMissing => IsoMissingVerdict - IsoPresent { observed_content_hash: observed } => - if (observed as String) != (intent.iso_content_hash as String) { + IsoPresent { observed_sha256: observed } => + if (observed as String) != (intent.iso_sha256 as String) { IsoHashMismatch } else { match port { @@ -1184,17 +1182,21 @@ fn parse_serve_port_observation(stdout: String, intent: OsInstallAttemptIntent) } } -fn parse_observe_content_hash_observed(stdout: String) -> ContentHash? { - let parts = split(s: stdout, delimiter: "content_hash_observed=") +fn parse_observe_iso_sha256_observed(stdout: String) -> NonEmptyStr? { + let parts = split(s: stdout, delimiter: "iso_sha256_observed=") if parts.length() <= 1 { none } else { let tail = parts.skip(n: 1).first() let line = split(s: tail, delimiter: "\n").first() - match (match split(s: line, delimiter: " ").first() { Absent => none Present { value: wire_field } => fnv1a64_structural_hex_digest(hex: wire_field) }) { - Present { value: structural } => - Present { value: as_content_hash_structural(structural: structural) } + match get(xs: split(s: line, delimiter: " "), index: 0) { Absent => none + Present { value: token } => + if token.length() == 0 { + none + } else { + Present { value: token as NonEmptyStr } + } } } } @@ -1205,11 +1207,11 @@ fn parse_serve_iso_observation(stdout: String, intent: OsInstallAttemptIntent) - IsoMissing } else { if string_contains(s: stdout, pattern: "iso=match") { - IsoPresent { observed_content_hash: intent.iso_content_hash } + IsoPresent { observed_sha256: intent.iso_sha256 } } else { if string_contains(s: stdout, pattern: "iso=mismatch") { - match parse_observe_content_hash_observed(stdout: stdout) { - Present { value: observed } => IsoPresent { observed_content_hash: observed } + match parse_observe_iso_sha256_observed(stdout: stdout) { + Present { value: observed } => IsoPresent { observed_sha256: observed } Absent => IsoMissing } } else { @@ -1397,13 +1399,3 @@ fn srv3_install_diagnostic_recommended_action_wire(verdict: Srv3InstallDiagnosti InconclusiveNeedsServeObservation => "collect actuator-host serve observation surfaces before next actuation" } } - -fn srv3_remote_bmc_probe_diagnostic_is_needs_serve_observation() -> Bool { - match diagnose_srv3_install( - intent: srv3_os_install_attempt_intent, - observation: srv3_remote_bmc_probe_observation, - ) { - InconclusiveNeedsServeObservation => true - _ => false - } -} diff --git a/dag/gunbc/srv3/srv3_seeded_install_media.dag b/dag/gunbc/srv3/srv3_seeded_install_media.dag index 6a4633b1362..cf6fa7ea4ff 100644 --- a/dag/gunbc/srv3/srv3_seeded_install_media.dag +++ b/dag/gunbc/srv3/srv3_seeded_install_media.dag @@ -2,11 +2,11 @@ module gunbc.srv3_seeded_install_media data gunbc_srv3_seeded_install_media_scope_disposition: Disposition = std.disposition.Terminal { - reason: "srv3 seeded autoinstall ISO — remaster stock ISO with embedded nocloud user-data + GRUB autoinstall ds=nocloud; content-hash keyed on stock sha256 + autoinstall payload; dissolves NoCloudNet seed-delivery gap when proven by unattended boot execution." + reason: "srv3 seeded autoinstall ISO — remaster stock ISO with embedded nocloud user-data + GRUB autoinstall ds=nocloud; published under the sha256 of the built bytes, found by consumers through a derivation record keyed on every build input; dissolves NoCloudNet seed-delivery gap when proven by unattended boot execution." } data srv3_seeded_install_media_remaster_scaffold: Disposition = std.disposition.Terminal { - reason: "srv3_seeded_install_media_remaster() routes through host_effect_apply to install_media_remaster_reconcile() in-process (TypedReconcileOnHost cell, dag/gunbc/host/host_effect_realize.dag) - no shell script is built, serialized, or run for this effect" + reason: "srv3_seeded_install_media_remaster() routes through host_effect_apply to srv3_seeded_install_media_publish() (build, create-only publish, derivation record) in-process (TypedReconcileOnHost cell, dag/gunbc/host/host_effect_realize.dag) - no shell script is built, serialized, or run for this effect" } data srv3_seeded_install_media_toolchain_ensure_scaffold: Disposition = std.disposition.Terminal { @@ -60,10 +60,7 @@ fn srv3_seeded_install_media_actuator_host_is_srv1() -> Bool { } fn srv3_seeded_install_media_install_path_is_versioned() -> Bool { - string_contains( - s: srv3_seeded_ubuntu_install_media_install_path as String, - pattern: "ubuntu-24.04.3-live-server-srv3-seeded.iso", - ) + string_contains(s: srv3_seeded_install_media_input.image_stem as String, pattern: "ubuntu-24.04.3-live-server-srv3-seeded") } fn srv3_seeded_autoinstall_delivery_is_on_iso() -> Bool { diff --git a/dag/gunbc/srv3/srv3_seeded_install_media_artifact.dag b/dag/gunbc/srv3/srv3_seeded_install_media_artifact.dag index aaf123cf728..a45b87a8107 100644 --- a/dag/gunbc/srv3/srv3_seeded_install_media_artifact.dag +++ b/dag/gunbc/srv3/srv3_seeded_install_media_artifact.dag @@ -1,17 +1,27 @@ module gunbc.srv3_seeded_install_media_artifact -import extdeps.provisioning.ubuntu_install_media { noble_numbat_2404_3_live_server_arm64 } +import extdeps.provisioning.ubuntu_install_media { + noble_numbat_2404_3_live_server_arm64, + ubuntu_install_media_artifacts_dir, + ubuntu_install_media_install_path, +} import extdeps.provisioning.ubuntu_seeded_install_media { - UbuntuSeededInstallMediaArtifactRow, + UbuntuSeededInstallMediaBuildInput, ubuntu_seeded_install_media_default_nocloud_dir, ubuntu_seeded_install_media_grub_kernel_cmdline, - seeded_install_media_content_hash, - ubuntu_seeded_install_media_install_path, } import gunbc.os_install { srv3_ubuntu_autoinstall_on_iso } import gunbc.os_install_emit { autoinstall_user_data } -import std.types { NonEmptyStr } -import std.content_hash { ContentHash } +import gunbc.seeded_install_media_policy { seeded_install_media_pinned_dates, seeded_install_media_builder_revision } +import gunbc.seeded_install_media_publish { + HostArtifactsDirectory, + SeededImagePublication, + SeededImageResolution, + SeededImageWriteBoundary, + publish_seeded_image, + resolve_seeded_image, +} +import std.types { List, NonEmptyStr } data srv3_autoinstall_user_data_body: NonEmptyStr = autoinstall_user_data( payload: srv3_ubuntu_autoinstall_on_iso, @@ -25,22 +35,34 @@ data srv3_seeded_grub_kernel_cmdline: NonEmptyStr = ubuntu_seeded_install_media_ nocloud_dir: srv3_seeded_nocloud_dir_on_iso, ) -data srv3_seeded_install_media_content_hash: ContentHash = seeded_install_media_content_hash( - stock_content_sha256: noble_numbat_2404_3_live_server_arm64.content_sha256 as NonEmptyStr, - autoinstall_user_data_body: srv3_autoinstall_user_data_body, - grub_kernel_cmdline: srv3_seeded_grub_kernel_cmdline, - nocloud_dir_on_iso: srv3_seeded_nocloud_dir_on_iso, -) - -data srv3_seeded_install_media_artifact: UbuntuSeededInstallMediaArtifactRow = UbuntuSeededInstallMediaArtifactRow { +// The install payload is srv3's autoinstall, so the image is an INSTALLER and keeps the label the +// earlier remaster gave it; nothing reads srv3's boot-medium label. +data srv3_seeded_install_media_input: UbuntuSeededInstallMediaBuildInput = UbuntuSeededInstallMediaBuildInput { stock_artifact: noble_numbat_2404_3_live_server_arm64, - autoinstall: srv3_ubuntu_autoinstall_on_iso, - autoinstall_user_data_body: srv3_autoinstall_user_data_body, - grub_kernel_cmdline: srv3_seeded_grub_kernel_cmdline, nocloud_dir_on_iso: srv3_seeded_nocloud_dir_on_iso, - content_hash: srv3_seeded_install_media_content_hash, + user_data_body: srv3_autoinstall_user_data_body, + seed_files: [], + grub_kernel_cmdline: srv3_seeded_grub_kernel_cmdline, + volume_id: "UBUNTU_SEEDED", + pinned_dates: seeded_install_media_pinned_dates, + image_stem: "ubuntu-24.04.3-live-server-srv3-seeded", + builder_revision: seeded_install_media_builder_revision, } -data srv3_seeded_ubuntu_install_media_install_path: NonEmptyStr = ubuntu_seeded_install_media_install_path( - artifact: srv3_seeded_install_media_artifact, -) +// The actuator host serves the image over its own NBD proxy from its artifacts directory, beside the +// stock medium it was derived from. +data srv3_seeded_install_media_boundary: SeededImageWriteBoundary = HostArtifactsDirectory { + dir: ubuntu_install_media_artifacts_dir(), +} + +fn srv3_seeded_install_media_publish() -> SeededImagePublication { + publish_seeded_image( + input: srv3_seeded_install_media_input, + stock_path: ubuntu_install_media_install_path(artifact: noble_numbat_2404_3_live_server_arm64), + boundary: srv3_seeded_install_media_boundary, + ) +} + +fn srv3_seeded_install_media_resolve() -> SeededImageResolution { + resolve_seeded_image(input: srv3_seeded_install_media_input, dir: ubuntu_install_media_artifacts_dir()) +} diff --git a/dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag b/dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag new file mode 100644 index 00000000000..5d6a77199b1 --- /dev/null +++ b/dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag @@ -0,0 +1,254 @@ +module test.claim.mtcollins1_census_image_local_wet + +import std.logic { Bool } +import std.types { String, NonEmptyStr, List } +import extdeps.exec.command { LocalExec } +import extdeps.posix.sh_invocation { posix_sh_program_command } +import extdeps.filesystem.filesystem_io { Filesystem } +import gunbc.command_runner { run_shell_command_observe } +import extdeps.shell.exec { ProcessObservation } +import gunbc.machine_intake_host_capture_envelope { + HostCaptureBound, + HostCaptureStage, + HostCaptureUnbound, + host_capture_bind, + host_capture_bound_edac_clean, + host_capture_bound_nproc_exact, + host_capture_bound_section_exit_zero, + host_capture_bound_workload_stable, + host_capture_census_stages, + host_capture_program, + host_capture_workload_command, +} +import gunbc.machine_intake_host_capture_historical_binding { + bindings_hold_against_capture, + census_historical_capture, +} +import std.content_hash { Sha256Hash } +import std.measure { byte_size_count } +import extdeps.shell +import extdeps.tools.sha256sum { Sha256FileDigest, Sha256FileDigestUnavailable, sha256sum_file_digest_via_shell } +import extdeps.provisioning.ubuntu_seeded_install_media { + ubuntu_seeded_install_media_built, + ubuntu_seeded_install_media_record_path, +} +import gunbc.machine_intake_boot_image_fetch { boot_image_published_path } +import gunbc.machine_intake_mtcollins1_census_image { mtcollins1_census_image_input } +import gunbc.seeded_install_media_publish { + SeededImageResolution, + SeededImageResolved, + SeededImageRecordedImageAbsent, + SeededImageRecordDiverged, + resolve_seeded_image, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE INHABITANCE HALF OF test.claim.mtcollins1_census_image (DESIGN §3: a supplied input owes one +// claim that the real producer emits that shape). The hermetic file hands the parser designed +// captures; these two claims run the real producers. The first reads the one capture a census host +// has actually written. The second executes the PROGRAM THE RENDERER WRITES under a real shell and +// hands its real output to the parser, so the route asserted is render -> run -> parse and not a +// string built from the same tokens the parser reads. +// +// It is local-wet for the reason the precedent in test.claim.spark_snapshot_verify_wrapper_local_wet +// gives: shell.Exec.RunArgv and Filesystem.Read carry no mock_response, and their subject here is the +// output of a real process and the bytes of a real file. Nothing leaves the host; there is no network. +// +// attempt=3 was written by the program this grammar reconstructs, so the grammar must read it on +// every axis the census verdict uses. A reconstruction that drifted from the real framing fails here. +// attempt=3's capture must BIND -- one begin, the matching end for the same machine and run -- and +// every verdict axis must read that bound envelope. This is the committed file, not a copy. +test fn the_attempt_three_capture_binds_and_reads_by_real_execution() -> Bool { + let read = Filesystem.Read(path: census_historical_capture.path as String) + read.success && match host_capture_bind(capture: read.content, machine: "mtcollins1") { + HostCaptureUnbound { reason: _ } => false + HostCaptureBound { envelope: e } => + host_capture_bound_nproc_exact(envelope: e, threads: "160") + && host_capture_bound_edac_clean(envelope: e, name: "edac-before") + && host_capture_bound_edac_clean(envelope: e, name: "edac-after") + && host_capture_bound_workload_stable(envelope: e) + && host_capture_bound_section_exit_zero(envelope: e, name: "boot-media") + } +} + +// THE HISTORICAL MAP IS CHECKED AGAINST THE BYTES, not by eye: every binding row's `original` must be +// the ARGV line the committed capture actually carries, every Exact row's command must BE that line, +// and the omitted section must be present in the file and absent from the roster. +test fn the_historical_binding_holds_against_the_committed_capture_by_real_execution() -> Bool { + let read = Filesystem.Read(path: census_historical_capture.path as String) + read.success && match host_capture_bind(capture: read.content, machine: "mtcollins1") { + HostCaptureUnbound { reason: _ } => false + HostCaptureBound { envelope: e } => bindings_hold_against_capture(envelope: e, stages: host_capture_census_stages) + } +} + +// THE PRODUCER IS CONSTRAINED BY THE CAPTURE'S COMMITTED IDENTITY, not by its name. The bindings +// above are statements about ONE artifact; read against a different file they would be statements +// about nothing, and the file sits in the worktree where anything may rewrite it. So the digest and +// the byte count the census observation committed are checked against the bytes on disk, and this +// is the claim that goes red on the brief's digest-disagreement refusal. It also makes the identity +// row load-bearing: before this, the row named a digest that nothing compared anything to. +test fn the_committed_capture_matches_its_recorded_identity_by_real_execution() -> Bool { + let path = census_historical_capture.path + let digest_holds = match sha256sum_file_digest_via_shell(path: path) { + Sha256FileDigestUnavailable { path: _, reason: _ } => false + Sha256FileDigest { digest: observed } => + match census_historical_capture.digest { + Sha256Hash(recorded) => (observed.hex as String) == (recorded.hex as String) + _ => false + } + } + let counted = run_shell_command_observe( + command: posix_sh_program_command(script: concat("printf '%s' \"$(wc -c < ", path as String, ")\"\n")), + transport: LocalExec, + ) + digest_holds + && counted.exit_code == 0 + && counted.stdout == (byte_size_count(b: census_historical_capture.byte_count) as String) +} + +// A roster whose stages print what the census stages print, cheaply: the nproc reading, mc0's counter +// lines, the workload's two result tokens, and one stage that FAILS so the exit framing is exercised +// on a nonzero status as well as zero. The halt returns, so running it halts nothing. +data census_round_trip_stages: List = [ + HostCaptureStage { name: "nproc", command: "echo 160" }, + HostCaptureStage { + name: "edac-before", + command: "printf '/sys/devices/system/edac/mc/mc0/ce_count == 0\\n/sys/devices/system/edac/mc/mc0/ue_count == 0\\n'", + }, + HostCaptureStage { name: "workload", command: "echo workload-write-rc=0; echo workload-digest-stable=yes" }, + HostCaptureStage { name: "boot-media", command: "echo 'it'\\''s quoted'; exit 3" }, +] + +test fn the_rendered_program_runs_and_its_output_parses_by_real_execution() -> Bool { + let program = host_capture_program( + machine: "witness", + console_device: "/dev/stdout", + stages: census_round_trip_stages, + halt_command: "true", + ) + let obs = run_shell_command_observe( + command: posix_sh_program_command(script: program as String), + transport: LocalExec, + ) + let out = obs.stdout + obs.exit_code == 0 + && match host_capture_bind(capture: out, machine: "witness") { + HostCaptureUnbound { reason: _ } => false + HostCaptureBound { envelope: e } => + host_capture_bound_nproc_exact(envelope: e, threads: "160") + && host_capture_bound_edac_clean(envelope: e, name: "edac-before") + && host_capture_bound_workload_stable(envelope: e) + && host_capture_bound_section_exit_zero(envelope: e, name: "boot-media") == false + } + && string_contains(s: out, pattern: "----EXIT boot-media: 3") + && string_contains(s: out, pattern: "\nit's quoted\n") +} + +fn census_image_path_in(dir: NonEmptyStr, digest: NonEmptyStr) -> NonEmptyStr { + boot_image_published_path( + export_dir: dir, + image_name: ubuntu_seeded_install_media_built(input: mtcollins1_census_image_input, output_digest: digest).image_name, + ) +} + +// THE RESOLVER'S THREE UNRESOLVED STATES KEEP THEIR REMEDIES APART, driven through the real resolver +// against a real directory. A record whose image was REMOVED resolves as absent (publish may rebuild +// into the free name), never as diverged; an image holding OTHER bytes under the recorded name is +// diverged (nothing is replaced); and the positive control -- the record naming the file's own +// measured digest -- resolves. The digest is read off the written file, not authored beside it. +test fn a_removed_image_is_absent_not_diverged_by_real_execution() -> Bool { + let dir = shell.Mktemp.Dir() + let d = dir.path as NonEmptyStr + let probe_path = concat(dir.path as String, "/probe") as NonEmptyStr + let wrote_probe = Filesystem.Write(path: probe_path as String, content: "census-image-bytes") + let digest = match sha256sum_file_digest_via_shell(path: probe_path) { + Sha256FileDigestUnavailable { path: _, reason: _ } => "unreadable" as NonEmptyStr + Sha256FileDigest { digest: x } => x.hex + } + let record = ubuntu_seeded_install_media_record_path(dir: d, input: mtcollins1_census_image_input) + let wrote_record = Filesystem.Write(path: record as String, content: concat(digest as String, "\n")) + let absent = match resolve_seeded_image(input: mtcollins1_census_image_input, dir: d) { + SeededImageRecordedImageAbsent { path: _, recorded: r } => r == digest + _ => false + } + let image = census_image_path_in(dir: d, digest: digest) + let wrote_other = Filesystem.Write(path: image as String, content: "some-other-bytes") + let diverged = match resolve_seeded_image(input: mtcollins1_census_image_input, dir: d) { + SeededImageRecordDiverged { path: _, recorded: _, observed: _ } => true + _ => false + } + let wrote_same = Filesystem.Write(path: image as String, content: "census-image-bytes") + let resolved = match resolve_seeded_image(input: mtcollins1_census_image_input, dir: d) { + SeededImageResolved { built: b, path: p } => b.output_digest == digest && p == image + _ => false + } + let cleaned = shell.Remove.RecursiveForce(path: dir.path) + dir.success && wrote_probe.success && wrote_record.success && wrote_other.success && wrote_same.success + && absent && diverged && resolved +} + +// THE WORKLOAD'S OWN FAILURE MODES, EXECUTED ON THE REAL RENDERED BODY. The token this stage emits is +// what the census verdict reads, so the body must not be able to emit it over a workload that hashed +// nothing. POSIX shell functions take precedence over PATH, so each case prefixes the UNMODIFIED body +// with stubs for the two tools it calls -- no 64 GiB is written and no real hashing happens, while +// every line of the body under test is the one the image carries. +// ONE SPAWN PER CASE. The stdout and the exit status are two fields of ONE observation, so reading +// them through two functions ran the program twice per case to learn what a single run already +// reported -- eight executions where four suffice. It also made the disagreeing case incoherent: +// that stub counts invocations, so a second spawn continued a sequence the first had started. +fn workload_case(stubs: String) -> ProcessObservation { + run_shell_command_observe( + command: posix_sh_program_command(script: concat(stubs, "\n", host_capture_workload_command() as String, "\n")), + transport: LocalExec, + ) +} + +// The token must be a WHOLE LINE, not a substring -- a body emitting "workload-digest-stable=yesX", +// or the token inside an argv echo, may not satisfy this. Captured stdout carries no trailing +// newline, so the capture is padded on BOTH sides before the line-delimited match. +fn workload_emitted_the_token(o: ProcessObservation) -> Bool { + string_contains(s: concat("\n", o.stdout, "\n"), pattern: "\nworkload-digest-stable=yes\n") +} + +data workload_stub_dd: String = "dd() { :; }; rm() { :; }; mkdir() { :; };" + +// Every named chunk hashes, both passes agree: the one case that may emit the token. +data workload_stub_complete: String = "sha256sum() { for f in \"$@\"; do echo \"deadbeef $f\"; done; };" + +// Both passes fail and print nothing. The two empty manifests AGREE -- this is the case a +// compare-only body calls stable. +data workload_stub_both_empty: String = "sha256sum() { return 1; };" + +// Both passes omit the same chunk and exit zero. The manifests agree again, over three files. +data workload_stub_partial: String = "sha256sum() { for f in \"$@\"; do case \"$f\" in *chunk3) continue;; esac; echo \"deadbeef $f\"; done; };" + +// Complete passes that DISAGREE: the genuine instability this stage exists to detect. +data workload_stub_disagree: String = "sha256sum() { n=$(cat /tmp/gunbc-wl-counter 2>/dev/null || echo 0); n=$((n+1)); echo $n > /tmp/gunbc-wl-counter; for f in \"$@\"; do echo \"dead$n $f\"; done; };" + +test fn the_workload_emits_its_token_only_on_a_complete_agreeing_pass_by_real_execution() -> Bool { + let complete = workload_case(stubs: concat(workload_stub_dd, workload_stub_complete)) + workload_emitted_the_token(o: complete) && complete.exit_code == 0 +} + +// THE THREE REDS. Each would be called STABLE by a body that only compares the two manifests: both +// empty agree, both partial agree, and the disagreeing case is the ordinary instability. None may +// emit the token, and each must leave the stage nonzero. +test fn a_failed_or_partial_or_disagreeing_workload_emits_no_token_by_real_execution() -> Bool { + let cleared = shell.Remove.FileForce(path: "/tmp/gunbc-wl-counter") + let empty = workload_case(stubs: concat(workload_stub_dd, workload_stub_both_empty)) + let partial = workload_case(stubs: concat(workload_stub_dd, workload_stub_partial)) + let cleared_again = shell.Remove.FileForce(path: "/tmp/gunbc-wl-counter") + let disagree = workload_case(stubs: concat(workload_stub_dd, workload_stub_disagree)) + // ABSENCE IS ASSERTED AS A SUBSTRING, not as a whole line: substring-absence is the STRONGER + // claim -- it also rules out the token appearing with anything appended -- and a RED may not be + // weakened to share a helper with the positive case. + string_contains(s: empty.stdout, pattern: "workload-digest-stable=yes") == false + && empty.exit_code != 0 + && string_contains(s: partial.stdout, pattern: "workload-digest-stable=yes") == false + && partial.exit_code != 0 + && string_contains(s: disagree.stdout, pattern: "workload-digest-stable=yes") == false + && disagree.exit_code != 0 +} diff --git a/dag/test/claim/machine_intake/mtcollins1_census_image_witness_test.dag b/dag/test/claim/machine_intake/mtcollins1_census_image_witness_test.dag new file mode 100644 index 00000000000..e5c6a595697 --- /dev/null +++ b/dag/test/claim/machine_intake/mtcollins1_census_image_witness_test.dag @@ -0,0 +1,376 @@ +module test.claim.mtcollins1_census_image + +import std.logic { Bool } +import std.types { Int, List, NonEmptyStr } +import std.string_type { String } +import extdeps.provisioning.ubuntu_seeded_install_media { + NoCloudSeedFile, + SeededInstallMediaPinnedDates, + UbuntuSeededInstallMediaBuildInput, + seeded_install_media_build_key, + ubuntu_seeded_install_media_built, +} +import gunbc.machine_intake_mtcollins1_boot_artifact { mtcollins1_diskless_image_name } +import gunbc.machine_intake_host_capture_envelope { + HostCaptureStage, + BoundHostCaptureEnvelope, + HostCaptureBound, + HostCaptureUnbound, + HistoricalAttemptOrdinal, + KernelBootId, + host_capture_bind, + host_capture_bound_edac_clean, + host_capture_bound_nproc_exact, + host_capture_bound_workload_stable, + host_capture_census_stages, + host_capture_line_close, + host_capture_run_identity_value, + host_capture_run_token, + host_capture_section_open, + host_capture_workload_chunk_count, + host_capture_workload_chunk_paths, + host_capture_workload_command, +} +import gunbc.machine_intake_host_capture_historical_binding { + census_historical_stage_bindings, + census_roster_is_fully_bound, + census_roster_matches_bindings, + census_stages_with_mutated_command, + roster_matches_bindings, +} +import gunbc.machine_intake_mtcollins1_census_image { + mtcollins1_census_capture_program, + mtcollins1_census_console_device, + mtcollins1_census_console_tty, + mtcollins1_census_image_input, + mtcollins1_census_machine, + mtcollins1_census_image_stem, + mtcollins1_census_program_file, + mtcollins1_census_volume_id, + mtcollins1_seeded_image_input, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data census_run_a: String = "boot-id=aaaa-1111" + +data census_run_b: String = "boot-id=bbbb-2222" + +fn census_marker(run: String, word: String) -> String { + concat("=====GUNBC-HOST-CAPTURE mtcollins1 ", run, " ", word, " 2026-09-19T00:00:00Z=====") +} + +data census_sections_body: String = "----SECTION nproc----\n----ARGV nproc: nproc\n160\n----EXIT nproc: 0\n----SECTION workload----\n----ARGV workload: echo workload-write-rc=$rc; [ \"$ok\" = 1 ] && echo workload-digest-stable=yes\nworkload-write-rc=0\nworkload-digest-stable=yes\n----EXIT workload: 0\n----SECTION edac-before----\n----ARGV edac-before: find\n/sys/devices/system/edac/mc/mc0/ce_count == 0\n/sys/devices/system/edac/mc/mc0/ue_count == 0\n----EXIT edac-before: 0" + +fn census_capture_one_run() -> String { + concat(census_marker(run: census_run_a, word: "begin"), "\n", census_sections_body, "\n", census_marker(run: census_run_a, word: "end"), "\n") +} + +fn census_binds(capture: String) -> Bool { + match host_capture_bind(capture: capture, machine: mtcollins1_census_machine) { + HostCaptureBound { envelope: _ } => true + HostCaptureUnbound { reason: _ } => false + } +} + +test fn one_run_binds_and_answers_every_verdict_axis() -> Bool { + match host_capture_bind(capture: census_capture_one_run(), machine: mtcollins1_census_machine) { + HostCaptureUnbound { reason: _ } => false + HostCaptureBound { envelope: e } => + host_capture_run_identity_value(run: e.run) == "aaaa-1111" + && host_capture_bound_nproc_exact(envelope: e, threads: "160") + && host_capture_bound_workload_stable(envelope: e) + && host_capture_bound_edac_clean(envelope: e, name: "edac-before") + } +} + +// THE CROSS-RUN JOIN, REFUSED. A begin from one run and an end from another is the failure the +// binding exists to prevent: each line is real, the sections between them are real, and the verdict +// they would compose never happened. +test fn a_begin_from_one_run_and_an_end_from_another_does_not_bind() -> Bool { + census_binds(capture: concat( + census_marker(run: census_run_a, word: "begin"), "\n", census_sections_body, "\n", + census_marker(run: census_run_b, word: "end"), "\n", + )) == false +} + +test fn an_end_before_a_begin_does_not_bind() -> Bool { + census_binds(capture: concat( + census_marker(run: census_run_a, word: "end"), "\n", census_sections_body, "\n", + census_marker(run: census_run_a, word: "begin"), "\n", + )) == false +} + +test fn an_unclosed_envelope_does_not_bind() -> Bool { + census_binds(capture: concat(census_marker(run: census_run_a, word: "begin"), "\n", census_sections_body, "\n")) == false +} + +// A SECTION OUTSIDE THE ENVELOPE IS NOT READ. The bound run's nproc says 160 and a later run's says +// 8; a verdict that searched the stream would find whichever it wanted. +test fn a_section_from_another_run_is_not_read() -> Bool { + let stream = concat( + census_marker(run: census_run_a, word: "begin"), "\n", census_sections_body, "\n", + census_marker(run: census_run_a, word: "end"), "\n", + census_marker(run: census_run_b, word: "begin"), "\n", + "----SECTION nproc----\n----ARGV nproc: nproc\n8\n----EXIT nproc: 0\n", + census_marker(run: census_run_b, word: "end"), "\n", + ) + match host_capture_bind(capture: stream, machine: mtcollins1_census_machine) { + HostCaptureUnbound { reason: _ } => false + HostCaptureBound { envelope: e } => + host_capture_bound_nproc_exact(envelope: e, threads: "160") + && host_capture_bound_nproc_exact(envelope: e, threads: "8") == false + } +} + +// THE SAME SPELLING IN TWO DIFFERENT IDENTITY FORMS IS NOT THE SAME RUN. attempt=3 is an ordinal +// the historical program printed; boot-id=3 would be a kernel boot id. They share a spelling and +// nothing else, so an envelope begun as one and ended as the other may not close -- comparing the +// projected text alone let exactly that through, which is the confusion the two forms exist to end. +test fn the_same_text_under_two_identity_forms_is_not_one_run() -> Bool { + census_binds(capture: concat( + census_marker(run: "attempt=3", word: "begin"), "\n", census_sections_body, "\n", + census_marker(run: "boot-id=3", word: "end"), "\n", + )) == false + && census_binds(capture: concat( + census_marker(run: "boot-id=3", word: "begin"), "\n", census_sections_body, "\n", + census_marker(run: "attempt=3", word: "end"), "\n", + )) == false +} + +// THE POSITIVE CONTROL for the claim above: the same spelling under the SAME form is one run and +// must bind, so the refusal above is the form discriminating and not the text failing to compare. +test fn the_same_text_under_one_identity_form_is_one_run() -> Bool { + census_binds(capture: concat( + census_marker(run: "attempt=3", word: "begin"), "\n", census_sections_body, "\n", + census_marker(run: "attempt=3", word: "end"), "\n", + )) + && census_binds(capture: concat( + census_marker(run: "boot-id=3", word: "begin"), "\n", census_sections_body, "\n", + census_marker(run: "boot-id=3", word: "end"), "\n", + )) +} + +// A NAME THAT OPENS TWICE REFUSES THE WHOLE CAPTURE rather than picking one: a later passing section +// must not be able to answer for an earlier failing one. +test fn a_duplicate_section_refuses_the_capture() -> Bool { + census_binds(capture: concat( + census_marker(run: census_run_a, word: "begin"), "\n", + "----SECTION nproc----\n----ARGV nproc: nproc\n8\n----EXIT nproc: 1\n", + "----SECTION nproc----\n----ARGV nproc: nproc\n160\n----EXIT nproc: 0\n", + census_marker(run: census_run_a, word: "end"), "\n", + )) == false +} + +test fn a_marker_without_a_run_identity_does_not_bind() -> Bool { + census_binds(capture: concat( + "=====GUNBC-HOST-CAPTURE mtcollins1 begin 2026-09-19T00:00:00Z=====\n", census_sections_body, "\n", + "=====GUNBC-HOST-CAPTURE mtcollins1 end 2026-09-19T00:04:00Z=====\n", + )) == false +} + +test fn the_envelope_is_bound_to_its_own_machine() -> Bool { + match host_capture_bind(capture: census_capture_one_run(), machine: "mtcollins2") { + HostCaptureUnbound { reason: _ } => true + HostCaptureBound { envelope: _ } => false + } +} + +// THE FALSE GREEN THE PROGRAM'S OWN ARGV LINE WOULD OPEN, ISOLATED TO ONE CONJUNCT. The section exits +// zero and carries `workload-write-rc=0` as a line, so every other conjunct holds; the passes +// DISAGREED, so the stable marker appears only inside the ARGV echo. Measured: relax the parser to +// substring matching and this claim FAILS. +test fn a_workload_token_in_the_argv_line_is_not_a_result() -> Bool { + let capture = concat( + census_marker(run: census_run_a, word: "begin"), "\n", + "----SECTION workload----\n----ARGV workload: echo workload-write-rc=$rc; [ \"$ok\" = 1 ] && echo workload-digest-stable=yes\nworkload-write-rc=0\n----EXIT workload: 0\n", + census_marker(run: census_run_a, word: "end"), "\n", + ) + match host_capture_bind(capture: capture, machine: mtcollins1_census_machine) { + HostCaptureUnbound { reason: _ } => false + HostCaptureBound { envelope: e } => host_capture_bound_workload_stable(envelope: e) == false + } +} + +// A stock boot of the same medium with no seed: firmware, kernel, then the installer's language menu. +data census_console_seed_absent_reaches_tui: String = "[ 0.000000] Kernel command line: BOOT_IMAGE=/casper/vmlinuz --- console=ttyAMA0,115200\n[ 17.340772] cloud-init: no datasource found\nWelcome!\nUse UP, DOWN and ENTER keys to select your language.\n[ English ]\n" + +test fn seed_absent_reaches_the_installer_and_never_binds() -> Bool { + census_binds(capture: census_console_seed_absent_reaches_tui) == false +} + +// THE HISTORICAL IDENTITY FORM IS KEPT APART FROM THE NEW ONE: attempt=3's ordinal parses as an +// ordinal, not as a boot id, so a reader can tell which producer wrote the capture. +test fn the_two_run_identity_forms_are_distinguished() -> Bool { + let historical = concat( + "=====GUNBC-HOST-CAPTURE mtcollins1 attempt=3 begin 2026-09-11T01:26:50Z=====\n", census_sections_body, "\n", + "=====GUNBC-HOST-CAPTURE mtcollins1 attempt=3 end 2026-09-11T01:30:29Z=====\n", + ) + match host_capture_bind(capture: historical, machine: mtcollins1_census_machine) { + HostCaptureUnbound { reason: _ } => false + HostCaptureBound { envelope: e } => + match e.run { + HistoricalAttemptOrdinal { ordinal: o } => (o as String) == "3" + KernelBootId { boot_id: _ } => false + } + } +} + +// EVERY HISTORICAL SECTION HAS EXACTLY ONE DISPOSITION AND EVERY ROSTER STAGE IS BOUND. +test fn every_stage_is_bound_to_the_historical_capture() -> Bool { + census_roster_is_fully_bound() + && census_roster_matches_bindings() + && census_historical_stage_bindings.length() == 19 +} + +// THE MAP REFUSES A MUTATED STAGE: editing a bound command away from what its row says makes the +// roster a different program from the one the binding describes. +test fn a_mutated_bound_stage_fails_the_historical_map() -> Bool { + roster_matches_bindings(stages: census_stages_with_mutated_command(name: "boot-media", command: "ls")) == false + && roster_matches_bindings(stages: census_stages_with_mutated_command(name: "nproc", command: "nproc --all")) == false + && roster_matches_bindings(stages: host_capture_census_stages) +} + +// THE WORKLOAD CANNOT MINT A CLEAN RESULT FROM TWO FAILED PASSES: the body requires both hash passes' +// statuses and each pass's coverage of every named chunk, not merely that the two lists agree -- two +// empty lists agree. Executed end to end in the local-wet file; this pins the shape. +test fn the_workload_requires_pass_status_and_full_coverage() -> Bool { + let body = host_capture_workload_command() as String + string_contains(s: body, pattern: "sa=$?") + && string_contains(s: body, pattern: "sb=$?") + && string_contains(s: body, pattern: "[ \"$sa\" = 0 ]") + && string_contains(s: body, pattern: "[ \"$sb\" = 0 ]") + && string_contains(s: body, pattern: concat("[ \"$na\" = ", host_capture_workload_chunk_count() as String, " ]")) + && string_contains(s: body, pattern: host_capture_workload_chunk_paths()) + && string_contains(s: body, pattern: "chunk*") == false +} + +test fn the_census_roster_is_the_brief_order() -> Bool { + join(map(host_capture_census_stages, s => s.name as String), ",") + == "subject-system,subject-baseboard,subject-chassis,boot-id,boot-cmdline,kernel,boot-media,nproc,lscpu,numa,meminfo,dmidecode-type16,dmidecode-type17,edac-mc-name,edac-find,edac-before,workload,edac-after" +} + +fn census_program_index(needle: String) -> Int { + let program = mtcollins1_census_capture_program(stages: host_capture_census_stages) as String + split(s: program, delimiter: needle).first().length() +} + +// HALT ONLY AFTER END. END is echoed after the last section, `sync` after END, and the power-off is +// the program's last line; nothing the census reads can follow the halt. +test fn the_program_writes_every_section_then_end_then_syncs_then_halts() -> Bool { + let program = mtcollins1_census_capture_program(stages: host_capture_census_stages) as String + let end_at = census_program_index(needle: concat(host_capture_run_token, "$GUNBC_BOOT_ID end")) + let last_section_at = census_program_index(needle: concat(host_capture_section_open, "edac-after", host_capture_line_close)) + let sync_at = census_program_index(needle: "\nsync\n") + let halt_at = census_program_index(needle: "\npoweroff -f\n") + string_contains(s: program, pattern: concat("exec >", mtcollins1_census_console_device as String, " 2>&1")) + && last_section_at < end_at + && end_at < sync_at + && sync_at < halt_at + && ends_with(s: program, suffix: "\npoweroff -f\n") +} + +// THE SEED RUNS THE PROGRAM BEFORE ANY INSTALLER SECTION AND LEAVES EVERY SECTION TO A HUMAN, so a +// failed power-off parks the unit at the installer rather than letting an unattended install touch +// storage. No storage or identity key is present for subiquity to act on. +test fn the_seed_runs_the_census_early_and_leaves_every_section_interactive() -> Bool { + let ud = mtcollins1_census_image_input.user_data_body as String + string_contains(s: ud, pattern: "#cloud-config") + && string_contains(s: ud, pattern: "early-commands:") + && string_contains(s: ud, pattern: "sh /cdrom/gunbc-census/capture.sh") + && string_contains(s: ud, pattern: "interactive-sections:") + && string_contains(s: ud, pattern: "*") + && string_contains(s: ud, pattern: "storage") == false + && string_contains(s: ud, pattern: "identity") == false +} + +// ONE TTY, BOTH SPELLINGS. The device the program writes to and the console the kernel is told to +// use derive from the same row, so they cannot be edited apart. +test fn the_boot_configuration_seeds_from_the_medium_on_the_serial_console() -> Bool { + let cmdline = mtcollins1_census_image_input.grub_kernel_cmdline as String + string_contains(s: cmdline, pattern: "autoinstall \"ds=nocloud;s=/cdrom/gunbc-census/\"") + && string_contains(s: cmdline, pattern: concat("console=", mtcollins1_census_console_tty as String, ",")) + && (mtcollins1_census_console_device as String) == concat("/dev/", mtcollins1_census_console_tty as String) + && mtcollins1_census_image_input.seed_files.length() == 1 + && (mtcollins1_census_image_input.volume_id as String).length() <= 32 +} + +fn census_input_varying( + program: NonEmptyStr, + grub_kernel_cmdline: NonEmptyStr, + volume_id: NonEmptyStr, + pinned_dates: SeededInstallMediaPinnedDates, + builder_revision: NonEmptyStr, +) -> UbuntuSeededInstallMediaBuildInput { + let base = mtcollins1_census_image_input + UbuntuSeededInstallMediaBuildInput { + stock_artifact: base.stock_artifact, + nocloud_dir_on_iso: base.nocloud_dir_on_iso, + user_data_body: base.user_data_body, + seed_files: [NoCloudSeedFile { name: mtcollins1_census_program_file, content: program }], + grub_kernel_cmdline: grub_kernel_cmdline, + volume_id: volume_id, + pinned_dates: pinned_dates, + image_stem: base.image_stem, + builder_revision: builder_revision, + } +} + +fn census_key_varying( + program: NonEmptyStr, + grub_kernel_cmdline: NonEmptyStr, + volume_id: NonEmptyStr, + pinned_dates: SeededInstallMediaPinnedDates, + builder_revision: NonEmptyStr, +) -> NonEmptyStr { + seeded_install_media_build_key(input: census_input_varying( + program: program, + grub_kernel_cmdline: grub_kernel_cmdline, + volume_id: volume_id, + pinned_dates: pinned_dates, + builder_revision: builder_revision, + )) +} + +// EVERY INPUT REACHES THE KEY. Reassembling the census input from its own fields reproduces the key +// (the positive control), and one byte of the capture program, the boot configuration, the label, +// either pinned date or the builder revision each move it, as does a roster with a stage appended. +// The key names the derivation; the image's identity is its measured bytes (next claim), and whether +// one changed input byte changes THOSE bytes is the wet build's control on srv2. +test fn every_build_input_reaches_the_derivation_key() -> Bool { + let base = mtcollins1_census_image_input + let program = mtcollins1_census_capture_program(stages: host_capture_census_stages) + let cmdline = base.grub_kernel_cmdline + let volid = base.volume_id + let dates = base.pinned_dates + let revision = base.builder_revision + let key = seeded_install_media_build_key(input: base) + key == census_key_varying(program: program, grub_kernel_cmdline: cmdline, volume_id: volid, pinned_dates: dates, builder_revision: revision) + && key != census_key_varying(program: concat(program as String, " ") as NonEmptyStr, grub_kernel_cmdline: cmdline, volume_id: volid, pinned_dates: dates, builder_revision: revision) + && key != census_key_varying(program: program, grub_kernel_cmdline: concat(cmdline as String, " quiet") as NonEmptyStr, volume_id: volid, pinned_dates: dates, builder_revision: revision) + && key != census_key_varying(program: program, grub_kernel_cmdline: cmdline, volume_id: "GUNBC_MTC1_CENSUS_OTHER", pinned_dates: dates, builder_revision: revision) + && key != census_key_varying(program: program, grub_kernel_cmdline: cmdline, volume_id: volid, pinned_dates: SeededInstallMediaPinnedDates { volume_uuid: "2025080615525001", file_date: dates.file_date }, builder_revision: revision) + && key != census_key_varying(program: program, grub_kernel_cmdline: cmdline, volume_id: volid, pinned_dates: SeededInstallMediaPinnedDates { volume_uuid: dates.volume_uuid, file_date: "2025.08.06.155251" }, builder_revision: revision) + && key != census_key_varying(program: program, grub_kernel_cmdline: cmdline, volume_id: volid, pinned_dates: dates, builder_revision: "xorriso-replay-pinned-dates/2") + && key != seeded_install_media_build_key(input: mtcollins1_seeded_image_input( + stages: append(host_capture_census_stages, items: [HostCaptureStage { name: "after-end", command: "true" }]), + image_stem: mtcollins1_census_image_stem, + volume_id: mtcollins1_census_volume_id, + )) +} + +test fn the_image_name_is_the_stem_and_the_measured_digest() -> Bool { + let a = ubuntu_seeded_install_media_built( + input: mtcollins1_census_image_input, + output_digest: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ) + let b = ubuntu_seeded_install_media_built( + input: mtcollins1_census_image_input, + output_digest: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", + ) + a.image_name == "gunbc-mtcollins1-census-ubuntu-24.04.3-arm64-0123456789abcdef.iso" + && a.image_name != b.image_name + && a.build_key == b.build_key + && a.image_name != mtcollins1_diskless_image_name +} diff --git a/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag b/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag index 5e728917d72..af3fb999576 100644 --- a/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag +++ b/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag @@ -3,6 +3,17 @@ module test.claim.nbd_proxy_virtual_media_install data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +// A SUPPLIED resolved image (DESIGN §3: a witness discriminates at one interface). What these +// claims check does not depend on which bytes the actuator serves, only on the path reaching the +// intent; the real resolution route is exercised by test.claim.mtcollins1_census_image. +data srv3_resolved_image_fixture: SeededImageResolution = SeededImageResolved { + built: ubuntu_seeded_install_media_built( + input: srv3_seeded_install_media_input, + output_digest: "abababababababababababababababababababababababababababababababab", + ), + path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso", +} + // The mapping is the subject here and it is unchanged: a boot target is derived // from the mechanism, never hardcoded to Pxe. What changed is that srv3 no // longer REACHES the mapping — its 3.22.00 evidence refuses before a mechanism @@ -24,7 +35,7 @@ test fn install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { // propagates and the plan refuses. A plan that survived the firmware change // would be the defect this witness exists to catch. test fn srv3_actuator_plan_is_solve_driven_and_follows_the_refusal() -> Bool { - match srv3_os_install_actuator_plan_standing { + match srv3_os_install_actuator_plan_standing(image: srv3_resolved_image_fixture) { ActuatorPlanRefused { cause: _ } => true ActuatorPlanReady { plan: _ } => false } @@ -63,6 +74,9 @@ test fn pxe_actuator_steps_stage_boot_restart() -> Bool { } } +// steps.first() is an Optional, so an ABSENT first step may not read as ready; the realization is +// taken against the resolved census image. Both facts are load-bearing and neither side of the +// merge that introduced them carried the other. test fn nbd_proxy_serve_step_maps_to_actuator_host_shell_intent() -> Bool { let steps = os_install_actuator_steps( mechanism: NbdProxyVirtualMediaInstall, @@ -71,7 +85,7 @@ test fn nbd_proxy_serve_step_maps_to_actuator_host_shell_intent() -> Bool { let first_step_ready = match steps.first() { Absent => false Present { value: first_step } => - match actuator_step_realization(step: first_step) { + match actuator_step_realization(step: first_step, image: srv3_resolved_image_fixture) { ActuatorStepReady { intent: intent } => match intent.target { HostOs { node: h } => @@ -108,19 +122,22 @@ test fn nbd_proxy_placeholder_scaffolds_carry_dissolve_on_markers() -> Bool { // needs no host effect" — so an install plan could report ready while omitting the very effects // that perform the install. test fn unrealized_actuator_steps_refuse_rather_than_reporting_absent() -> Bool { - match actuator_step_realization(step: ActuatorRedfishInsertMedia) { + match actuator_step_realization(step: ActuatorRedfishInsertMedia, image: srv3_resolved_image_fixture) { ActuatorStepRefused { cause: ActuatorStepRealizationAbsent { step: _ } } => true ActuatorStepRefused { cause: ActuatorStepActuatorHostUnsatisfiable { step: _, reason: _ } } => false + ActuatorStepRefused { cause: ActuatorStepImageUnresolved { step: _, reason: _ } } => false ActuatorStepReady { intent: _ } => false } - && match actuator_step_realization(step: ActuatorPxeNetworkBootStaging) { + && match actuator_step_realization(step: ActuatorPxeNetworkBootStaging, image: srv3_resolved_image_fixture) { ActuatorStepRefused { cause: ActuatorStepRealizationAbsent { step: _ } } => true ActuatorStepRefused { cause: ActuatorStepActuatorHostUnsatisfiable { step: _, reason: _ } } => false + ActuatorStepRefused { cause: ActuatorStepImageUnresolved { step: _, reason: _ } } => false ActuatorStepReady { intent: _ } => false } - && match actuator_step_realization(step: ActuatorFirmwareUpdate) { + && match actuator_step_realization(step: ActuatorFirmwareUpdate, image: srv3_resolved_image_fixture) { ActuatorStepRefused { cause: ActuatorStepRealizationAbsent { step: _ } } => true ActuatorStepRefused { cause: ActuatorStepActuatorHostUnsatisfiable { step: _, reason: _ } } => false + ActuatorStepRefused { cause: ActuatorStepImageUnresolved { step: _, reason: _ } } => false ActuatorStepReady { intent: _ } => false } } @@ -132,9 +149,49 @@ test fn plan_with_unrealized_steps_is_refused_and_names_them() -> Bool { steps: os_install_actuator_steps( mechanism: PxeHttpInstall, export: srv3_nbd_proxy_export_placeholder - ) + ), + image: srv3_resolved_image_fixture, ) { ActuatorPlanUnrealized { refusals: rs } => list_length(items: rs) == 1 ActuatorPlanRealized { intents: _ } => false } } + +fn nbd_serve_step_refuses_image(step: OsInstallActuatorStep, image: SeededImageResolution) -> Bool { + match actuator_step_realization_under(step: step, selection: ActuatorSelected { host: srv1_host }, image: image) { + ActuatorStepRefused { cause: ActuatorStepImageUnresolved { step: _, reason: _ } } => true + _ => false + } +} + +// THE SERVED PATH IS THE RESOLVED IMAGE'S, and an unresolved image REFUSES the serve step. Both +// directions under a satisfiable actuator selection, so the image is the only thing that differs: +// the resolved arm must carry exactly the fixture's path into the NBD intent, and each unresolved +// arm must refuse by name rather than serve a path of its own. +test fn nbd_serve_step_serves_the_resolved_image_and_refuses_an_unresolved_one() -> Bool { + let selection = ActuatorSelected { host: srv1_host } + match os_install_actuator_steps( + mechanism: NbdProxyVirtualMediaInstall, + export: srv3_nbd_proxy_export_placeholder + ).first() { + Absent => false + Present { value: step } => nbd_serve_step_claims_hold(step: step, selection: selection) + } +} + +fn nbd_serve_step_claims_hold(step: OsInstallActuatorStep, selection: OsInstallActuatorSelection) -> Bool { + let served = match actuator_step_realization_under(step: step, selection: selection, image: srv3_resolved_image_fixture) { + ActuatorStepReady { intent: i } => + match i.effect { + Srv3NbdProxyServe { intent: nbd, lease_key: _ } => + nbd.iso_path == "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso" + _ => false + } + ActuatorStepRefused { cause: _ } => false + } + served + && nbd_serve_step_refuses_image(step: step, image: SeededImageNotBuilt { record_path: "/var/lib/gunbc/artifacts/.x.built" }) + && nbd_serve_step_refuses_image(step: step, image: SeededImageRecordDiverged { path: "/var/lib/gunbc/artifacts/x.iso", recorded: "ab", observed: "cd" }) + && nbd_serve_step_refuses_image(step: step, image: SeededImageRecordedImageAbsent { path: "/var/lib/gunbc/artifacts/x.iso", recorded: "ab" }) + && nbd_serve_step_refuses_image(step: step, image: SeededImageUnobservable { reason: "record unreadable" }) +} diff --git a/dag/test/claim/os_install_actuator_selection_witness_test.dag b/dag/test/claim/os_install_actuator_selection_witness_test.dag index 20f7d843112..122d93b6e02 100644 --- a/dag/test/claim/os_install_actuator_selection_witness_test.dag +++ b/dag/test/claim/os_install_actuator_selection_witness_test.dag @@ -47,6 +47,15 @@ data unsatisfiable_actuator_selection_fixture: OsInstallActuatorSelection = Actu reason: "fixture: no candidate host satisfies the srv3 actuator requirement conjunction", } +// A SUPPLIED resolved image, so the actuator selection is the only thing here that can refuse. +data srv3_resolved_image_fixture: SeededImageResolution = SeededImageResolved { + built: ubuntu_seeded_install_media_built( + input: srv3_seeded_install_media_input, + output_digest: "abababababababababababababababababababababababababababababababab", + ), + path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso", +} + test fn unsatisfiable_selection_refuses_the_nbd_proxy_serve_step() -> Bool { match actuator_step_realization_under( step: os_install_actuator_steps( @@ -54,12 +63,14 @@ test fn unsatisfiable_selection_refuses_the_nbd_proxy_serve_step() -> Bool { export: srv3_nbd_proxy_export_placeholder, ).first(), selection: unsatisfiable_actuator_selection_fixture, + image: srv3_resolved_image_fixture, ) { ActuatorStepRefused { cause: c } => match c { ActuatorStepActuatorHostUnsatisfiable { step: _, reason: why } => string_contains(s: why, pattern: "fixture: no candidate host") ActuatorStepRealizationAbsent { step: _ } => false + ActuatorStepImageUnresolved { step: _, reason: _ } => false } ActuatorStepReady { intent: _ } => false } diff --git a/dag/test/claim/srv3/srv3_host_effect_apply_witness_test.dag b/dag/test/claim/srv3/srv3_host_effect_apply_witness_test.dag index 466506e5677..53ff2406f82 100644 --- a/dag/test/claim/srv3/srv3_host_effect_apply_witness_test.dag +++ b/dag/test/claim/srv3/srv3_host_effect_apply_witness_test.dag @@ -167,7 +167,7 @@ test fn witness_srv3_typed_reconcile_ssh_transport_refuses_typed() -> Bool { } test fn witness_srv3_nbd_proxy_apply_foreign_port_refuses_at_apply_time() -> Bool { - let effect = srv3_nbd_proxy_serve_host_effect() + let effect = srv3_nbd_proxy_serve_host_effect(iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso") match effect { Srv3NbdProxyServe { intent: i, lease_key: k } => match host_effect_nbd_proxy_serve_apply_with_observation( @@ -188,7 +188,7 @@ test fn witness_srv3_nbd_proxy_apply_foreign_port_refuses_at_apply_time() -> Boo } test fn witness_srv3_nbd_proxy_apply_stale_matching_drains_not_foreign_kill() -> Bool { - let effect = srv3_nbd_proxy_serve_host_effect() + let effect = srv3_nbd_proxy_serve_host_effect(iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso") match effect { Srv3NbdProxyServe { intent: i, lease_key: k } => { let binding = host_effect_nbd_proxy_serve_session_binding(intent: i, lease_key: k) @@ -214,7 +214,7 @@ test fn witness_srv3_nbd_proxy_apply_stale_matching_drains_not_foreign_kill() -> } test fn witness_srv3_nbd_proxy_websocat_argv_materializes_token_not_shell_env_ref() -> Bool { - let effect = srv3_nbd_proxy_serve_host_effect() + let effect = srv3_nbd_proxy_serve_host_effect(iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso") match effect { Srv3NbdProxyServe { intent: i, lease_key: _ } => host_effect_nbd_proxy_serve_websocat_argv_materialized_omits_shell_env_ref( @@ -234,7 +234,7 @@ test fn witness_srv3_nbd_proxy_websocat_argv_materializes_token_not_shell_env_re // genuinely wrong" are different facts, and the apply refusal reason names the transport, not a // fabricated "port inaccessible" string. test fn witness_srv3_nbd_proxy_apply_emit_artifact_transport_observe_refuses_fail_closed() -> Bool { - let effect = srv3_nbd_proxy_serve_host_effect() + let effect = srv3_nbd_proxy_serve_host_effect(iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso") match effect { Srv3NbdProxyServe { intent: i, lease_key: k } => host_effect_nbd_proxy_serve_apply_refuses_when_observe_unimplemented( @@ -249,7 +249,7 @@ test fn witness_srv3_nbd_proxy_apply_emit_artifact_transport_observe_refuses_fai } test fn witness_srv3_nbd_proxy_serve_session_binding_carries_lease() -> Bool { - let effect = srv3_nbd_proxy_serve_host_effect() + let effect = srv3_nbd_proxy_serve_host_effect(iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso") match effect { Srv3NbdProxyServe { intent: i, lease_key: k } => match host_effect_nbd_proxy_serve_session_binding(intent: i, lease_key: k) { @@ -266,7 +266,7 @@ test fn witness_srv3_nbd_proxy_serve_session_binding_carries_lease() -> Bool { test fn witness_srv3_nbd_proxy_on_bmc_refuses_typed() -> Bool { match host_effect_apply( target: srv3_bmc_target(), - effect: srv3_nbd_proxy_serve_host_effect(), + effect: srv3_nbd_proxy_serve_host_effect(iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso"), evidence: host_effect_identity_evidence, transport: LocalShell ) { diff --git a/dag/test/claim/srv3/srv3_os_install_actuate_witness_test.dag b/dag/test/claim/srv3/srv3_os_install_actuate_witness_test.dag index 261333f37fa..d327b9b9e41 100644 --- a/dag/test/claim/srv3/srv3_os_install_actuate_witness_test.dag +++ b/dag/test/claim/srv3/srv3_os_install_actuate_witness_test.dag @@ -3,26 +3,38 @@ module test.claim.srv3_os_install_actuate data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +// A SUPPLIED resolved image (DESIGN §3: a witness discriminates at one interface). What these +// claims check does not depend on which bytes the actuator serves, only on the path reaching the +// intent; the real resolution route is exercised by test.claim.mtcollins1_census_image. +data srv3_resolved_image_fixture: SeededImageResolution = SeededImageResolved { + built: ubuntu_seeded_install_media_built( + input: srv3_seeded_install_media_input, + output_digest: "abababababababababababababababababababababababababababababababab", + ), + path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso", +} + // Was "has three steps". The count only ever mattered as a proxy for the plan // being derived from the solved mechanism, and srv3's BMC upgrade now exercises // that derivation directly: the solve refuses, so the plan refuses with it. A // plan still offering three nbd-proxy steps for a host whose firmware does not // implement nbd-proxy is the failure this replaces. test fn srv3_actuator_plan_follows_the_solve_and_refuses() -> Bool { - srv3_os_install_actuator_plan_refuses_on_unestablished_path() - && match srv3_os_install_actuator_plan_standing { + srv3_os_install_actuator_plan_refuses_on_unestablished_path(image: srv3_resolved_image_fixture) + && match srv3_os_install_actuator_plan_standing(image: srv3_resolved_image_fixture) { ActuatorPlanRefused { cause: _ } => true ActuatorPlanReady { plan: _, intents: _ } => false } } test fn srv3_nbd_proxy_serve_intent_targets_vm_slot_index() -> Bool { - let intent = gunbc.srv3_nbd_proxy_serve_intent.srv3_nbd_proxy_serve_intent() + let intent = gunbc.srv3_nbd_proxy_serve_intent.srv3_nbd_proxy_serve_intent( + iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso", + ) intent.bmc_host == asrock_srv3_live_bmc_host && intent.ws_path == bmcweb_nbd_proxy_ws_path(variant: VmSlotIndexEndpoint, slot: 0, index: 0) && asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint == VmSlotIndexEndpoint - && intent.iso_path == srv3_actuator_virtual_media_iso_install_path - && string_contains(s: intent.iso_path as String, pattern: "srv3-seeded.iso") + && intent.iso_path == "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso" } test fn srv3_bmcweb_login_body_snapshot_uses_factory_when_unresolved() -> Bool { @@ -36,7 +48,7 @@ test fn srv3_actuate_prep_paths_are_declared() -> Bool { } test fn srv3_nbd_proxy_serve_argv_is_emittable_from_intent() -> Bool { - let intent = gunbc.srv3_nbd_proxy_serve_intent.srv3_nbd_proxy_serve_intent() + let intent = gunbc.srv3_nbd_proxy_serve_intent.srv3_nbd_proxy_serve_intent(iso_path: "/var/lib/gunbc/artifacts/fixture.iso") let websocat_argv = fold( nbd_proxy_serve_websocat_argv(intent: intent), init: "", @@ -54,7 +66,7 @@ test fn srv3_nbd_proxy_serve_argv_is_emittable_from_intent() -> Bool { } test fn nbd_proxy_serve_session_token_is_literal_in_websocat_argv() -> Bool { - let intent = gunbc.srv3_nbd_proxy_serve_intent.srv3_nbd_proxy_serve_intent() + let intent = gunbc.srv3_nbd_proxy_serve_intent.srv3_nbd_proxy_serve_intent(iso_path: "/var/lib/gunbc/artifacts/fixture.iso") let websocat_argv = fold( nbd_proxy_serve_websocat_argv(intent: intent), init: "", diff --git a/dag/test/claim/srv3/srv3_os_install_diagnostic_witness_test.dag b/dag/test/claim/srv3/srv3_os_install_diagnostic_witness_test.dag index 331e2ec4bbc..655b9bacd84 100644 --- a/dag/test/claim/srv3/srv3_os_install_diagnostic_witness_test.dag +++ b/dag/test/claim/srv3/srv3_os_install_diagnostic_witness_test.dag @@ -24,7 +24,7 @@ fn base_observations( }, virtual_media: BmcVirtualMediaObservation { inserted_or_connected: true, - image_or_endpoint: Present { value: srv3_actuator_virtual_media_iso_install_path }, + image_or_endpoint: Present { value: srv3_diagnostic_intent_fixture.iso_path }, connected_to_expected_port: true, bmc_session_valid: true, }, @@ -42,7 +42,7 @@ fn serve_ready_sol_silent_pre_boot_observations() -> OsInstallActuationObservati base_observations( serve_process: ServeProcessesPresent, serve_port: ServePortListening { port: extdeps.bmc.webui.nbd_proxy_serve.srv3_nbd_proxy_local_port, owner_matches_expected: true }, - iso: IsoPresent { observed_content_hash: srv3_seeded_install_media_content_hash }, + iso: IsoPresent { observed_sha256: "abababababababababababababababababababababababababababababababab" }, redfish_boot_enabled: BootOverrideDisabled, elapsed_ms: 0, sol: SolConsoleContentObservation { @@ -59,13 +59,25 @@ fn serve_ready_sol_silent_pre_boot_observations() -> OsInstallActuationObservati ) } +// A SUPPLIED built image: the diagnostic fold decides over the digest it is handed, and which bytes +// the actuator actually built is the resolution route's question, not this fold's. +data srv3_diagnostic_image_fixture: UbuntuSeededInstallMediaBuilt = ubuntu_seeded_install_media_built( + input: srv3_seeded_install_media_input, + output_digest: "abababababababababababababababababababababababababababababababab", +) + +data srv3_diagnostic_intent_fixture: OsInstallAttemptIntent = srv3_os_install_attempt_intent_for( + image: srv3_diagnostic_image_fixture, + iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-abababababababab.iso", +) + test fn serve_absent_is_not_ready_to_boot() -> Bool { let verdict = diagnose_os_install_actuation( - intent: srv3_os_install_attempt_intent, + intent: srv3_diagnostic_intent_fixture, observations: base_observations( serve_process: NbdkitAbsent, serve_port: ServePortAbsent, - iso: IsoPresent { observed_content_hash: srv3_seeded_install_media_content_hash }, + iso: IsoPresent { observed_sha256: "abababababababababababababababababababababababababababababababab" }, redfish_boot_enabled: BootOverrideDisabled, elapsed_ms: 0, sol: SolConsoleContentObservation { @@ -90,8 +102,8 @@ test fn wrong_iso_hash_refuses_serve_ready() -> Bool { process: ServeProcessesPresent, port: ServePortListening { port: extdeps.bmc.webui.nbd_proxy_serve.srv3_nbd_proxy_local_port, owner_matches_expected: true }, session: TokenPresent, - iso: IsoPresent { observed_content_hash: content_hash_of_value(value: "wrong-hash") }, - intent: srv3_os_install_attempt_intent, + iso: IsoPresent { observed_sha256: "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" }, + intent: srv3_diagnostic_intent_fixture, ) { IsoHashMismatch => true _ => false @@ -100,11 +112,11 @@ test fn wrong_iso_hash_refuses_serve_ready() -> Bool { test fn boot_override_consumed_without_install_evidence() -> Bool { let verdict = diagnose_os_install_actuation( - intent: srv3_os_install_attempt_intent, + intent: srv3_diagnostic_intent_fixture, observations: base_observations( serve_process: ServeProcessesPresent, serve_port: ServePortListening { port: extdeps.bmc.webui.nbd_proxy_serve.srv3_nbd_proxy_local_port, owner_matches_expected: true }, - iso: IsoPresent { observed_content_hash: srv3_seeded_install_media_content_hash }, + iso: IsoPresent { observed_sha256: "abababababababababababababababababababababababababababababababab" }, redfish_boot_enabled: BootOverrideDisabled, elapsed_ms: 120000, sol: SolConsoleContentObservation { @@ -132,7 +144,7 @@ test fn only_bmc_static_lease_is_not_os_installed() -> Bool { OnlyBmcLeasePresent => !install_diagnostic_verdict_is_os_installed( verdict: diagnose_os_install_actuation( - intent: srv3_os_install_attempt_intent, + intent: srv3_diagnostic_intent_fixture, observations: serve_ready_sol_silent_pre_boot_observations(), ), ) @@ -142,12 +154,12 @@ test fn only_bmc_static_lease_is_not_os_installed() -> Bool { test fn router_srv3_dynamic_lease_is_os_installed() -> Bool { let verdict = diagnose_os_install_actuation( - intent: srv3_os_install_attempt_intent, + intent: srv3_diagnostic_intent_fixture, observations: OsInstallActuationObservations { serve_process: ServeProcessesPresent, serve_port: ServePortListening { port: extdeps.bmc.webui.nbd_proxy_serve.srv3_nbd_proxy_local_port, owner_matches_expected: true }, bmc_session: TokenPresent, - iso_readiness: IsoPresent { observed_content_hash: srv3_seeded_install_media_content_hash }, + iso_readiness: IsoPresent { observed_sha256: "abababababababababababababababababababababababababababababababab" }, redfish_system: RedfishSystemBootObservation { power_state: On, boot_override_enabled: BootOverrideDisabled, @@ -184,12 +196,12 @@ test fn router_srv3_dynamic_lease_is_os_installed() -> Bool { test fn kvm_frozen_alone_does_not_dominate_router_success() -> Bool { let verdict = diagnose_os_install_actuation( - intent: srv3_os_install_attempt_intent, + intent: srv3_diagnostic_intent_fixture, observations: OsInstallActuationObservations { serve_process: ServeProcessesPresent, serve_port: ServePortListening { port: extdeps.bmc.webui.nbd_proxy_serve.srv3_nbd_proxy_local_port, owner_matches_expected: true }, bmc_session: TokenPresent, - iso_readiness: IsoPresent { observed_content_hash: srv3_seeded_install_media_content_hash }, + iso_readiness: IsoPresent { observed_sha256: "abababababababababababababababababababababababababababababababab" }, redfish_system: RedfishSystemBootObservation { power_state: On, boot_override_enabled: BootOverrideDisabled, @@ -225,11 +237,11 @@ test fn kvm_frozen_alone_does_not_dominate_router_success() -> Bool { test fn sol_silent_past_timeout_is_hung_or_inconclusive_not_success() -> Bool { let hung = diagnose_os_install_actuation( - intent: srv3_os_install_attempt_intent, + intent: srv3_diagnostic_intent_fixture, observations: base_observations( serve_process: ServeProcessesPresent, serve_port: ServePortListening { port: extdeps.bmc.webui.nbd_proxy_serve.srv3_nbd_proxy_local_port, owner_matches_expected: true }, - iso: IsoPresent { observed_content_hash: srv3_seeded_install_media_content_hash }, + iso: IsoPresent { observed_sha256: "abababababababababababababababababababababababababababababababab" }, redfish_boot_enabled: BootOverrideDisabled, elapsed_ms: srv3_install_hang_no_router_lease_ms, sol: SolConsoleContentObservation { @@ -251,7 +263,7 @@ test fn sol_silent_past_timeout_is_hung_or_inconclusive_not_success() -> Bool { test fn serve_ready_pre_boot_is_ready_to_boot() -> Bool { let verdict = diagnose_os_install_actuation( - intent: srv3_os_install_attempt_intent, + intent: srv3_diagnostic_intent_fixture, observations: serve_ready_sol_silent_pre_boot_observations(), ) install_diagnostic_verdict_is_ready_to_boot(verdict: verdict) @@ -260,12 +272,12 @@ test fn serve_ready_pre_boot_is_ready_to_boot() -> Bool { test fn boot_once_armed_is_boot_attempt_in_progress() -> Bool { let verdict = diagnose_os_install_actuation( - intent: srv3_os_install_attempt_intent, + intent: srv3_diagnostic_intent_fixture, observations: OsInstallActuationObservations { serve_process: ServeProcessesPresent, serve_port: ServePortListening { port: extdeps.bmc.webui.nbd_proxy_serve.srv3_nbd_proxy_local_port, owner_matches_expected: true }, bmc_session: TokenPresent, - iso_readiness: IsoPresent { observed_content_hash: srv3_seeded_install_media_content_hash }, + iso_readiness: IsoPresent { observed_sha256: "abababababababababababababababababababababababababababababababab" }, redfish_system: RedfishSystemBootObservation { power_state: On, boot_override_enabled: BootOverrideOnce, @@ -274,7 +286,7 @@ test fn boot_once_armed_is_boot_attempt_in_progress() -> Bool { }, virtual_media: BmcVirtualMediaObservation { inserted_or_connected: true, - image_or_endpoint: Present { value: srv3_actuator_virtual_media_iso_install_path }, + image_or_endpoint: Present { value: srv3_diagnostic_intent_fixture.iso_path }, connected_to_expected_port: true, bmc_session_valid: true, }, @@ -301,11 +313,11 @@ test fn boot_once_armed_is_boot_attempt_in_progress() -> Bool { test fn autoinstall_running_fixture() -> Bool { let verdict = diagnose_os_install_actuation( - intent: srv3_os_install_attempt_intent, + intent: srv3_diagnostic_intent_fixture, observations: base_observations( serve_process: ServeProcessesPresent, serve_port: ServePortListening { port: extdeps.bmc.webui.nbd_proxy_serve.srv3_nbd_proxy_local_port, owner_matches_expected: true }, - iso: IsoPresent { observed_content_hash: srv3_seeded_install_media_content_hash }, + iso: IsoPresent { observed_sha256: "abababababababababababababababababababababababababababababababab" }, redfish_boot_enabled: BootOverrideDisabled, elapsed_ms: 1800000, sol: SolConsoleContentObservation { @@ -328,3 +340,17 @@ test fn checklist_surfaces_and_runbook_are_declared() -> Bool { bmc_install_debug_checklist_surface_count_is_seven() && srv3_os_install_diagnostic_runbook_is_declared() } + +// A REMOTE BMC PROBE ALONE CANNOT JUDGE THE SERVE. srv3_remote_bmc_probe_observation is what the BMC +// answers when nothing has observed the actuator host: serve unknown, no SOL, no lease table. The fold +// must ask for that observation rather than reading the absence as a verdict about the install. This +// claim replaces a same-named production fn that no caller reached, so the reading now executes. +test fn a_remote_bmc_probe_alone_needs_the_serve_observation() -> Bool { + match diagnose_srv3_install( + intent: srv3_diagnostic_intent_fixture, + observation: srv3_remote_bmc_probe_observation, + ) { + InconclusiveNeedsServeObservation => true + _ => false + } +} diff --git a/dag/test/claim/srv3/srv3_seeded_install_media_real_execution_witness_test.dag b/dag/test/claim/srv3/srv3_seeded_install_media_real_execution_witness_test.dag index bd556fd7092..1bb33e2f77f 100644 --- a/dag/test/claim/srv3/srv3_seeded_install_media_real_execution_witness_test.dag +++ b/dag/test/claim/srv3/srv3_seeded_install_media_real_execution_witness_test.dag @@ -2,7 +2,7 @@ module test.claim.srv3_seeded_install_media_real_execution import std.logic { Bool } import std.types { String, NonEmptyStr } -import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_install_media_artifact } +import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_install_media_input } import extdeps.provisioning.ubuntu_seeded_install_media_remaster { install_media_remaster_ensure_grub_cmdline } import extdeps.shell import extdeps.filesystem.filesystem_io @@ -18,7 +18,7 @@ test fn install_media_remaster_ensure_grub_cmdline_inserts_by_real_execution() - let dir = shell.Mktemp.Dir() let grub_cfg = concat(dir.path as String, "/grub.cfg") as NonEmptyStr let wrote = Filesystem.Write(path: grub_cfg as String, content: grub_cfg_fixture_without_cmdline) - let cmdline = srv3_seeded_install_media_artifact.grub_kernel_cmdline + let cmdline = srv3_seeded_install_media_input.grub_kernel_cmdline let applied = install_media_remaster_ensure_grub_cmdline(grub_cfg: grub_cfg, cmdline: cmdline) let after = Filesystem.Read(path: grub_cfg as String) let idempotent_reapply = install_media_remaster_ensure_grub_cmdline(grub_cfg: grub_cfg, cmdline: cmdline) diff --git a/dag/test/claim/srv3/srv3_seeded_install_media_witness_test.dag b/dag/test/claim/srv3/srv3_seeded_install_media_witness_test.dag index 5dbfefa90ac..96fddee4602 100644 --- a/dag/test/claim/srv3/srv3_seeded_install_media_witness_test.dag +++ b/dag/test/claim/srv3/srv3_seeded_install_media_witness_test.dag @@ -13,9 +13,12 @@ test fn srv3_seeded_autoinstall_user_data_is_cloud_config() -> Bool { && ud == autoinstall_user_data(payload: srv3_ubuntu_autoinstall_on_iso) } -test fn srv3_seeded_install_media_content_hash_is_derived() -> Bool { - srv3_seeded_install_media_artifact.content_hash != "" - && srv3_seeded_install_media_artifact.stock_artifact.content_sha256 != "" +// The srv3 image is the same derivation as every other seeded image: its build key reaches the stock +// medium's pinned digest and the rendered autoinstall, and its identity is the measured output. +test fn srv3_seeded_install_media_input_is_the_shared_derivation() -> Bool { + srv3_seeded_install_media_input.stock_artifact.content_sha256 == noble_numbat_2404_3_live_server_arm64.content_sha256 + && srv3_seeded_install_media_input.user_data_body == srv3_autoinstall_user_data_body + && seeded_install_media_build_key(input: srv3_seeded_install_media_input) != "" } test fn srv3_seeded_install_media_path_and_delivery() -> Bool { @@ -25,30 +28,6 @@ test fn srv3_seeded_install_media_path_and_delivery() -> Bool { && srv3_seeded_install_media_actuator_host_is_srv1() } -test fn srv3_seeded_install_media_remaster_dissolution_trigger_is_terminal() -> Bool { - match install_media_remaster_bash_emit_dissolution_trigger { - Scaffold { dissolves_to: _, bind: _ } => false - Terminal { reason: _ } => true - } -} - -test fn install_media_remaster_probe_refusal_shape_doc_names_infra_failure() -> Bool { - string_contains(s: install_media_remaster_probe_refusal_shape_doc, pattern: "InstallMediaRemasterVerifyInfraFailure") - && string_contains(s: install_media_remaster_probe_refusal_shape_doc, pattern: "InstallMediaRemasterStale") - && string_contains(s: install_media_remaster_probe_refusal_shape_doc, pattern: "fail-closed") -} - -test fn install_media_remaster_scaffolds_dissolved() -> Bool { - match install_media_remaster_classification_scaffold { - Scaffold { dissolves_to: _, bind: _ } => false - Terminal { reason: _ } => true - } - && match install_media_remaster_chmod_access_upsert_scaffold { - Scaffold { dissolves_to: _, bind: _ } => false - Terminal { reason: _ } => true - } -} - test fn srv3_seeded_unattended_not_claimed_until_execution() -> Bool { srv3_seeded_install_media_unattended_not_proven_yet() } diff --git a/docs/runbooks/srv3-os-install-actuate.md b/docs/runbooks/srv3-os-install-actuate.md index eb0302b8b8e..4e5a0d45da7 100644 --- a/docs/runbooks/srv3-os-install-actuate.md +++ b/docs/runbooks/srv3-os-install-actuate.md @@ -26,9 +26,13 @@ what was deleted. `/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-arm64.iso` (grounded on Ubuntu 24.04.3 point release + cited sha256 from [cdimage SHA256SUMS](https://cdimage.ubuntu.com/releases/24.04/release/SHA256SUMS)). Do not hand-fetch; the fetch receipt is the prereq read-back. -2. **Seeded ISO remaster before serve** — step 0b produces the actuator virtual-media path - (`gunbc.srv3_os_install_actuate_scope.srv3_actuator_virtual_media_iso_install_path` → - `.../ubuntu-24.04.3-live-server-srv3-seeded.iso`). `srv3_nbd_proxy_serve` binds this authority, not the stock path. +2. **Seeded ISO remaster before serve** — step 0b builds and publishes the seeded image from + `gunbc.srv3_seeded_install_media_artifact` `srv3_seeded_install_media_input` (via + `gunbc.seeded_install_media_publish` `publish_seeded_image`). The image is named by the sha256 of its built bytes + (`.../ubuntu-24.04.3-live-server-srv3-seeded-.iso`) and a derivation record beside it + maps the build inputs to that digest. `srv3_nbd_proxy_serve` resolves the path through that record + (`srv3_seeded_install_media_resolve`), re-measuring the file first, and **refuses** to serve if the image is not + built, its bytes disagree with the record, or it cannot be read. It never serves the stock path. 3. **`srv3_nbd_proxy_serve` is a held session** — one-shot `host_effect_apply` reconciles `Srv3NbdProxyServe` via systemd transient units (nbdkit + websocat). Units keep serving after the command returns; stop them when install completes. Open a second terminal for boot-once. @@ -88,8 +92,8 @@ gunbc run --source-root dag \ ``` Expected stdout includes `InstallMediaRemasterToolchainReceipt: outcome=Present` or `outcome=Installed`, then -`InstallMediaRemasterReceipt:` with seeded ISO path -`/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded.iso`. +`install-media-remaster: built and published at` (or `already published at`) the seeded ISO path +`/var/lib/gunbc/artifacts/ubuntu-24.04.3-live-server-srv3-seeded-.iso` with its sha256. ## Runnable prep (no BMC side effects except login) diff --git a/src/v2/workflow/floor_route_gap.dag b/src/v2/workflow/floor_route_gap.dag index ae73ce2e34c..0b09ef28e0e 100644 --- a/src/v2/workflow/floor_route_gap.dag +++ b/src/v2/workflow/floor_route_gap.dag @@ -1338,8 +1338,51 @@ fn floor_route_gap_expectation_chunk_19() -> List { } } +// ALL SIX test.claim.mtcollins1_census_image_local_wet identities: the census envelope grammar read +// against the one capture a census host actually wrote, the historical stage map checked against those +// same committed bytes, the renderer's program executed under a real shell and parsed back, the image +// resolver driven against a real directory, and the two workload claims that execute the UNMODIFIED +// rendered workload body under stubbed tools -- the pair that makes workload-digest-stable=yes +// discriminating. Their first effects are Filesystem.Read, shell.Exec.RunArgv and shell.Mktemp.Dir, +// none mocked by the hermetic envelope. THE ROSTER IS THE WHOLE FILE: a claim omitted here runs on no +// lane at all, which is how three of these were previously unexecuted while the file looked enrolled. +// Enrolled as the triple chunk_06's header requires: this expectation, the WetScheduledClaim rows in +// v2.workflow.local_repo_wet_terminal local_repo_wet_schedule, and the gunbc.ci_layer_roots +// witness_exclusion_frontier row classifying the file LocalRepoWetLane. +fn floor_route_gap_expectation_chunk_20() -> List { + Cons { + head: FloorRouteGapExpectation { identity: "test.claim.mtcollins1_census_image_local_wet.the_attempt_three_capture_binds_and_reads_by_real_execution", operation: "Read", ground: NoMockResponse {} }, + tail: + Cons { + head: FloorRouteGapExpectation { identity: "test.claim.mtcollins1_census_image_local_wet.the_historical_binding_holds_against_the_committed_capture_by_real_execution", operation: "Read", ground: NoMockResponse {} }, + tail: + Cons { + head: FloorRouteGapExpectation { identity: "test.claim.mtcollins1_census_image_local_wet.the_committed_capture_matches_its_recorded_identity_by_real_execution", operation: "RunArgv", ground: NoMockResponse {} }, + tail: + Cons { + head: FloorRouteGapExpectation { identity: "test.claim.mtcollins1_census_image_local_wet.the_rendered_program_runs_and_its_output_parses_by_real_execution", operation: "RunArgv", ground: NoMockResponse {} }, + tail: + Cons { + head: FloorRouteGapExpectation { identity: "test.claim.mtcollins1_census_image_local_wet.a_removed_image_is_absent_not_diverged_by_real_execution", operation: "Dir", ground: NoMockResponse {} }, + tail: + Cons { + head: FloorRouteGapExpectation { identity: "test.claim.mtcollins1_census_image_local_wet.the_workload_emits_its_token_only_on_a_complete_agreeing_pass_by_real_execution", operation: "RunArgv", ground: NoMockResponse {} }, + tail: + Cons { + head: FloorRouteGapExpectation { identity: "test.claim.mtcollins1_census_image_local_wet.a_failed_or_partial_or_disagreeing_workload_emits_no_token_by_real_execution", operation: "RunArgv", ground: NoMockResponse {} }, + tail: + Empty {} + } + } + } + } + } + } + } +} + fn floor_route_gap_expectation_chunks() -> List> { - Cons { head: floor_route_gap_expectation_chunk_00(), tail: Cons { head: floor_route_gap_expectation_chunk_01(), tail: Cons { head: floor_route_gap_expectation_chunk_02(), tail: Cons { head: floor_route_gap_expectation_chunk_03(), tail: Cons { head: floor_route_gap_expectation_chunk_04(), tail: Cons { head: floor_route_gap_expectation_chunk_05(), tail: Cons { head: floor_route_gap_expectation_chunk_06(), tail: Cons { head: floor_route_gap_expectation_chunk_07(), tail: Cons { head: floor_route_gap_expectation_chunk_08(), tail: Cons { head: floor_route_gap_expectation_chunk_09(), tail: Cons { head: floor_route_gap_expectation_chunk_10(), tail: Cons { head: floor_route_gap_expectation_chunk_11(), tail: Cons { head: floor_route_gap_expectation_chunk_12(), tail: Cons { head: floor_route_gap_expectation_chunk_13(), tail: Cons { head: floor_route_gap_expectation_chunk_14(), tail: Cons { head: floor_route_gap_expectation_chunk_15(), tail: Cons { head: floor_route_gap_expectation_chunk_16(), tail: Cons { head: floor_route_gap_expectation_chunk_17(), tail: Cons { head: floor_route_gap_expectation_chunk_18(), tail: Cons { head: floor_route_gap_expectation_chunk_19(), tail: Empty {} } } } } } } } } } } } } } } } } } } } } + Cons { head: floor_route_gap_expectation_chunk_00(), tail: Cons { head: floor_route_gap_expectation_chunk_01(), tail: Cons { head: floor_route_gap_expectation_chunk_02(), tail: Cons { head: floor_route_gap_expectation_chunk_03(), tail: Cons { head: floor_route_gap_expectation_chunk_04(), tail: Cons { head: floor_route_gap_expectation_chunk_05(), tail: Cons { head: floor_route_gap_expectation_chunk_06(), tail: Cons { head: floor_route_gap_expectation_chunk_07(), tail: Cons { head: floor_route_gap_expectation_chunk_08(), tail: Cons { head: floor_route_gap_expectation_chunk_09(), tail: Cons { head: floor_route_gap_expectation_chunk_10(), tail: Cons { head: floor_route_gap_expectation_chunk_11(), tail: Cons { head: floor_route_gap_expectation_chunk_12(), tail: Cons { head: floor_route_gap_expectation_chunk_13(), tail: Cons { head: floor_route_gap_expectation_chunk_14(), tail: Cons { head: floor_route_gap_expectation_chunk_15(), tail: Cons { head: floor_route_gap_expectation_chunk_16(), tail: Cons { head: floor_route_gap_expectation_chunk_17(), tail: Cons { head: floor_route_gap_expectation_chunk_18(), tail: Cons { head: floor_route_gap_expectation_chunk_19(), tail: Cons { head: floor_route_gap_expectation_chunk_20(), tail: Empty {} } } } } } } } } } } } } } } } } } } } } } } fn floor_route_gap_expectations() -> List { diff --git a/src/v2/workflow/local_repo_wet_terminal.dag b/src/v2/workflow/local_repo_wet_terminal.dag index 4881c3a3c3d..b6c96c76d70 100644 --- a/src/v2/workflow/local_repo_wet_terminal.dag +++ b/src/v2/workflow/local_repo_wet_terminal.dag @@ -550,6 +550,48 @@ fn local_repo_wet_schedule() -> List { function: "observe_install_media_fetch_covers_absent_verified_and_mismatch_by_real_execution", expectation: ExpectedToHold {} }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.mtcollins1_census_image_local_wet", function: "the_attempt_three_capture_binds_and_reads_by_real_execution" }, + entry: "dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag", + function: "the_attempt_three_capture_binds_and_reads_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.mtcollins1_census_image_local_wet", function: "the_committed_capture_matches_its_recorded_identity_by_real_execution" }, + entry: "dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag", + function: "the_committed_capture_matches_its_recorded_identity_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.mtcollins1_census_image_local_wet", function: "the_historical_binding_holds_against_the_committed_capture_by_real_execution" }, + entry: "dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag", + function: "the_historical_binding_holds_against_the_committed_capture_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.mtcollins1_census_image_local_wet", function: "the_rendered_program_runs_and_its_output_parses_by_real_execution" }, + entry: "dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag", + function: "the_rendered_program_runs_and_its_output_parses_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.mtcollins1_census_image_local_wet", function: "a_removed_image_is_absent_not_diverged_by_real_execution" }, + entry: "dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag", + function: "a_removed_image_is_absent_not_diverged_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.mtcollins1_census_image_local_wet", function: "the_workload_emits_its_token_only_on_a_complete_agreeing_pass_by_real_execution" }, + entry: "dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag", + function: "the_workload_emits_its_token_only_on_a_complete_agreeing_pass_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.mtcollins1_census_image_local_wet", function: "a_failed_or_partial_or_disagreeing_workload_emits_no_token_by_real_execution" }, + entry: "dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag", + function: "a_failed_or_partial_or_disagreeing_workload_emits_no_token_by_real_execution", + expectation: ExpectedToHold {} + }, WetScheduledClaim { identity: WitnessIdentity { module_path: "test.claim.spark_snapshot_verify_wrapper_local_wet", function: "a_failing_verify_exits_nonzero_and_says_failed_by_real_execution" }, entry: "dag/test/claim/spark/spark_snapshot_verify_wrapper_local_wet_test.dag",