Skip to content

Qualify one disposable snapshot upgrade

deployments/qualification/qualify.py boots one prepared baseline image in an owned QEMU guest, observes its existing LychD deployment, upgrades only that guest to an explicitly pinned Tumbleweed snapshot, reboots it and repeats the observations. It uses rootless QEMU TCG and user networking with one SSH forward bound to 127.0.0.1. It changes no controller networking, desktop configuration or production host. A missing VM, repository, tool or input fails the run. Offline contract tests and --check validate tooling; they are not VM qualification.

This is a candidate qualification procedure. No successful snapshot transition is claimed by its presence. A passing receipt covers the exact baseline image, snapshots, controller source, application revision and OCI image recorded there. It establishes neither fresh installation, another snapshot transition, workstation hardware/browser behavior nor application-code upgrade. The same application source and image remain in use before and after the OS upgrade.

Prepare the private baseline

On an explicitly disposable builder, authenticate an official openSUSE image using its signed checksum and independently authenticated signing-key fingerprint. The official download page provides cloud images, NET installation ISOs and verification instructions. Either an authenticated official cloud qcow2 or NET ISO is valid installation lineage; a newly installed blank disk need not be Cloud-derived. Retain the original medium, signed checksum, public key and their SHA-256 identities; moving aliases and old-image retention are not permanent contracts. The original signed image and prepared baseline have separate identities. An operator may separately prepare an older baseline from a newer authenticated image, recording that explicit builder transition; the harness still requires the prepared guest's actual release and packages to match its baseline pin. The qualification run itself admits only a strictly later candidate.

Prepare the baseline using the Server procedure, including a model-free running body, exact source and admitted OCI image, operator and runtime SSH public keys, required native deployment protocol version 1 and any deliberately selected access profile. This preparation is a separate effectful operation: the harness does not construct a working baseline from an unconfigured cloud image. Keep private material outside every checkout. The baseline must retain the Server Btrfs layout and ordinary hardened administrator policy: SSH as operator, with password-backed sudo and root SSH disabled. Do not add NOPASSWD or a root-login exception. Keep the disposable operator's sudo password in one private mode-0600 file outside Git, pin it as ssh.admin_become_password, and use its operator key as ssh.admin_key. No production credential belongs here. The password file contains one UTF-8 password, with an optional final LF and no other control characters or leading/trailing whitespace.

Create /etc/lychd-qualification.json as a root-owned regular file, mode 0600, containing exactly {"schema":1,"disposable":true,"baseline_id":"<a recorded UUID>"}. Record its /etc/machine-id, snapshot VERSION_ID, source SHA and OCI image ID. Shut down the builder cleanly and retain a standalone qcow2 image with no backing file. Never use a production disk or a writable live image. The baseline marker is an additional guard; ownership of the actual QEMU process, pinned image and localhost-only connection establish the harness target. The prepared artifact must boot with BIOS firmware and virtio disk/network devices under QEMU q35; this first lane does not select UEFI firmware or qualify Secure Boot. The launcher includes a VGA device while keeping -display none; no graphical listener or VNC is opened. Configure the guest boot loader/kernel for ttyS0 when a retained serial console is needed. A bootable Cloud image with XFS does not satisfy the Server floor; install the required Btrfs layout separately instead of weakening that floor.

Pin one transition

Copy the plan example outside the checkout, replace every placeholder and restrict it and referenced private files to the current operator. The closed schema admits one baseline, one strictly later candidate, one prepared image, one application revision/image and one exact SSH host key. Host keys are pinned before the first connection; ssh-keyscan is not an admission step. The private operator and runtime keys must match keys already installed in the baseline. ssh.admin_user is exactly operator; no arbitrary administrator or root profile is admitted. The harness generates its own one-host inventory and SSH configuration; it accepts no arbitrary command hooks. Both observation phases first verify that the direct operator login has a nonzero effective UID. Privileged fixed guest actions use password-backed sudo -k -S through the operator connection. A fresh input marker proves sudo consumed the password line before guest JSON is parsed; cached or passwordless sudo cannot silently bypass this check. Ansible receives only the path of the frozen password file through --become-password-file. The password value travels only in process memory and stdin, never in command arguments, environment variables or diagnostics.

