Files
omarchy-iso/plans/autoinstall.md
T
Jeffrey Guenther f5aa88b06c Plan autoinstall against the quattro install pipeline
PR #72 targeted the pre-quattro tree and can't be rebased -- the bash
seams it hooked into are gone. This plans the same feature against the
configurator/dashboard/orchestrator split, and drops the three parts of
that PR that were flagged as questionable.

Claude-Session: https://claude.ai/code/session_01Sy3HeoBqb13ahLuaiP2UkM
2026-08-04 05:54:00 -07:00

11 KiB
Raw Blame History

Autoinstall Plan

Goal

Let the standard Omarchy ISO install itself with no keyboard present, driven by config files handed to the VM on a second drive. This makes Omarchy usable as a Packer/Proxmox base image for disposable dev environments: create VM, boot, walk away, SSH in.

Product Requirements

  • No ISO rebuild. The shipped ISO autoinstalls when config is present and runs the configurator when it is not.
  • No new menu entry, no new boot parameter, no separate ISO flavor.
  • Config format is exactly what the configurator already writes — no second schema to maintain.
  • The installed machine is reachable over SSH with a key supplied at install time.
  • Interactive installs behave identically to today.

Chosen Architecture

Trigger: config presence, not a mode flag

The installer looks for a drive labeled cidata at startup. If it is there and carries the configurator's output files, those files are copied into /root and ./configurator is skipped — the rest of the install runs the ordinary path against ordinary inputs. If it is not there, nothing changes.

This is Ryan Hughes' suggestion from PR #72 and it is what keeps the diff small: the install pipeline can be rewritten freely as long as it keeps reading its five files out of /root, and autoinstall follows along for free.

cidata is the cloud-init NoCloud label, so Proxmox, libvirt, and Packer all already know how to attach one.

What the ISO already does for us

Two things that would otherwise need building are already true on a stock quattro install, per manifests/fresh-4-semantic.json:

  • sshd.service is enabled.
  • NetworkManager.service is enabled and active, with DHCP by default.

So remote access needs only an authorized_keys file. No systemd-networkd unit (it would contend with NetworkManager), no systemctl enable sshd, and specifically no loosening of PasswordAuthentication — key auth is the only thing autoinstall adds.

Non-interactivity belongs to the dashboard

omarchy-install-dashboard owns every prompt that survives to the end of an install: the reboot confirm and, on failure, the action menu. Both are gum calls reading from the TTY, and both hang forever on a headless VM. Autoinstall therefore sets one env var that the dashboard reads, rather than patching prompts out of scripts after the fact — which is what PR #72 did to finished.sh, and what everyone correctly disliked.


Non-Goals

  • No config generator, validator, or schema doc. The configurator is the source of truth for the format; users copy its output.
  • No secret management. Whoever builds the cidata drive owns the password hash and the public key on it.
  • No PXE, no network-fetched config, no cloud-init compatibility beyond reusing the drive label.
  • No changes to disk selection, encryption, or any install phase behavior.

Repo Implementation Plan

1) Detect the cidata drive and skip the configurator

File

  • configs/airootfs/root/.automated_script.sh

Changes

  • Add a load_cidata function above the existing cd /root (:93):
    • Look for /dev/disk/by-label/cidata then /dev/disk/by-label/CIDATA; return non-zero if neither exists.
    • Mount read-only on a temp mountpoint; return non-zero if the mount fails.
    • Require both user_configuration.json and user_credentials.json on the drive — this is the same pair InstallContext.from_env treats as mandatory. Missing either means the drive is not an autoinstall drive; unmount and return non-zero.
    • Copy the required pair plus, when present, user_full_name.txt, user_email_address.txt, user_encrypt_installation.txt, and ssh.json into /root.
    • Unmount before returning. Leave nothing mounted for the install to trip over.
  • Replace the bare ./configurator call (:94) with: run load_cidata; on success export OMARCHY_UI_INTERACTIVE=no, on failure run ./configurator.
  • Pass --ssh-keys-file /root/ssh.json to omarchy-iso-install in the dashboard invocation (:101–:110), alongside the existing five flags. Always pass it; the orchestrator no-ops when the file is absent, which keeps the interactive path on the identical command line.

Notes

  • The three text files are optional by construction: _read_text in context.py returns "" for a missing path and encrypt falls back to false. Do not add stricter checks here than the orchestrator itself applies.
  • load_cidata must not run before warm_offline_mirror is backgrounded — the mirror warm should keep its head start.

Acceptance criteria

  • ISO with no cidata drive attached: configurator runs, byte-identical UX to today.
  • ISO with a cidata drive carrying the five files: configurator never appears; install starts within seconds of boot.
  • ISO with a drive labeled cidata that is empty or carries only one of the two required files: falls back to the configurator rather than failing the boot.
  • Nothing remains mounted from /dev/disk/by-label/cidata once the install begins.

2) Non-interactive dashboard

File

  • configs/airootfs/usr/local/bin/omarchy-install-dashboard

