Installation

Ze provides commands for local installation, remote PXE provisioning, and appliance ISO installer media.

Local Installation

ze install local copies the ze binary to a standard system location and creates the config directory.

Quick Start

sudo ze install local

This presents an interactive menu to select the installation prefix:

Select installation prefix:
  1) /usr/local  (recommended)
  2) /usr  (system)
  3) /opt/ze  (self-contained)
Choice [1]:

Use --prefix for non-interactive use:

sudo ze install local --prefix /usr/local

Flags

Flag Default Description
--prefix interactive Installation prefix (binary goes to <prefix>/bin/ze)
--dry-run Print what would be done without making changes

What It Does

  1. Copies the running ze binary to <prefix>/bin/ze
  2. If no database.zefs exists at the config path: creates the config directory

The config directory is resolved from the binary path following GNU prefix conventions:

Binary location Config directory
/usr/local/bin/ze /etc/ze
/usr/bin/ze /etc/ze
/opt/ze/bin/ze /opt/ze/etc/ze

After installation, run ze init to bootstrap the database.

Systemd Service

After installing the binary and bootstrapping the database, use ze install systemd to set up the systemd service:

sudo ze install local --prefix /usr/local
sudo ze init
sudo ze install systemd --start

The generated service-management unit file:

[Unit]
Description=Ze Network OS
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=ze
Group=ze
ExecStart=<prefix>/bin/ze start
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
LimitCORE=infinity
WorkingDirectory=<config-dir>
Environment=ZE_CONFIG_DIR=<config-dir>
Environment=XDG_RUNTIME_DIR=/run/ze
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ProtectHome=true
RuntimeDirectory=ze

[Install]
WantedBy=multi-user.target

ze install systemd refuses to run unless <config-dir>/database.zefs exists. It creates the ze user and group if missing, changes ownership of the config directory and database.zefs to ze:ze, writes /etc/systemd/system/ze.service, runs systemctl daemon-reload, and enables the service. Use --dry-run to print the unit file without root or systemd, --config <dir> to override the config directory in the unit, --force to overwrite an existing unit, and --start to start the service after enabling it.

The systemd unit sets XDG_RUNTIME_DIR=/run/ze, so the daemon socket is /run/ze/ze.socket. For local operator CLI access, configure daemon { socket "/run/ze/ze.socket"; } or export XDG_RUNTIME_DIR=/run/ze.

Uninstalling

ze uninstall systemd removes the systemd service; ze uninstall local removes the binary and optionally the config directory. Always remove the service before the binary, so nothing is left running (or trying to restart) a binary that is gone.

ze uninstall systemd            # stop, disable, and remove the systemd unit
ze uninstall systemd --purge    # also remove the ze user and group
ze uninstall local              # remove binary
ze uninstall local --purge      # also remove config directory and database
ze uninstall local --dry-run    # preview what would be removed

Flags

Flag Default Description
--prefix detect from running binary Installation prefix
--purge Also remove config directory and database
--dry-run Print what would be done without making changes
--yes Skip confirmation prompt

Without --yes, uninstall shows what will be removed and asks for confirmation before proceeding.

To check the service status, use systemctl status ze.service directly.

Installing on Real Hardware (End to End)

This is the bare-metal walkthrough for the PXE install flow. It is exactly what make ze-install-qemu-test exercises in software (build an image, serve it, boot an installer kernel + initrd that writes the disk, then log in over SSH) β€” see End-to-End QEMU Verification to dry-run the same chain before touching hardware. The reference subsections below (Remote Provisioning, Installer Kernel, Installer Initrd, Bootstrap Mode) cover each piece in detail; this section sequences them.

1. Build the disk image

Use the structured appliance builder (full reference: appliance guide, "ze appliance"):

ze appliance init prod
# For real hardware: set image.kernel-profile to "hardware" and
# image.arch to match the target CPU in appliance.json before build.
ze appliance build prod

