Imported from randomparity/iso-chain-loader (
AGENTS.md). Install upstream withnpx skills add randomparity/iso-chain-loader. Copyright stays with the author.
Repository Guidelines
Project Overview
This repository builds and exercises a bounded ppc64le (POWER9) optical bootstrap chain: a
powerpc-ieee1275 GRUB ISO whose menu hands off to a dracut/systemd launcher that configures
static IPv4, downloads and SHA-256-verifies a fixed artifact set, and kexecs into either the
Fedora 44 text installer (Anaconda), with a Kickstart read from the ISO after the launcher verified
its digest, the Rocky Linux 9.8 text installer (interactive without a Kickstart, ADR 0013, or
unattended from a Kickstart build derives from the manifest's SSH keys and login user, ADR 0019),
the interactive openSUSE Leap 15.6 installer (linuxrc and YaST, ADR 0014), or the Ubuntu 26.04.1
live-server installer (casper/subiquity, ADR 0012; unattended from ISO-root cloud-init user data
build derives from the manifest's SSH keys and login user, ADR 0020).
Two properties dominate every design decision:
- No fallbacks. There is no DHCP, no IPv6, no alternate profile, no alternate source, no HTTP redirect following, and no credential use. A run either proves the declared path or fails.
- Evidence over assertion. Success is only claimed through canonical record files that bind SHA-256 digests of console logs, HTTP access logs, packet captures, and disk images.
Manifest v3 was proven under QEMU pSeries/POWER9, and the v4 Fedora install's QEMU proof is
docs/experiments/2026-10-03-fedora-v4-qemu-install.md. The
Ubuntu profile's QEMU proof is docs/experiments/2026-10-02-ubuntu-installer.md, and the Rocky
profile's is docs/experiments/2026-10-02-rocky-installer.md, with its unattended install in
docs/experiments/2026-10-02-rocky-unattended-install.md, and the unattended Ubuntu install's is
docs/experiments/2026-10-03-ubuntu-unattended-install.md; the openSUSE profile's is
docs/experiments/2026-10-02-opensuse-installer.md. One authorized
PowerVM POWER9 install is recorded in docs/experiments/2026-10-01-powervm-iso-carried-kickstart.md.
HMC/VIOS orchestration belongs to issue #6, and firmware security remains separate work.
Authenticated FTP sources are not accepted yet: ADR 0016 decides how their credential travels,
docs/experiments/2026-10-02-authenticated-ftp-sources.md records each installer's emulator
result, and issue #37 owns the implementation.
Architecture & Data Flow
One stdlib-only Python CLI drives preparation, construction, execution, and verification.
graph LR
N[signed netinst ISO + mirror .treeinfo] --> P[prepare-fedora-source]
M[manifest v4 JSON] -->|canonical bytes + sha256| ISO[launcher.iso]
K[kernel + dracut initramfs] --> ISO
P -->|Kickstart| ISO
ISO -->|GRUB menu, 5s timeout| L[iso-chain-launch.sh]
ISO -->|optical media, exact size + sha256| L
R[HTTPS Fedora repository] -->|pinned kernel + initrd + .treeinfo + repomd.xml| L
L -->|kexec| A[Fedora Anaconda, Kickstart from the labelled ISO]
A --> E[console / access log / pcap / disk hashes]
E --> V[verify-* evidence validators]
Stages, in order:
- Manifest v4 (
--config) is the single source of truth. Exactly six required top-level fields,version,lpar,network,source,profiles,selected_profile, plus an optionaloperation_binding(32 lower-case hex digits, ADR 0015) and an optional pair,ssh_authorized_keysandlogin_user, thatbuildrefuses until a profile applies them (ADR 0017). Manifests and target requests are at most 2 MiB.build --target --base-configcomposes one from aniso-chain-target-v1request and an operator base manifest holdingversion,source, andprofiles, keeping only the requested profile.versionmust be the integer4(a v3 manifest is rejected with a regeneration hint); a profile isfedora/44,opensuse/15.6,rocky/9.8, orubuntu/26.04.1, each with its own exact field set, and the selected profile must exist in the map. A Fedora profile pins the netinstvmlinuzandinitrd.imgby URL path undersource, and names its Kickstart as an ISO media path/profiles/<dir>/<file>. A Rocky profile has Fedora's fields minuskickstart, and its repository path must end in/BaseOS/ppc64le/os; withssh_authorized_keysandlogin_userpresent,buildrenders its Kickstart fromassets/kickstart/rocky-9.8-unattended.ksand the manifest, stages it as/profiles/<profile>/ks.cfg, and binds it like a Fedora Kickstart, and refuses those values unless every profile is Rocky (ADR 0019). An Ubuntu profile pins the netbootlinuxandinitrdand names thelive_isothat casper fetches; a manifest carrying one allows only the default route and at most two DNS servers. With login values and every profile Ubuntu,buildrenders/user-data(cloud-init autoinstall, JSON after#cloud-config, fixed parts fromassets/autoinstall/ubuntu-26.04.1.json) and an empty/meta-dataat the ISO root and binds the user data's size and digest (ADR 0020). An openSUSE profile pins the repository'sboot/ppc64le/linuxandinitrdand names only arepository.path; a manifest carrying one allows only the default route and at most one DNS server. - Canonicalization.
load_manifest_bytes()emitsjson.dumps(..., ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"and returns its SHA-256. That digest is embedded in the ISO and bound into every kernel argument. - Preparation.
prepare-initramfsruns dracut withassets/dracut/assets (container-prepare-initramfsruns it in alinux/ppc64lecontainer on other hosts);prepare-fedora-sourceverifies the signed Fedora 44 netinst ISO digest, binds a copied mirror.treeinfoto it throughimages/boot.iso, extracts and checks the kernel and initrd to pin their mirror copies, and writesprofile.jsonfor pasting into a private manifest (ADR 0011);prepare-rocky-sourcedoes the same from the signed Rocky 9.8 boot ISO and BaseOS tree, with no Kickstart (ADR 0013);prepare-ubuntu-sourcedoes the same from the signed Ubuntu live-server ISO, publishing the extracted netboot kernel and initrd besideprofile.json(ADR 0012);prepare-opensuse-sourcepins the kernel and initrd from a tree and itsgpgv-verifiedCHECKSUMS, with no ISO (ADR 0014). - Construction.
buildstages/iso-chain/config.json,/boot/vmlinuz,/boot/initramfs.img,/boot/grub/grub.cfg, and every profile's digest-checked Kickstart from--profiles, then callsgrub2-mkrescuewith the volume IDISO_CHAIN_<first 16 digest hex>. GRUB usesset timeout=5andset default="<selected_profile>", unless its top-level search finds agrubenvin/grub2,/boot/grub2,/grub, or/boot/grub; then theinstalled diskentry, oneconfigfileof that directory'sgrub.cfg, is the default (ADR 0018); a keyed Ubuntu ISO also requiresiso_chain_installed=1in thatgrubenv(ADR 0020). The kernel command line carries every profile's paths, sizes, and digests plusipv6.disable=1andrd.systemd.unit=iso-chain.target, and must stay under 2,048 bytes. It is held in a top-leveliso_chain_args_<n>variable so each menu entry stays under the 1,024 bytes Fedora's GRUB can replay after a PowerVM CAS reboot.buildwrites--output, or links<iso_sha256>.isointo--publish-dir(bound manifests only), and for a one-profile ISO prints one canonicaliso-chain-media-v1result line on stdout;inspect --resultprints it for an unbound one-profile ISO. - Execution.
smokeboots with a disposable snapshot overlay and stops before installation;install-fedoracreates a fresh standalone qcow2, installs, then boots the disk with no ISO and no NIC;install-rockyandinstall-ubuntuinstall with-no-reboot, then boot the disk with the ISO and NIC still attached until the console shows<lpar> login:. - Verification.
verify-log,verify-pcap,verify-launcher-log,verify-installer-evidence,verify-fedora-install-evidence,verify-rocky-install-evidence, andverify-ubuntu-install-evidencere-derive claims from canonical evidence records.
Core code patterns
- Frozen dataclass domain model:
NetworkConfig,Artifact,Repository,InstallerProfile,Manifest.Manifest.profile()allowlists lookups; nothing is mutable after parsing. - Hand-written strict validation, no schema library.
_object_pairsrejects duplicate JSON keys,_manifest_objectenforces exact field sets, and small typed validators (_string,_integer,_sha256,_identifier,_ipv4_address,_url_path,_validate_network,_validate_routes,_validate_dns) enforce grammar and bounds. - One exception type.
ValidationError(ValueError)carries a user-facing message;main()printserror: <message>to stderr and returns exit code2. UncaughtOSErrorreturns1; a failed child returns its own code. Invalid input must never echo private data. - No-replace, no-symlink filesystem policy. Outputs use
open("xb"),os.openwithO_EXCL/O_NOFOLLOW, hard-link publication, and_publish_directory()calling libcrenameat2(..., RENAME_NOREPLACE)on Linux orrenamex_np(..., RENAME_EXCL)on Darwin throughctypes. Every artifact path must be a real regular file that does not already exist. - Bounded reads.
MAX_*constants cap manifests, command lines, treeinfo, Kickstart, logs, and install captures; anything over the limit is a validation failure, not a truncation. - Testable seams. No DI framework: functions take
Path,Manifest,argparse.Namespace, or rawbytesexplicitly (load_manifest_bytes,_kernel_arguments,_grub_config,qemu_command,install_qemu_commands,verify_launcher_log,verify_pcap). - No logging module. Results go to
stdoutviaprint(); evidence markers use fixed strings such asISO_CHAIN_EVIDENCE:andPASS_LINES.
Key Directories
scripts/—iso_chain.py, the entire CLI (build, prepare, serve, run, verify).tests/— stdlibunittestsuite plus a Bash launcher black-box test.assets/dracut/— guest launcher:iso-chain-launch.sh,iso-chain-launch.service,iso-chain.target.assets/kickstart/—fedora-44-power9.ks, the reference unattended installation fixture.assets/autoinstall/—ubuntu-26.04.1.json, the fixed unattended Ubuntu autoinstall keys.docs/adr/— twenty accepted, binding ADRs (0001–0020).docs/workflow/specs/anddocs/workflow/plans/— datedYYYY-MM-DD-<slug>.mddesign contracts and implementation plans; a spec and its plan share a date and slug.docs/experiments/— dated emulator evidence records with explicit boundaries.docs/solutions/— dated durable solution records (front matter plus Problem / Root cause / Solution / Prevention).
Development Commands
Host prerequisites: macOS arm64 or x86_64 Linux, Python 3.14, just >= 1.57, and uv 0.12.12 or a
compatible release. Everything runs through just.
just setup # .venv + hash-locked tools + Git hook
just check # aggregate CI checks (non-mutating)
just fix # ruff/rumdl fixes, then just check
just check-tests # shell test, then unittest discovery
just build-image # ISO build image (podman, else docker)
.venv/bin/pre-commit run --all-files # run every configured hook directly
.venv/bin/python -m unittest -v tests.test_iso_chain.ManifestV3Tests # single test class
bash tests/test_iso_chain_launch.sh # shell launcher test alone
just check runs, in order: check-justfile, check-whitespace, check-python-lint,
check-python-format, check-tests, check-markdown, check-secrets.
Subcommands of scripts/iso_chain.py: build, container-build, inspect, prepare-initramfs,
container-prepare-initramfs, prepare-fedora-source, prepare-rocky-source,
prepare-opensuse-source, prepare-ubuntu-source, serve-source,
validate-external-source, smoke, install-fedora, install-rocky, install-ubuntu,
verify-log, verify-pcap, verify-launcher-log, verify-installer-evidence,
verify-fedora-install-evidence, verify-rocky-install-evidence,
verify-ubuntu-install-evidence. Every command
prints argparse-generated help only; see README.md for a full worked sequence of every stage.
Code Conventions & Common Patterns
- Formatting: ruff, line length 100,
target-version = "py314"inpyproject.toml. No custom lint rules, no Black, no type checker, no coverage tool. Markdown is linted by rumdl (also line length 100; code blocks and tables exempt). - Naming:
snake_casefunctions,PascalCaseclasses and dataclasses,_-prefixed internal helpers,UPPER_CASEmodule constants (MAX_LOG_BYTES,PASS_LINES,MAX_INSTALLER_ISO_BYTES). - Typing: annotate every function; use modern unions (
str | None,Path | None) and@dataclass(frozen=True)for value objects. - Imports: standard library only in
scripts/; no third-party Python runtime dependency. - Error handling: raise
ValidationErrorwith a message phrased for the operator; translate malformed input, missing paths, bad evidence, and subprocess/network failures before they escape. - Validate before acting: reject non-canonical forms (MAC casing, IPv4 CIDR, URL paths), and prove inputs invalid before spawning any external command.
- Determinism: canonical JSON, sorted output, dracut
--reproducible, and--no-cacheon all checks. Re-running a check must never touch a repository file. - Security invariants: public origins require HTTPS (HTTP is for loopback and controlled test servers); redirects, credentials, query strings, and fragments are rejected; artifact size and SHA-256 must match the manifest exactly.
- Private data: manifests, media, source trees, logs, access logs, disk hashes, and packet
captures can carry machine or network identifiers. Keep them in private storage, use
umask 077and fresh paths per run, and never commit them.
Important Files
scripts/iso_chain.py— entry point;parser()andmain()are the only dispatch boundary.assets/dracut/iso-chain-launch.sh— guest-side contract: strictiso_chain.*argument parsing, exact-MAC selection, static IPv4, capacity checks, thedisk: passedguard requiring exactly one non-optical disk whose first and last MiB are zero, mounting the one optical device whose/iso-chain/config.jsonmatchesiso_chain.config_sha256, the verified media Kickstart, pinned kernel, initrd, and metadata downloads,inst.ks=cdrom:LABEL=...,kexec -l,kexec -e.assets/kickstart/fedora-44-power9.ks— Fedora 44 fixture; destroys only/dev/vdaand writes theinstalled-boot: passed boot_id=...completion marker.assets/kickstart/fedora-44-powervm.ks— the same installation for a PowerVM partition's single vSCSI disk,/dev/sda;InstallTestsholds it identical to the reference apart from the disk.assets/kickstart/rocky-9.8-unattended.ks— the unattended Rocky templatebuildappends to the rendered login and network lines; its%prerepeats the blank-disk guard and partitions only the disk it counted;RockyKickstartTestsholds its structure.assets/autoinstall/ubuntu-26.04.1.json— the unattended Ubuntu autoinstall keysbuildmerges with the rendered network and login: offline apt, theearly-commandsblank-disk guard, and thelate-commandscompletion marker;UbuntuUserDataTestsholds its structure.Justfile— source of truth for every check, setup, and fix command.pyproject.toml— ruff and rumdl configuration; note there is no[project]table..pre-commit-config.yaml,.githooks/pre-commit— six local hooks that delegate to focusedjustrecipes;just setupinstalls the launcher into the resolved Git hooks path..github/workflows/checks.yml— onechecksjob whose matrix runs the samejust setupandjust checkonubuntu-latestandmacos-latest, with a pinned uv action instead of a separate Python setup step.docs/adr/0003,0004,0005,0006,0007— the binding choices for GRUB+kexec bootstrap, the dracut launcher, the verified initramfs bundle (withdrawn by 0011), manifest v3, and external-source validation.docs/adr/0008,0009,0010— the macOS build container, the uv development environment, and portable no-replace publication.docs/adr/0011— ISO-carried installer artifacts, manifest v4, and the signed netinst anchor.docs/adr/0012,0013,0014— the Ubuntu casper handoff, the Kickstart-free Rocky Anaconda handoff, and the openSUSE linuxrc handoff.docs/adr/0015— target requests, theoperation_binding, digest-named publication, and theiso-chain-media-v1producer result for hmcpctl.docs/adr/0016— authenticated FTP credentials as source URL userinfo, its exposure and mitigations; implementation belongs to issue #37.docs/adr/0017— SSH keys and the login user as manifest fields, carried only in the digest-bound/iso-chain/config.json.docs/adr/0018— the launcher menu's installed-disk default and the launcher's blank-disk guard before any installer handoff.docs/adr/0019— the unattended Rocky install from a build-derived Kickstart.docs/adr/0020— the unattended Ubuntu install from ISO-root user data that cloud-init's NoCloud reads through a kernel-command-linefs_label, and itsgrubenvcompletion marker.docs/workflow/specs/2026-10-01-iso-carried-artifacts-design.md— current contract for the manifest, preparation, launcher media, and the public repository path.docs/solutions/2026-09-10-stream-subprocess-evidence-before-eof.md— the solution-record format to follow when capturing a non-obvious fix.
Runtime/Tooling Preferences
- Python 3.14 is required (
.python-version, CIpython-version: "3.14").pyproject.tomldeclares norequires-python; the version file and lock are authoritative. - No runtime dependencies. Development tools are pinned in
requirements-dev.inand installed from the hash-lockedrequirements-dev.lockwithuv pip install --require-hashes:pre-commit==4.6.2,ruff==0.16.6,rumdl==0.2.66,detect-secrets==1.5.0. The lock carries macOS arm64 artifacts as well as Linux x86_64 ones, and uv supplies CPython 3.14 when the host does not. Regenerating the lock is a separate review action. justowns the command surface. Focused recipes are invoked by both the local pre-commit hooks and CI; never duplicate a check command line in a hook or workflow (ADR 0001).- Checks are read-only. Only
just fixmutates files, and it re-runs the full check afterwards..secrets.baselinerecords the line numbers of its false positives, so a change that grows or shrinks a file it covers must refresh those numbers in the same change — otherwisecheck-secretsrewrites its disposable baseline copy and fails. Adding or removing a recorded finding remains a separate review action, never part of check or fix. - Target build tooling is not installed by
just setup:buildneedsgrub2-mkrescueandxorriso—container-buildruns it inside the pinnediso-chain-builder:44image, choosingpodmanwhen it is onPATHanddockerotherwise, which is how macOS builds the ISO —prepare-fedora-sourceneedsxorriso(the builder image has it),prepare-initramfsneeds a ppc64le host withdracutand/usr/lib/modules/<ver>, whichcontainer-prepare-initramfssupplies through the emulatediso-chain-initramfs:44image fromContainerfile.initramfs(just build-initramfs-image), and the run/verify commands needqemu-system-ppc64,qemu-img,cpio,xz, andtcpdump. - CI does not build or boot media. Bootable-media validation requires a ppc64le emulator or real ppc64le hardware and is deliberately out of the automated pipeline.
Testing & QA
- Frameworks: stdlib
unittest(tests/test_iso_chain.py, twenty-six test classes such asManifestV4Tests,BuildTests,RockyKickstartTests,UbuntuUserDataTests,ContainerBuildTests,InstallTests,InstallerEvidenceTests,UbuntuEvidenceTests,UbuntuSourceTests,RockyEvidenceTests,RockyInstallEvidenceTests,UbuntuInstallEvidenceTests,RockySourceTests,OpenSUSEEvidenceTests,OpenSUSESourceTests,PrepareTests) plus the Bash black-boxtests/test_iso_chain_launch.sh. No pytest, no conftest, no coverage threshold. - Run:
just check-tests, orjust checkfor the full suite in CI terms. The local pre-commit hooks omitcheck-tests; only CI's aggregatejust checkruns it, so runjust check-testsbefore shipping. - Python fixtures: module factories
manifest_data(**changes)(canonical v4 manifest),ubuntu_manifest_data(**changes)(the same with one Ubuntu profile),rocky_manifest_data(**changes)(the same with one Rocky profile),opensuse_manifest_data(**changes)(the same with one openSUSE profile), andvalid_log()(boot evidence); each class builds a per-test temp directory viaPath(self.enterContext(tempfile.TemporaryDirectory()))and localargsnamespace builders. - Mocking: patch
scripts.iso_chain.subprocess.run/Popenfor external tools (grub2-mkrescue,xorriso,cpio,xz,qemu-img,tcpdump,dracut) and narrow seams likeshutil.disk_usage,os.open, andPath.openfor race and limit cases.SourceServerTestsandExternalSourceTestsstart the real server on127.0.0.1:0and useurllibwith timeouts. - Shell test harness: the harness pins
LC_ALL=Cso the launcher sees the guest's locale, andwrite_fake_commandsinstalls fakeip,curl,kexec,sync,stat,sha256sum,udevadm,mount,umount, andblkid(one-ttag per call, exit 2 when none matches) onPATH, with optical devices modelled as directories; the launcher runs against injectedISO_CHAIN_SYS_CLASS_NET,ISO_CHAIN_RESOLV_CONF,ISO_CHAIN_CMDLINE,ISO_CHAIN_CALLS,ISO_CHAIN_FAULT,ISO_CHAIN_MEMINFO,ISO_CHAIN_RUN_DIR,ISO_CHAIN_MEDIA_DEVICES, andISO_CHAIN_SYS_BLOCKandISO_CHAIN_DEV_DIR, whose disks are sysfs directories beside sparse files. Success prints exactlylauncher shell tests: passed; failures printtest failure: <detail>and exit 1. - Conditional skip:
ExternalMirrorOptInTestsruns only when bothISO_CHAIN_EXTERNAL_MIRRORandISO_CHAIN_EXTERNAL_MANIFESTare set; there is no implicit mirror. - Expectations for new tests: add cases to the matching behavior class, cover boundary and
canonical-form rejections, assert that invalid input never echoes private or untrusted values,
and assert validation fails before any external command runs (
run.assert_not_called()). Deterministic, isolated, and safe in the full suite. - Asset coupling:
InstallTestsreadsassets/kickstart/fedora-44-power9.ks;buildandRockyKickstartTestsreadassets/kickstart/rocky-9.8-unattended.ks;buildandUbuntuUserDataTestsreadassets/autoinstall/ubuntu-26.04.1.json;PrepareTestsreadsassets/dracut/iso-chain-launch.serviceandiso-chain.target; the shell test requires both dracut scripts to exist and be executable. Changing an asset without updating these tests will fail the suite.
