The JSON wrapper was the one piece of autoinstall input that wasn't an existing format: the other files are the configurator's own output, and the keys end up in sshd's authorized_keys anyway. So take that file directly -- one key per line, comments and key options work as they do everywhere else -- and drop the parsing in between. The only thing JSON bought was parse-error detection, and the phase still fails loudly when the file carries no usable keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 KiB
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
One thing that would otherwise need building is already true on a stock install: NetworkManager.service is enabled and active with DHCP by default, so autoinstall needs no systemd-networkd unit — one would contend with NetworkManager.
Remote access needs three things, all verified against a real install rather than inferred:
authorized_keysfor the user.systemctl enable sshd.service. Omarchy shipsopensshbut leaves the service disabled.ufw allow ssh.install/config/firewall.shopens LocalSend (53317) and docker DNS and nothing else, and ufw runs default-deny incoming.
Still no loosening of PasswordAuthentication — key auth only.
Do not take manifests/fresh-4-semantic.json as the authority on any of this. It claims sshd.service: enabled and carries an allow tcp 22 ufw rule; a real install of this ISO has neither. It is a snapshot of one machine's state, not a contract about stock behavior, and trusting it cost two test cycles.
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
cidatadrive 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
Files
configs/airootfs/usr/local/bin/omarchy-cidata-loadconfigs/airootfs/root/.automated_script.shconfigs/profiledef.sh
Changes
- Add
omarchy-cidata-loadalongside the other install commands in/usr/local/bin(with the matchingfile_permissionsentry inprofiledef.sh). It exits 0 when an autoinstall drive was loaded:udevadm settlefirst: the by-label symlinks come from udev, and probing before enumeration finishes would silently drop a headless install into the wizard.- Look for
/dev/disk/by-label/cidatathen/dev/disk/by-label/CIDATA; exit non-zero if neither exists. - Mount read-only on a temp mountpoint; exit non-zero if the mount fails.
- Require both
user_configuration.jsonanduser_credentials.jsonon the drive — this is the same pairInstallContext.from_envtreats as mandatory. Missing either means the drive is not an autoinstall drive; unmount and exit non-zero. - Copy the required pair plus, when present,
user_full_name.txt,user_email_address.txt,user_encrypt_installation.txt, andauthorized_keysinto/rootwith a singlecp, and exit non-zero if the copy fails — a "loaded" drive with missing inputs would fail the install later with a worse error. - Unmount before exiting. Leave nothing mounted for the install to trip over.
- The script takes an optional path prefix so the unit tests can point it at a sandbox; production callers pass nothing.
- Replace the bare
./configuratorcall in.automated_script.shwith: runomarchy-cidata-load; on success exportOMARCHY_UI_INTERACTIVE=no, on failure run./configurator. - Pass
--authorized-keys-file /root/authorized_keystoomarchy-iso-installin the dashboard invocation, 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_textincontext.pyreturns""for a missing path andencryptfalls back to false. Do not add stricter checks here than the orchestrator itself applies. omarchy-cidata-loadmust not run beforewarm_offline_mirroris backgrounded — the mirror warm should keep its head start.
Acceptance criteria
- ISO with no
cidatadrive attached: configurator runs, byte-identical UX to today. - ISO with a
cidatadrive carrying the five files: configurator never appears; install starts within seconds of boot. - ISO with a drive labeled
cidatathat 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/cidataonce the install begins.
2) Non-interactive dashboard
File
configs/airootfs/usr/local/bin/omarchy-install-dashboard
Changes
- Read
OMARCHY_UI_INTERACTIVEonce near the other env reads at the top. - In
reboot_prompt()(:474): when non-interactive, return 0 immediately instead of callinggum confirm. The existingOMARCHY_UI_AUTO_REBOOT=nocheck at:737stays in front of the actualreboot, 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 existingOMARCHY_UI_FAILURE_ACTION=exitearly return (:600), so a failed autoinstall exits with the installer's status instead of blocking ongum chooseforever. 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=noplusOMARCHY_UI_AUTO_REBOOT=nostops at the finish screen without prompting.
3) SSH keys as an orchestrator phase
Files
configs/airootfs/usr/local/bin/omarchy-iso-installconfigs/airootfs/usr/share/omarchy-iso/orchestrator/context.pyconfigs/airootfs/usr/share/omarchy-iso/orchestrator/main.pyconfigs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py
Changes
omarchy-iso-install: add--authorized-keys-fileto the arg loop, exportingOMARCHY_INSTALL_AUTHORIZED_KEYS_FILE. Same shape as the five existing flags, ahead of theunknown argcatch-all (:19).context.py: add anauthorized_keys_path: Path | Nonefield populated from that variable infrom_env,Nonewhen unset or when the file does not exist.phases_impl.py: addconfigure_ssh_access(ctx):- Return immediately when
ctx.authorized_keys_pathisNone. - Read the file as sshd's own authorized_keys format, dropping blank lines and # comments; write the keys one per line to
<target>/home/<user>/.ssh/authorized_keys. .sshat0700,authorized_keysat0600, both owned by the user viaarch-chroot <target> chown -R <user>:<user>. Do not hardcode uid 1000 as PR #72 did — ask the target for it.arch-chroot <target> systemctl enable sshd.service. Works cleanly in the chroot.arch-chroot <target> ufw allow ssh. This one exits non-zero in a chroot (ERROR: problem running) because ufw cannot reach netfilter, but it writes the rule to/etc/ufw/user.rulesbefore failing, and that file is whatufw.serviceloads on first boot. So ignore the exit status and assert the rule landed in the file instead. No ufw-presence check: ufw is part of Omarchy's default package set, and the rule assertion already fails loudly if it is somehow missing.- An
authorized_keyswith no usable keys 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.
- Return immediately when
main.py: register("Configuring SSH access", configure_ssh_access)inbuild_phasesafterconfigure_login(:52) and beforevalidate_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
authorized_keys:ssh <user>@<vm>works with the corresponding private key on first boot, no password. - Install without
authorized_keys: phase runs, does nothing, install unaffected;~/.sshis not created. authorized_keysis0600and owned by the install user, not root.- Port 22 is open in ufw on the installed system. Test this by connecting, not by reading the phase log — the rule is written from a chroot that cannot verify it.
- 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
cidatadrive is, the file table, how to produce the drive, and a Proxmoxqm createexample 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 -6for the credential hash and theauthorized_keysformat. - State plainly that the config files are the configurator's own output — the documented way to get a starting
user_configuration.jsonis to run one interactive install and copy what it wrote. - Do not repeat PR #72's networking claims. NetworkManager comes enabled with DHCP on a stock install; sshd ships disabled, which is why the SSH phase exists.
5) Validation
Unit tests
./test/all runs everything under test/: cidata-load-test.sh exercises omarchy-cidata-load against a sandbox with mount/umount/udevadm stubbed, and test_configure_ssh_access.py runs the orchestrator phase against a temp target with subprocess.run recorded and ufw's write-the-rule-then-fail chroot behavior simulated. The Python tests run through unittest discovery alongside the repo's existing test_*.py suites.
Manual matrix
- Build the ISO from this branch.
- Interactive control run: boot with no
cidatadrive, install normally, confirm nothing changed. - Autoinstall run on Proxmox: attach ISO +
cidatadrive, 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. - Encrypted autoinstall:
user_encrypt_installation.txtset true; confirm the LUKS prompt appears on boot (this is the one autoinstall case that still needs a human at first boot — document it). - Full Packer cycle: build, destroy, rebuild from the same
cidatadrive; confirm reproducibility.
Negative tests
- Drive labeled
cidatawith no files → configurator runs. - Drive with
user_configuration.jsononly → configurator runs. - An
authorized_keyswith no usable keys → 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 omarchy-cidata-load 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.shsed— replaced by section 2. The dashboard renders the reboot prompt now. - The
systemd-networkdDHCP unit — unnecessary; NetworkManager is already enabled and would contend with it. - The
PasswordAuthentication yesrewrite — unnecessary and a security downgrade.
load_cidata is the one piece that carries over close to intact. PR #72's ufw allow ssh also carries over: it was dropped from the first draft of this plan on the reasoning that an enabled sshd is a reachable sshd, and a test install proved otherwise. That PR was right to include it.