This produces ~/.config/ze/appliances/prod/ze-<timestamp>.img with TLS, SSH credentials, and a seed config baked into its /perm zefs. Match image.arch in appliance.json to the target CPU.

2. Build the installer kernel and initrd

The target PXE-boots a kernel + initrd, not the disk image. Build both for the target architecture:

ze appliance kernel prod                     # reads arch + profile from appliance.json
ze appliance initrd                          # build/initrd/initrd.img.gz (pure Go)

Or build directly with Make:

make -C tools/installer-kernel PROFILE=hardware ARCH=amd64   # real hardware, x86_64
make -C tools/installer-kernel PROFILE=hardware ARCH=arm64   # real hardware, ARM
ze appliance initrd                                          # initrd builds in pure Go

The PROFILE selects the driver set: qemu (default, virtio only) or hardware (EFI stub, framebuffer, Intel/Realtek/Broadcom/Mellanox NICs, AHCI, NVMe). A stock distro kernel will not boot the module-free initrd; see Installer Kernel.

3. Start the provisioning server

On a ze device on the (isolated) provisioning network:

sudo ze install remote \
  --interface eth0 \
  --network 192.168.50.0/24 \
  --image ~/.config/ze/appliances/prod/ze-<timestamp>.img \
  --kernel build/kernel/Image \
  --initrd build/initrd/initrd.img.gz \
  --ssh-username admin \
  --ssh-password 'choose-a-strong-one'

--kernel and --initrd copy the installer files to build/pxe/boot/. Stock iPXE binaries from tools/ipxe-binaries/ are copied to build/pxe/tftp/ if not already present. If the files are already staged from a previous run, omit --kernel and --initrd.

This runs DHCP (with PXE options and iPXE chainloading), TFTP (bootloaders), and the HTTP image server on eth0. It serves the image at /install/image/<filename>, a dynamically generated iPXE boot script at /install/boot/boot.ipxe, and a credential database.zefs (generated from --ssh-*, password stored hashed) at /install/database.zefs. See Remote Provisioning (PXE).

4. Net-boot the target

Set the target firmware to network boot. It then:

  1. DHCPs an address and TFTP bootfile, chainloads iPXE.
  2. iPXE sends a second DHCP request; the server detects iPXE via option 77 and responds with the HTTP boot script URL instead of the TFTP bootfile.
  3. iPXE fetches boot.ipxe from the image server, which contains the kernel command line with ze.server, ze.image, and ip=dhcp.
  4. iPXE loads the installer kernel + initrd via HTTP and boots.
  5. The initrd downloads the image and database.zefs, writes the first fixed disk (/dev/sda, /dev/nvme0n1, /dev/mmcblk0 ...; removable, virtual, optical and mtdblock flash devices are skipped), and reboots.
  6. The target boots ze in bootstrap mode and starts SSH.

5. Log in and configure

ssh admin@<target-ip>      # the password given to --ssh-password

ze's SSH endpoint is the network-OS CLI. Configure with ze config edit and commit; the committed config replaces the bootstrap config on the next restart.

Troubleshooting

Appliance ISO Install

For appliances built with ze appliance build, ISO media is an offline install transport for the gokrazy image. The image is gzip-compressed inside the ISO; the installer initrd decompresses it during installation. Create it with:

ze appliance build prod
ze appliance kernel --profile hardware prod   # download or build installer kernel (reads arch from config)
ze appliance initrd                           # download or build installer initrd
ze appliance iso prod

Use ze appliance iso --check to verify all prerequisites (kernel, initrd, grub, xorriso) are available before building. The kernel and initrd commands download pre-built artifacts from the release server when available, falling back to a local QEMU VM build (kernel) or make build (initrd). Cached artifacts are stored under $XDG_CACHE_HOME/ze/ (default ~/.cache/ze/).

For arm64 targets (the kernel version is single-sourced in internal/appliance/kernel.version; --version only overrides it and must be a kernel >= 7):

ze appliance kernel --arch arm64
ze appliance iso --kernel build/kernel/Image prod