Changes

  • Read OMARCHY_UI_INTERACTIVE once near the other env reads at the top.
  • In reboot_prompt() (:474): when non-interactive, return 0 immediately instead of calling gum confirm. The existing OMARCHY_UI_AUTO_REBOOT=no check at :737 stays in front of the actual reboot, so a debug run can set both and stop on the finish screen without a keyboard.
  • In failure_menu() (:597): treat non-interactive as equivalent to the existing OMARCHY_UI_FAILURE_ACTION=exit early return (:600), so a failed autoinstall exits with the installer's status instead of blocking on gum choose forever. The failure screen and log tail still render, and the serial console still captures them.

Why here and not in the caller

The dashboard is the only process that owns the visible UI. Suppressing its prompts anywhere else means editing scripts it invokes — the finished.sh sed from PR #72 — which breaks the moment those scripts move.

Acceptance criteria

  • Autoinstall run reaches the finish screen and reboots with no input.
  • Autoinstall run whose install fails renders the failure screen, exits non-zero, and does not hang.
  • Interactive run still prompts for reboot and still offers the failure menu.
  • OMARCHY_UI_INTERACTIVE=no plus OMARCHY_UI_AUTO_REBOOT=no stops at the finish screen without prompting.

3) SSH keys as an orchestrator phase

Files

  • configs/airootfs/usr/local/bin/omarchy-iso-install
  • configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py
  • configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py
  • configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py

Changes

  • omarchy-iso-install: add --ssh-keys-file to the arg loop, exporting OMARCHY_INSTALL_SSH_KEYS_FILE. Same shape as the five existing flags, ahead of the unknown arg catch-all (:19).
  • context.py: add an ssh_keys_path: Path | None field populated from that variable in from_env, None when unset or when the file does not exist.
  • phases_impl.py: add configure_remote_access(ctx):
    • Return immediately when ctx.ssh_keys_path is None.
    • Parse the file as a JSON array of public key strings; write them one per line to <target>/home/<user>/.ssh/authorized_keys.
    • .ssh at 0700, authorized_keys at 0600, both owned by the user via arch-chroot <target> chown -R <user>:<user>. Do not hardcode uid 1000 as PR #72 did — ask the target for it.
    • A malformed or empty ssh.json must fail the phase loudly. A machine that installs "successfully" and is then unreachable is worse than one that stops with an error on screen.
  • main.py: register ("Configuring remote access", configure_remote_access) in build_phases after configure_login (:52) and before validate_boot.

Model to follow

configure_login (phases_impl.py:1251) — direct writes against ctx.target, arch-chroot reserved for ownership and systemctl. Match its structure.

Acceptance criteria

  • Install with ssh.json: ssh <user>@<vm> works with the corresponding private key on first boot, no password.
  • Install without ssh.json: phase runs, does nothing, install unaffected; ~/.ssh is not created.
  • authorized_keys is 0600 and owned by the install user, not root.
  • Password SSH auth is left at the distro default — autoinstall does not enable it.
  • Interactive installs are unaffected: the flag is passed but the file never exists.

4) Documentation

File

  • README.md

Changes

  • New "Autoinstall" section covering: what the cidata drive is, the file table, how to produce the drive, and a Proxmox qm create example with disk-first boot order so the empty disk falls through to the ISO on the first boot and boots the installed system afterward.
  • Show openssl passwd -6 for the credential hash and the ssh.json array format.
  • State plainly that the config files are the configurator's own output — the documented way to get a starting user_configuration.json is to run one interactive install and copy what it wrote.
  • Do not repeat PR #72's networking claims. Say that sshd and NetworkManager come enabled on a stock install and that autoinstall only adds the key.

5) Validation

Manual matrix

  1. Build the ISO from this branch.
  2. Interactive control run: boot with no cidata drive, install normally, confirm nothing changed.
  3. Autoinstall run on Proxmox: attach ISO + cidata drive, boot, no input; confirm the configurator never renders, the install completes, the machine reboots on its own, and SSH with the key works against the booted system.
  4. Encrypted autoinstall: user_encrypt_installation.txt set true; confirm the LUKS prompt appears on boot (this is the one autoinstall case that still needs a human at first boot — document it).
  5. Full Packer cycle: build, destroy, rebuild from the same cidata drive; confirm reproducibility.

Negative tests

  • Drive labeled cidata with no files → configurator runs.
  • Drive with user_configuration.json only → configurator runs.
  • Malformed ssh.json → install stops in the remote-access phase with a visible error, does not reboot into an unreachable machine.
  • Install failure under autoinstall (e.g. a disk target that does not exist) → failure screen renders, process exits non-zero, no hang.

Regression surface

The interactive path shares every line of this change except the load_cidata branch, so run the standard acceptance harness (./bin/omarchy-iso-test) on the built ISO before proposing the PR.


Relationship to PR #72

PR #72 targeted the pre-quattro tree and cannot be rebased: use_omarchy_helpers, run_configurator, install_arch, install_omarchy, and chroot_bash no longer exist. This plan reimplements the same feature against the current tree and drops three things that PR deliberately flagged as questionable:

  • The finished.sh sed — replaced by section 2. The dashboard renders the reboot prompt now.
  • The systemd-networkd DHCP unit — unnecessary; NetworkManager is already enabled and would contend with it.
  • The PasswordAuthentication yes rewrite — unnecessary and a security downgrade.

load_cidata is the one piece that carries over close to intact.