# Kubernetes VM Test Lab `tools/lab/govoplan-lab.py` creates and operates an amd64 Ubuntu/K3s test environment on local or SSH-accessible libvirt hypervisors. It provides the commands requested for the complete VM lifecycle: | Command | Effect | | --- | --- | | `doctor` | Validate the strict inventory and, with `--online`, every hypervisor. | | `create --apply` | Download checksum-pinned cloud images, create VM overlays and boot the declared VMs. | | `deploy --apply` | Verify the signed GovOPlaN release, deploy shared state, install pinned K3s and apply GovOPlaN. | | `update --apply` | Pull newly pinned state images, update K3s serially and roll the selected GovOPlaN release. | | `status` | Show libvirt VM state, Kubernetes nodes and GovOPlaN pods. | | `pause --apply` | Gracefully shut down workers, control planes and shared state while retaining disks. | | `resume --apply` | Start the retained environment in dependency order and wait for readiness. | | `verify` | Collect sanitized live-cluster evidence and optionally perform the API-pod-loss drill. | | `destroy --apply --confirm ` | Delete only the lab-owned domains and overlays; local evidence is retained by default. | Every mutating command is a dry run unless `--apply` is present. Destruction also requires the exact lab name. Generated credentials, CA keys, manifests and evidence are written below the configured `state_directory` with owner-only permissions. Keep that directory outside the repository and include it in the workstation backup policy. Existing domains are reused or removed only when their GovOPlaN ownership description and both expected lab disk paths match. ## What The Lab Proves The supplied inventories describe two different assurance levels: - `tools/lab/govoplan-lab.example.toml` creates four VMs on one libvirt host. It is suitable for development, deployment rehearsal, migration testing, application-pod replacement and recovery-tool exercises. It cannot close GovOPlaN #27 because one physical host remains one failure domain. - `tools/lab/govoplan-lab.acceptance.example.toml` places the two workers on different hypervisors and puts the control and state VMs on a third. It can produce the bounded stateless application-tier evidence required by #27 when the declared hypervisors are genuinely independent physical failure domains. Both examples use one control-plane VM and one state VM. This keeps the bounded #27 target economical, but it does not prove control-plane or state-service high availability. For control-plane failover, declare exactly three control nodes on independent hosts. PostgreSQL, Redis and object-storage failover must be tested against independently operated HA services; the lab's single state VM is intentionally a replaceable integration fixture. Approximate minimum capacity for the four-VM profile is 10 vCPUs, 16 GiB RAM and 192 GiB of thin-provisioned disk. A six-VM profile with three controls needs additional capacity. Do not overcommit memory on an acceptance target. ## 1. Prepare The Hypervisors On each Ubuntu/Debian libvirt host: ```bash sudo apt-get update sudo apt-get install -y \ qemu-kvm libvirt-daemon-system libvirt-clients virtinst cloud-image-utils curl sudo systemctl enable --now libvirtd ``` Use a dedicated lab-administration account. Remote hypervisors are managed over SSH and the lifecycle invokes `sudo -n` there, so that account needs bounded non-interactive permission for libvirt, image and cloud-init operations. `NOPASSWD: ALL` is acceptable only on isolated lab hypervisors. On a local hypervisor, put the workstation account in the `libvirt` group and point `vm_image_directory` at a directory writable by that account and traversable by `libvirt-qemu`. The lifecycle connects explicitly to `qemu:///system` and does not require passwordless local sudo. Log out and back in after a new group assignment before running `doctor --online`. Create the configured image directory before running the doctor; it deliberately rejects a missing or non-writable storage root instead of silently falling back to a different filesystem. Create a dedicated SSH key on the management workstation: ```bash ssh-keygen -t ed25519 -f "$HOME/.ssh/govoplan-lab" \ -C "GovOPlaN Kubernetes lab" ``` Install its public key for every remote hypervisor account. The same public key is injected into the VMs. The lifecycle keeps its own `ssh_known_hosts` file, uses `accept-new` for first contact, and rejects changed host keys until a lab-owned VM is deliberately recreated. ### Network contract The configured `bridge` must exist on every selected hypervisor. All VM addresses are static. Reserve them outside DHCP allocation and ensure that the management workstation can route directly to every VM address; the lifecycle does not tunnel VM traffic through the hypervisor SSH connection. Permit only these flows inside the lab network: | Port | Source and destination | Purpose | | --- | --- | --- | | TCP 22 | management workstation to every VM/hypervisor | Provisioning and evidence collection | | TCP 6443 | all K3s nodes and management path to controls | Kubernetes API | | UDP 8472 | K3s node to K3s node | Default Flannel VXLAN; never expose publicly | | TCP 10250 | K3s node to K3s node | Kubelet metrics and API | | TCP 2379-2380 | control to control, only with three controls | Embedded etcd | | TCP 80/443 | test clients to K3s nodes | Traefik/ServiceLB ingress | | TCP 5432/6379/9443 | K3s nodes to the state VM | PostgreSQL, Redis and TLS-protected Garage S3 | | TCP 3025/3143 | approved test clients/workers to the state VM | GreenMail SMTP/IMAP test endpoints | The official [K3s networking requirements](https://docs.k3s.io/installation/requirements#networking) remain authoritative. Restrict state ports to the lab network even though the generated integration stack binds them on the state VM. ## 2. Create The Inventory Start with the one-host rehearsal: ```bash install -d -m 0700 "$HOME/.config/govoplan/labs" cp tools/lab/govoplan-lab.example.toml \ "$HOME/.config/govoplan/labs/development.toml" chmod 0600 "$HOME/.config/govoplan/labs/development.toml" ``` Edit at least the bridge, network, static addresses and SSH key paths. For a multi-host run, copy the acceptance example and replace every example hostname, failure-domain declaration and network value. Strict parsing rejects unknown keys, mutable HTTP inputs, malformed checksums, duplicate addresses/MACs and an acceptance inventory that collapses workers onto one declared hypervisor or failure domain. Cloud image, K3s binary, K3s installer and GovOPlaN release inputs are URL plus SHA-256 pairs. Updating means changing those reviewed pins and then running the `update` command; the tool deliberately does not follow `latest` aliases. The one-host example uses the dedicated `govoplan-lab` NAT network. Its DHCP pool ends at `192.168.123.99`; the static lab addresses start at `192.168.123.201`. Define and start it once on the local hypervisor: ```bash virsh --connect qemu:///system net-define \ tools/lab/libvirt/govoplan-lab-network.xml virsh --connect qemu:///system net-autostart govoplan-lab virsh --connect qemu:///system net-start govoplan-lab ``` Re-running those commands is unnecessary when `virsh net-info govoplan-lab` already reports an active, persistent network. The lab destroy command leaves this reusable network in place. ## 3. Validate And Create The VMs ```bash LAB="$HOME/.config/govoplan/labs/development.toml" PYTHON="/mnt/DATA/git/govoplan/.venv/bin/python" "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" doctor --online "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" create "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" create --apply ``` The preview is safe to run repeatedly. Creation reuses a domain whose exact lab-owned name already exists and otherwise creates a thin qcow2 overlay under `vm_image_directory//`. ## 4. Deploy GovOPlaN If `git.add-ideas.de` requires authentication for release images, export a read-only package/container-registry identity for this shell. A Gitea package token can be used as the password: ```bash export GOVOPLAN_LAB_REGISTRY_USERNAME='package-reader' read -r -s GOVOPLAN_LAB_REGISTRY_PASSWORD export GOVOPLAN_LAB_REGISTRY_PASSWORD ``` Then preview and apply: ```bash "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" deploy "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" deploy --apply "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" status unset GOVOPLAN_LAB_REGISTRY_PASSWORD ``` Deployment verifies the downloaded release manifest and keyring by pinned digest and by the existing GovOPlaN signature policy. It deploys PostgreSQL, Redis, single-node Garage and GreenMail on the state VM. The API, WebUI, workers and scheduler run in K3s from digest-pinned release images. A private lab CA protects both ingress and S3; backend pods receive only the CA Secret and keep TLS verification enabled. The CA profile carries critical `CA:TRUE` and `keyCertSign,cRLSign` constraints. `deploy` and `update` rotate older lab CAs that do not satisfy that profile and reissue the ingress/S3 certificate. The final output identifies two local files below `state_directory`: - `hosts` maps the public GovOPlaN and S3 test names to their VM addresses; - `pki/ca.crt` is the private lab CA certificate. Add the host mappings to the test client's resolver and trust the CA only on devices used for this lab. On Debian/Ubuntu: ```bash STATE="$HOME/.local/share/govoplan/labs/govoplan-k8s-lab" cat "$STATE/hosts" sudo install -m 0644 "$STATE/pki/ca.crt" \ /usr/local/share/ca-certificates/govoplan-k8s-lab.crt sudo update-ca-certificates ``` Review mappings before adding them to `/etc/hosts`; the lifecycle does not edit the workstation's trust or resolver configuration. Reinstall `pki/ca.crt` in the client trust store after an automatic CA rotation. ### Enroll the first administrator The production runtime does not create a default password. Issue one expiring, single-use first-administrator credential inside an API pod and copy its owner-only artifact out immediately: ```bash KUBECTL="$STATE/bin/kubectl" POD="$($KUBECTL -n govoplan get pods \ -l app.kubernetes.io/component=api \ -o jsonpath='{.items[0].metadata.name}')" ARTIFACT="$STATE/first-admin-enrollment.json" umask 077 $KUBECTL -n govoplan exec "$POD" -- \ python -m govoplan_core.commands.first_admin issue \ --reason 'initial Kubernetes lab enrollment' \ --output /tmp/first-admin-enrollment.json $KUBECTL -n govoplan exec "$POD" -- \ cat /tmp/first-admin-enrollment.json > "$ARTIFACT" $KUBECTL -n govoplan exec "$POD" -- \ rm -f /tmp/first-admin-enrollment.json chmod 0600 "$ARTIFACT" ``` Submit the token from that artifact once to `/api/v1/bootstrap/first-admin` with the administrator email, display name, password, tenant slug and tenant name. The password must contain at least 12 characters. The lab command performs that exchange without placing either the token or password in process arguments, rejects redirects, and removes the artifact only after HTTP 201: ```bash "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" enroll-admin \ --email 'owner@example.org' \ --display-name 'System Owner' \ --tenant-slug default \ --tenant-name 'Default Tenant' "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" enroll-admin \ --email 'owner@example.org' \ --display-name 'System Owner' \ --tenant-slug default \ --tenant-name 'Default Tenant' \ --apply ``` The public lab hostname must already resolve on the management workstation; the command verifies TLS through the generated private CA directly. ## 5. Collect #27 Evidence Create a short-lived API key authorized to read the Ops status endpoint. In the current Access administration UI, open **Tenant API keys** and select only **View tenant settings** (`admin:settings:read`); the Ops endpoint explicitly accepts that compatibility scope. A dedicated operator credential may instead use `ops:operations:read`. Then run: ```bash export GOVOPLAN_OPS_API_KEY='short-lived-value' "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" verify \ --exercise-api-pod-loss unset GOVOPLAN_OPS_API_KEY ``` The verifier requires ready API and WebUI pods across at least two Kubernetes nodes, all Deployments available, consistent runtime composition, queue coverage and a valid database-connection budget. During the optional drill it deletes one ready API pod, probes public readiness and waits for replacement. It writes sanitized output to `state_directory/evidence/kubernetes-multi-host.json` and never stores the API key. A rehearsal inventory prints an explicit warning that its result is not independent-failure-domain evidence. Retain these private artifacts together for review: 1. `inventory.json` and the reviewed inventory TOML; 2. the adopted release manifest/keyring and installation receipt; 3. `kubernetes.json`; 4. the Kubernetes verifier output; 5. private cluster logs for the approved drill window; 6. the operator's out-of-band evidence that the worker hypervisors are independent physical hosts or availability zones. GovOPlaN #37 additionally requires independent assessment and production approval keys. Running its evidence jobs in containers is supported, but a container does not create an independent authority. Follow `TARGET_MATURITY_EVIDENCE_RUNBOOK.md` after the #27 drill passes. ## 6. Update, Pause, Resume And Remove After reviewing and changing pinned image/K3s/release values in the inventory: ```bash "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" update "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" update --apply ``` Workers are cordoned, drained, updated and uncordoned one at a time. K3s controls are reconciled serially. The release-specific migration Job remains subject to GovOPlaN's signed backup-evidence gate. The lab update command is not a substitute for creating recovery evidence before a destructive state-schema change. To stop compute use without deleting disks: ```bash "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" pause --apply "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" resume --apply ``` To remove VM resources while preserving local evidence: ```bash "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" destroy "$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" destroy \ --apply --confirm govoplan-k8s-lab ``` Add `--purge-local-state` only after evidence and recovery material have been retained elsewhere. That option deletes the generated local CA, secrets, manifests and evidence as well as the VMs. ## Acceptance Boundary This tool supplies reproducible infrastructure and executes the bounded stateless-node drill. It does not certify the truth of operator-entered failure domains, provide HA PostgreSQL/Redis/Garage, create production backup evidence, or approve its own results. Those boundaries are deliberate: #27 can close after a passing run on independently controlled hosts; broader production maturity remains governed by #35, #37 and the target evidence runbook.