The ISO installer decompresses and writes the embedded image to the target disk. Unlike PXE provisioning, it does not download /install/database.zefs or write a separate database after the disk image, because the appliance build already injected /perm/ze/database.zefs into the image.

The ISO bootloader target follows image.arch: amd64 images produce BOOTX64.EFI, arm64 images produce BOOTAA64.EFI. By default the command checks for a cached kernel under $XDG_CACHE_HOME/ze/installer-kernel/ then falls back to build/kernel/Image. Pass --kernel to use a specific kernel path.

If the target has more than one fixed disk, create the ISO with an explicit whole-disk target. The initrd rejects ambiguous implicit disk selection in ISO mode, excludes the ISO source media from target candidates, and requires a builder-generated media id match before it trusts a mounted installer volume.

ze appliance iso --target /dev/vda prod

The generated ISO includes the installer kernel, initrd, the selected image, its checksum, and metadata. It contains the full provisioned appliance image, so handle it like the .img artifact.

USB write method: the ISO can be written with dd, Etcher, or Rufus in DD mode. Ventoy is also supported when the installer kernel includes loop device and FAT/exFAT filesystem support (the hardware kernel profile has this). The initrd detects the ISO file on the Ventoy data partition, loop-mounts it, and proceeds with the installation. When using the qemu kernel profile, Ventoy is not supported.

ISO installs power off after the disk write so the removable installer media can be removed before the next boot. They do not auto-reboot while the ISO is still present.

Remote Provisioning (PXE)

ze install remote is a one-command provisioning server that PXE-boots target machines with a gokrazy image containing ze.

Warning: the provisioning network MUST be isolated. Run ze install remote only on a dedicated segment with no other DHCP server. On a shared network a foreign DHCP server (for example a corporate one) can win the boot-time race and hand the target a lease with no route back to ze.server; the installer then probes an unreachable server until ze.wait expires and drops to a debug shell, and the disk is never written. The initrd now re-validates the kernel lease and recovers when a reachable interface exists (see Kernel Command Line), but a second DHCP server on the same L2 segment is unsupported and can still break provisioning.

How It Works

  1. The operator runs ze install remote on an existing ze device connected to the provisioning network.
  2. Ze generates a config enabling DHCP (with PXE extensions), TFTP, and an HTTP image server, then forks itself (ze -) with the config piped to stdin.
  3. A target machine PXE-boots: DHCP assigns an IP and directs it to the TFTP bootloader, which chain-loads the installer kernel and initrd via HTTP.
  4. The installer writes the gokrazy image to disk and reboots.
  5. The target boots into ze in bootstrap mode: discovers all interfaces, enables DHCP client on each ethernet NIC, and starts SSH for operator access.

Quick Start

ze install remote \
  --interface eth0 \
  --network 192.168.1.0/24 \
  --image /path/to/gokrazy.img \
  --ssh-username admin \
  --ssh-password changeme

This starts three servers on eth0:

Protocol Port Purpose
DHCP 67/udp IP assignment with PXE options (bootfile, next-server)
TFTP 69/udp Bootloader delivery (iPXE for BIOS/UEFI)
HTTP 80/tcp Disk image and boot file serving

The server IP is resolved from the interface's first IPv4 address. If the interface has no IPv4 address, the address from --network is added via netlink and removed on exit (if --network is a network address like 192.168.1.0/24, the first host 192.168.1.1 is used). Use --address to override.

Flags

Flag Required Default Description
--interface βœ“ Network interface to bind all servers
--network βœ“ Provisioning subnet CIDR (/8 to /30)
--image βœ“ Path to gokrazy disk image file
--ssh-username βœ“ Admin username for the installed target
--ssh-password βœ“ Admin password (bcrypt-hashed before embedding)
--address βœ• First IPv4 on interface (or from --network if none) Server IP override
--kernel βœ• Path to installer kernel (copied to boot directory)
--initrd βœ• Path to installer initrd (copied to boot directory)
--pxe-dir βœ• build/pxe PXE serve root: boot files under <dir>/boot, TFTP under <dir>/tftp