Pin both snapshots' exact official repositories and each repodata/repomd.xml SHA-256. openSUSE's history repositories use https://download.opensuse.org/history/<snapshot>/tumbleweed/repo/oss/; admit non-OSS only when that exact dated repository actually exists and is needed. There is no assumed retention guarantee. Preserve required artifacts before they disappear or stop. For each repository, admit the exact public signing-key file, SHA-256 and fingerprint. This explicit trust decision is distinct from trusting a key offered during an upgrade.

The harness refuses replaceable or untrusted ancestor directories. It copies admitted inputs, including the archive and uv binary referenced by variables, into its own private work directory through checked file descriptors and verifies the copied hashes. Later consumers use those copies. Keep the work parent owned by the controller user with mode 0700.

The harness verifies signed checksum provenance with gpgv, verifies all file hashes, and checks public-key fingerprints before launch. The controller-source digest covers the exact Ansible and qualification implementation; obtain it with qualify.py --source-digest after all candidate changes are settled. Private Ansible variables are JSON, individually hashed, and must pin the same application revision and image as the plan. Existing role parsers remain the owners of their detailed host and application input validation.

Run and retain evidence

Run as an ordinary unprivileged controller identity; real runs refuse effective UID 0. Use a private, absent work directory on a controller with Python 3.13+, QEMU (qemu-img and qemu-system-x86_64), OpenSSH, GnuPG and the isolated deployment controller already available. This procedure installs no local tools. Resource ceilings are fixed at two virtual CPUs, 3072 MiB RAM, a 24 GiB maximum overlay file and a 60-minute execution budget, with bounded command/boot waits and at most 25 additional seconds for owned-process/output cleanup. Each stage log and combined QEMU/serial output is capped at 32 MiB; exceeding a cap fails the run. The prepared image must have a virtual disk no larger than 64 GiB. Allow sufficient free disk.

python3 deployments/qualification/qualify.py --source-digest
python3 deployments/qualification/qualify.py --plan /private/plan.json \
  --controller /private/controller --work-dir /private/new-run --check
python3 deployments/qualification/qualify.py --plan /private/plan.json \
  --controller /private/controller --work-dir /private/new-run \
  --authorize-upgrade RUN_UUID:BASELINE_SNAPSHOT:CANDIDATE_SNAPSHOT

The exact authorization token is mandatory for a real run. --check never starts a VM or performs an upgrade. Ordinary Ansible host apply continues to exclude zypper dup and reboot. The harness selects isolated repository and service directories, verifies pinned metadata before and after refresh, keeps signature checks enabled, imports only admitted public keys, then runs zypper dup --no-allow-vendor-change with automatic refresh disabled. It never uses --releasever as a substitute for an explicitly dated repository. The zypper manual owns these option semantics.

Before and after the upgrade, require the exact snapshot, baseline identity, machine identity, rootless runtime login, responding user manager, host verification, native application acceptance and a no-change Server observation. Native acceptance uses lychd status --json --deployment-revision SHA --deployment-image-id ID --deployment-verify through the existing Ansible entrypoints. The optional access lane additionally runs the existing access verifier; an omitted lane provides no private-road qualification. Installed endpoint, browser and two-device acceptance still follows its owning guide.

The reboot must change the kernel boot ID and reach the exact candidate snapshot. Every required stage and a final controlled guest shutdown must pass before receipt.json says passed. A failure records the last stage and stops only the QEMU process owned by this invocation, using bounded termination and then a kill if necessary. It preserves the overlay, serial output and private per-stage logs; no automatic effectful retry, rollback or image deletion follows. The same bounded shutdown runs on success. Inspect failed evidence before preparing a new run.

receipt.json contains only a fixed set of public identities, stage results and log digests. The directory also contains private paths, inventory and diagnostic output: keep the entire directory private. Review the sanitized receipt before sharing it. Never upload raw logs, plans, keys, SSH configuration, memory/disk images or private Ansible variables as public CI artifacts. No manual CI workflow is supplied until the operator has an authenticated prepared-baseline artifact distribution and disposable-key delivery contract; missing artifacts cannot produce a green qualification job.