DHCP Pool

The DHCP pool range scales with the subnet size. The server IP is excluded from the pool. Examples:

Network Server Pool Start Pool Stop
10.0.0.0/24 10.0.0.1 10.0.0.2 10.0.0.254
192.168.1.0/28 192.168.1.1 192.168.1.2 192.168.1.14
10.1.1.0/30 10.1.1.1 10.1.1.2 10.1.1.2

PXE Boot

The DHCP server detects PXE clients via option 60 (PXEClient:) and reads option 93 (client architecture) to select the bootfile:

Architecture Bootfile
BIOS (type 0) ipxe.pxe
UEFI (type 6, 7, 9) ipxe.efi

When the PXE client is iPXE (detected via option 77 user-class prefix "iPXE") and boot-script-url is configured, the DHCP server sends the HTTP boot script URL as the bootfile instead of the TFTP binary. This two-stage chainload (firmware -> iPXE via TFTP -> boot.ipxe via HTTP) eliminates the need for custom-embedded iPXE builds.

The image server generates boot.ipxe dynamically at /install/boot/boot.ipxe with the correct ze.server, ze.image (lexicographically last .img file), and ze.port (when not 80). A static boot.ipxe file in the boot directory takes precedence over dynamic generation for operator customization.

Bootfiles are served from build/pxe/tftp/ via TFTP. The installer kernel and initrd are served from build/pxe/boot/ via HTTP. Stock iPXE binaries are bundled in tools/ipxe-binaries/ and staged automatically by ze install remote.

The default --pxe-dir build/pxe is relative, resolved against the working directory. make ze-pxe stages artifacts into build/pxe from the repo root, so run ze install remote from the repo root too (as pxe.sh does), or pass an absolute --pxe-dir. Run from a different directory and the server looks for build/pxe under that directory and reports the missing artifacts by their resolved absolute path.

SSH Credentials

The --ssh-password is bcrypt-hashed (cost 10) before being embedded in the generated config as ssh-password-hash. The plaintext password is never written to disk or config. It is visible in the process listing (ps aux) while ze install remote is running.

Generated Config

ze install remote generates a standard ze config in brace format and pipes it to a child ze process. The generated config enables three plugins:

The same provisioning setup can be achieved by writing the config manually and running ze <config-file>.

Watching Provisioning Activity

ze install remote runs the install servers at info log level by default, so the terminal shows the live boot sequence with no extra flags:

Set ze.log to change verbosity (ze.log=debug, ze.log=warn); an explicit ze.log or per-subsystem ze.log.{dhcpserver,tftpserver,imageserver} overrides the info default.

If you see the iPXE downloads (boot.ipxe, vmlinuz, initrd.img.gz) but never an /install/image/<name> request, the target booted the kernel but its initrd could not reach the server to pull the image (commonly a foreign DHCP lease on a non-isolated network) and the install did not complete.

Confirming Which Image Is Installed

A failed PXE install never writes the disk, so the target reboots into whatever was on it before (for example a previous ISO install) and can look unchanged. To confirm the build actually running, ze appliance build bakes a manifest into the image at /perm/ze/build.json, which ze version reports:

ze 26.06.17 (built …)
  …
  image:    ze-20260617-160000.img (built 20260617-160000)
  appliance: prod

The baked manifest omits the image checksum (it would be self-referential, since baking changes the image); the full sha256 is in the external build.json next to the image and in the provisioning server's imageserver: image to install log. Compare the image:/built line on the target with that server log line to verify the latest image installed.

Requirements

Shutdown

SIGTERM or SIGINT sent to ze install remote is forwarded to the child ze process. The child shuts down cleanly (closes listeners, drains connections). Closing the parent also sends EOF on the stdin pipe, which ze treats as a shutdown signal.

Bootstrap Mode

When ze starts with a zefs database but no config file and no template, it enters bootstrap mode automatically. This is the expected state after a PXE-provisioned device boots for the first time.

What Happens

  1. Ze detects no config in zefs (no file/active/ze.conf, no file/template/ze.conf).
  2. Interface discovery enumerates all OS network interfaces.
  3. A minimal config is generated: DHCP client enabled on every ethernet interface, SSH server enabled.
  4. Ze starts with this config. DHCP clients acquire addresses, SSH becomes reachable.

Operator Workflow

  1. SSH into the device using the credentials pre-provisioned by ze install remote.
  2. Configure ze via the CLI (ze config edit, then commit).
  3. The committed config replaces the bootstrap config. On the next restart, ze starts in normal mode.

Constraints

Limitations

Installer Initrd

The installer initrd is a minimal Linux image that performs the actual disk write on target hardware. It is the final step in the PXE chain: the bootloader fetches the kernel and initrd via HTTP, the kernel boots, and the initrd's init script installs ze.

What It Does

  1. Parses ze.source, ze.server, ze.port, ze.image, ze.target, and ze.media-id from the kernel command line
  2. In HTTP mode, downloads the gokrazy disk image from http://<server>:<port>/install/image/<name>
  3. In ISO mode, mounts local ISO media read-only and selects the embedded compressed image
  4. Writes the image to the selected non-removable block device (decompressing in ISO mode)
  5. In HTTP mode only, re-reads the partition table, mounts partition 4 (ext4, /perm), downloads database.zefs, and writes it to /perm/ze/database.zefs
  6. In HTTP mode, reboots. In ISO mode, powers off so the operator can remove the installer media before the next boot.

Building the Initrd

Prerequisites: the Go toolchain only. The initrd is built in pure Go, so no busybox, cpio, or gzip host tools are required.

ze appliance initrd

This cross-compiles cmd/ze-installer for the target architecture and packs the single static binary into build/initrd/initrd.img.gz (the /init entry of a pure-Go newc cpio written through compress/gzip). Copy it alongside a Linux kernel to the boot directory served by the image server (build/pxe/boot/).

Kernel Command Line

The bootloader sets these parameters:

Parameter Required Default Purpose
ze.source βœ• http Source mode: http for PXE or iso for local ISO media
ze.server HTTP only IPv4 address of the ze-install server
ze.port βœ• 80 TCP port of the install HTTP server (1-65535)
ze.image βœ• ze.img Name of the disk image to install
ze.target βœ• Explicit whole-disk target such as /dev/vda
ze.wait βœ• 30 Max server probe attempts before giving up (0 = skip probe)
ze.media-id ISO only Builder-generated 32-hex token that identifies the booted installer ISO
ip=dhcp HTTP only Kernel-level network configuration (with userspace fallback)

ze.port exists for install servers that cannot bind the privileged port 80 (for example an unprivileged HTTP server, or a QEMU test harness that serves on an ephemeral port). ISO mode does not use ze.server, ze.port, or ip=dhcp.

On some hardware (e.g. Intel I226-V) the kernel ip=dhcp autoconfiguration races against NIC carrier detection and times out before the link comes up; on a non-isolated network it can also win a lease from a foreign DHCP server (for example a corporate one) whose default route cannot reach ze.server. The initrd guards against both. It trusts the kernel-provided default route only when ze.server actually answers an HTTP probe; if there is no default route, or the route present cannot reach the server, it brings up all non-loopback interfaces, waits up to 10 seconds for carrier, and runs an in-process DHCP client (nclient4) on each interface, verifying the server is reachable before accepting the lease. Interfaces that obtain a lease but cannot route to ze.server:ze.port are flushed and skipped. If no interface produces a working route, the installer drops to the recovery console. (A foreign DHCP server on a shared segment can still defeat this if it also wins the per-interface lease race, which is why the provisioning network must be isolated.) Even after the network is configured, the install server may not be reachable yet (switch STP port transitions can block traffic for 30-50 seconds after a reboot). The initrd probes the server with a 2-second timeout, retrying up to 30 times. Each probe distinguishes "TCP unreachable" from "HTTP error response" so a server that returns 404 is still considered reachable. Interface state and routing tables are logged every 10 attempts for diagnostics.

The installer fans its progress and FATAL lines to every console listed in /sys/class/tty/console/active, not just the single /dev/console the kernel marks preferred. On a headless box the preferred console can be a dead VGA tty while kernel messages still reach the serial line, which would otherwise hide all installer output. The generated boot.ipxe also selects the console set per client architecture via iPXE's ${buildarch}: x86 clients get console=tty0 console=ttyS0, while arm64 also keeps console=ttyAMA0 (the ARM PL011 UART, which never registers on x86 and can dead-end /dev/console if left on an x86 cmdline).

Existing ze install remote deployments need no ze.source change because the default source is http.

The installer selects a non-removable block device via sysfs. Virtual devices (loop, ram, dm, zram, md), optical drives (sr), floppies (fd), and firmware/CFI flash (mtdblock, the QEMU virt machine's pflash) are skipped.

In HTTP mode, the installer preserves the existing first-candidate behavior. In ISO mode, it also excludes the ISO source media and refuses to choose implicitly when more than one fixed candidate remains. Use ze.target=/dev/vda to name an explicit whole disk.

Supported target disk forms include /dev/sda (SATA/SCSI), /dev/vda (virtio-blk, used by QEMU/KVM), /dev/nvme0n1 (NVMe), and /dev/mmcblk0 (eMMC).

Error Handling

If the installer encounters an error (missing server IP, no disk found, download failure), it does not silently reboot. It opens a Go recovery console on every active console (preferring a serial console, since headless installs are driven over serial) so the operator can diagnose network or hardware issues. The recovery console is a fixed menu (retry network, diagnostics, reboot, power off), not a shell. Its policy has three branches: with ze.shell-auth set the console is password-gated (sha256 of the typed password); with no credential on ISO media it opens ungated (the operator is physically present); with no credential on a network install it prints the error, waits ~30 seconds, and reboots, so an unattended box never hangs waiting for a password nobody can supply.

Running Tests

The installer logic has Go unit tests alongside each source file in internal/install/disk (cmdline parsing, disk detection, the netlink lease apply, the stall-timeout download, the partition-node wait, and the recovery console / fatal policy):

go test ./internal/install/disk/...

End-to-end boot and install are covered by the QEMU evidence harness, which boots the real Go initrd:

make ze-install-qemu-test       # HTTP PXE install
make ze-install-iso-qemu-test   # ISO install

No External Binaries

The initrd contains exactly one file: the cmd/ze-installer Go binary as /init (PID 1). There is no busybox and no shell. Every operation the old shell init shelled out to (mount, DHCP, HTTP download, link/address/route, reboot) is an in-process golang.org/x/sys/unix syscall or a vendored library call, so there is no applet that can be not found at install time. See ai/rules/initrd-no-external-tools.md.

Installer Kernel

The initrd carries no kernel modules, so the kernel it boots alongside must have NIC drivers, disk drivers, ext4, devtmpfs, initramfs and ip=dhcp autoconfiguration all built in (=y). Stock distro/cloud kernels ship these as modules and cannot boot the initrd. ze deliberately ships no installer kernel: the right kernel is site-specific.

tools/installer-kernel/ builds a reference kernel with two profiles:

Profile File Drivers Use case
qemu (default) qemu.config virtio NIC + block QEMU tests, fast build
hardware hardware.config virtio + EFI + framebuffer + Intel/Realtek/Broadcom/Mellanox NICs + AHCI + NVMe Bare metal PXE/ISO install

Both profiles merge onto a shared base (kernel.config) that provides IP autoconfiguration, SCSI, ext4, initramfs, devtmpfs and serial console.

ze appliance kernel prod                       # reads arch + profile from appliance.json
ze appliance kernel --profile hardware --arch amd64
ze appliance kernel --builder qemu --arch arm64 prod

# Or build directly with Make:
make -C tools/installer-kernel                              # qemu profile, arm64 (default)
make -C tools/installer-kernel BUILDER=docker ARCH=amd64   # docker backend, x86_64
make -C tools/installer-kernel PROFILE=hardware ARCH=amd64 # hardware profile, x86_64

Set image.kernel-profile to "hardware" in appliance.json so ze appliance kernel <name> picks it up automatically. The CLI selects Docker first, then the shared QEMU backend, unless --builder forces one path.

ze appliance kernel resolves the open profile registry in Go, passes the resolved fragments to tools/kernel-builder/build.py, reads the emitted build/config, and fails loudly if any required symbol is not =y. The installer Makefile keeps its config fragments and .require manifests in tools/installer-kernel/ and delegates Docker/QEMU execution to tools/kernel-builder/. The QEMU path is tools/kernel-builder/qemu-build.py, which validates repo-relative builder, source, and output paths before booting the VM. Output is build/Image (the kernel) and build/config (the resolved config). See tools/installer-kernel/README.md for the full rationale and driver lists.

End-to-End QEMU Verification

make ze-install-qemu-test exercises the entire chain in QEMU with no hardware: it builds the initrd, builds a real appliance image with ze appliance (see the appliance guide), boots the installer kernel + initrd against a blank virtio disk, has the initrd download and write the image and zefs over HTTP, then boots the written disk and logs in over SSH as the provisioned power user. That final login is the regression test for credential loading from the installed zefs.

ZE_INSTALL_KERNEL=$PWD/build/kernel/Image make ze-install-qemu-test

make ze-install-iso-qemu-test exercises the appliance ISO transport. It builds the initrd and appliance image, creates an ISO through ze appliance iso, boots that ISO in QEMU, verifies the embedded image is written without the PXE-only ZeFS download branch, verifies the installer powers off safely, inspects the written GPT layout, and logs in over SSH using credentials from the embedded ZeFS database.

ZE_INSTALL_KERNEL=$PWD/build/kernel/Image make ze-install-iso-qemu-test

The ISO evidence self-skips with INSTALL-ISO-QEMU: SKIP when QEMU, a suitable installer kernel, UEFI firmware, grub-mkstandalone/grub2-mkstandalone, xorriso, or image-build tooling is unavailable.

The test self-skips (does not fail) when ZE_INSTALL_KERNEL is unset or a container runtime / qemu-system-* is unavailable, because there is no safe default installer kernel.

Environment Knobs

Variable Default Purpose
ZE_INSTALL_KERNEL (none β€” self-skips) Path to the installer kernel Image/vmlinuz
ZE_INSTALL_ARCH host arch (amd64 for ISO evidence) Target architecture for QEMU installer evidence and generated appliance config (arm64/amd64)
ZE_INSTALL_BOOT_TIMEOUT 300 Seconds to wait for the installer to write the disk
ZE_INSTALL_IMAGE_SIZE appliance default (2 GiB) Override image size-bytes (must stay large enough for the gokrazy A/B layout)
ZE_INSTALL_SSH_USER / ZE_INSTALL_SSH_PASS admin / secret Power-user credentials provisioned into the image and used for the AC login
ZE_INSTALL_NIC virtio-net-pci QEMU NIC model for the installer boot
ZE_INSTALL_KEEP unset Keep the work directory (image, written disk, serial logs) for inspection
ZE_INSTALL_IMAGE / ZE_INSTALL_ZEFS unset Reuse a prebuilt image + zefs instead of building one

QEMU Networking Note

The test points the guest at the slirp gateway (ze.server=10.0.2.2 ze.port=<ephemeral>) rather than a guestfwd forward. A guestfwd services only the first guest connection, which stalls the installer's second and third downloads (image, zefs); the gateway handles the sequential connections the installer makes. This is purely a test-harness concern β€” real PXE installs use a real network and ze install remote on port 80.