Rename OEM vocabulary to provisioning (ISO)

Matches the runtime rename. The deferred-provisioning install mode replaces
'OEM' everywhere:
  omarchy_install.oem            → omarchy_install.defer_provisioning
  --oem / --oem-file             → --defer-provisioning / --defer-provisioning-file
  OMARCHY_INSTALL_OEM_FILE       → OMARCHY_INSTALL_DEFER_PROVISIONING_FILE
  cidata 'oem' marker            → 'defer-provisioning'
  /var/lib/omarchy/oem/          → /var/lib/omarchy/provisioning/
  /etc/omarchy/oem.key           → /etc/omarchy/provisioning.key
  install/oem/omarchy-oem-setup.service → install/provisioning/omarchy-provision-owner.service
  stage_oem_state phase          → stage_provisioning_state ('Staging provisioning')

Orchestrator internals (OEM_STATE_DIR, _validate_oem_state, ctx.oem, …),
the configurator, cidata-load, the dashboard phase band, the harness
(--oem → --provision), and the tests all move together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Fable 5 committed 2026-08-08 02:37:50 -07:00
1 parent 7c196f8d12
commit 5f2f336aba
12 files changed
+263 -263

No files matched your search

+27 -27
View File
@@ -14,8 +14,8 @@
# Usage: omarchy-iso-test [release/omarchy.iso] [options] # Usage: omarchy-iso-test [release/omarchy.iso] [options]
# #
# --encrypt Drive the encrypted install flow (default: unencrypted) # --encrypt Drive the encrypted install flow (default: unencrypted)
# --oem Drive the OEM install flow: no user during install, the # --provision Drive the deferred-provisioning install flow: no user during install, the
# first boot runs omarchy-oem-setup and creates it there # first boot runs omarchy-provision-owner and creates it there
# --reuse-base Skip the install phase if a base image already exists # --reuse-base Skip the install phase if a base image already exists
# --install-only Stop after producing the installed base image # --install-only Stop after producing the installed base image
# --sync-omarchy DIR Push DIR's test/acceptance into the guest before running # --sync-omarchy DIR Push DIR's test/acceptance into the guest before running
@@ -38,7 +38,7 @@ MEMORY=8192
INSTALL_TIMEOUT=2400 INSTALL_TIMEOUT=2400
BOOT_TIMEOUT=600 BOOT_TIMEOUT=600
ENCRYPT=false ENCRYPT=false
OEM=false PROVISION=false
REUSE_BASE=false REUSE_BASE=false
INSTALL_ONLY=false INSTALL_ONLY=false
KEEP_RUNNING=false KEEP_RUNNING=false
@@ -53,7 +53,7 @@ GUEST_HOSTNAME="omarchy-test"
while (($#)); do while (($#)); do
case "$1" in case "$1" in
--encrypt) ENCRYPT=true ;; --encrypt) ENCRYPT=true ;;
--oem) OEM=true ;; --provision) PROVISION=true ;;
--reuse-base) REUSE_BASE=true ;; --reuse-base) REUSE_BASE=true ;;
--install-only) INSTALL_ONLY=true ;; --install-only) INSTALL_ONLY=true ;;
--keep-running) KEEP_RUNNING=true ;; --keep-running) KEEP_RUNNING=true ;;
@@ -90,7 +90,7 @@ OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
BASE_NAME="$(basename "$ISO" .iso)" BASE_NAME="$(basename "$ISO" .iso)"
$ENCRYPT && BASE_NAME+="-encrypted" $ENCRYPT && BASE_NAME+="-encrypted"
$OEM && BASE_NAME+="-oem" $PROVISION && BASE_NAME+="-provision"
BASE_DIR="$REPO_DIR/test-runs/$BASE_NAME" BASE_DIR="$REPO_DIR/test-runs/$BASE_NAME"
RUN_DIR="$BASE_DIR/runs/$(date +%Y%m%d-%H%M%S)" RUN_DIR="$BASE_DIR/runs/$(date +%Y%m%d-%H%M%S)"
BASE_DISK="$BASE_DIR/base.qcow2" BASE_DISK="$BASE_DIR/base.qcow2"
@@ -606,12 +606,12 @@ audio-panel|meta_l-ctrl-a|omarchy-keyboard-panel|omarchy.audio'
drive_configurator() { drive_configurator() {
log "Driving the installer wizard" log "Driving the installer wizard"
# Keyboard is the first screen. Ctrl+C there is the hidden entry into OEM # Keyboard is the first screen. Ctrl+C there is the hidden entry into deferred provisioning
# mode; a normal install just selects the layout. # mode; a normal install just selects the layout.
wait_for_screen "keyboard layout" 300 wait_for_screen "keyboard layout" 300
capture_console "success-installer-01-keyboard-layout" capture_console "success-installer-01-keyboard-layout"
if $OEM; then if $PROVISION; then
press ctrl-c # arm "prepare for another owner" press ctrl-c # arm "prepare for another owner"
wait_for_screen "another owner" 30 wait_for_screen "another owner" 30
capture_console "success-installer-02-prepare-confirm" capture_console "success-installer-02-prepare-confirm"
@@ -699,30 +699,30 @@ drive_configurator() {
capture_console "success-installer-14-install-started" capture_console "success-installer-14-install-started"
} }
# The OEM first boot runs the user form on tty1 (omarchy-oem-setup) before # The deferred-provisioning first boot runs the user form on tty1 (omarchy-provision-owner) before
# SDDM. Encrypted OEM installs auto-unlock from the staged keyfile, so there # SDDM. Encrypted deferred-provisioning installs auto-unlock from the staged keyfile, so there
# is no LUKS prompt to answer here either way. # is no LUKS prompt to answer here either way.
drive_oem_setup() { drive_provision_owner() {
log "Driving the OEM first-boot setup" log "Driving the deferred-provisioning first-boot setup"
# OEM defers the keyboard step to first boot; it comes before the user form. # Deferred provisioning defers the keyboard step to first boot; it comes before the user form.
# Pick English (UK) (one row above the preselected English (US)) so the # Pick English (UK) (one row above the preselected English (US)) so the
# verifier can prove the deferred step applied a non-default layout — UK # verifier can prove the deferred step applied a non-default layout — UK
# leaves every letter identical to US, so the typed password (sent as raw # leaves every letter identical to US, so the typed password (sent as raw
# keycodes) still matches the literal one the harness pipes to sudo. # keycodes) still matches the literal one the harness pipes to sudo.
wait_for_screen "keyboard layout" 600 wait_for_screen "keyboard layout" 600
press up press up
capture_console "success-oem-01-keyboard-layout-uk" capture_console "success-provision-01-keyboard-layout-uk"
press ret press ret
wait_for_screen "Username" 120 wait_for_screen "Username" 120
capture_console "success-oem-02-username" capture_console "success-provision-02-username"
type_text "$GUEST_USER" type_text "$GUEST_USER"
press ret press ret
wait_for_screen "Password" 60 wait_for_screen "Password" 60
type_text "$GUEST_PASSWORD" type_text "$GUEST_PASSWORD"
capture_console "success-oem-03-password" capture_console "success-provision-03-password"
press ret press ret
wait_for_screen "Conf *irm" 60 wait_for_screen "Conf *irm" 60
@@ -740,16 +740,16 @@ drive_oem_setup() {
type_text "test" type_text "test"
press shift-apostrophe press shift-apostrophe
type_text "omarchy.org" type_text "omarchy.org"
capture_console "success-oem-04-email" capture_console "success-provision-04-email"
press ret press ret
wait_for_screen "look right" 60 wait_for_screen "look right" 60
capture_console "success-oem-05-review" capture_console "success-provision-05-review"
press ret press ret
# Finalization (offline Node unpack, LUKS re-key on encrypted installs) # Finalization (offline Node unpack, LUKS re-key on encrypted installs)
# runs before SDDM appears. # runs before SDDM appears.
capture_console "success-oem-06-finalizing" capture_console "success-provision-06-finalizing"
} }
wait_for_install() { wait_for_install() {
@@ -952,30 +952,30 @@ install_phase() {
drive_configurator drive_configurator
wait_for_install wait_for_install
if $OEM; then if $PROVISION; then
drive_oem_setup drive_provision_owner
elif $ENCRYPT; then elif $ENCRYPT; then
unlock_luks unlock_luks
fi fi
bootstrap_ssh bootstrap_ssh
# SSH can come up (console bootstrap logs in on a spare TTY) while # SSH can come up (console bootstrap logs in on a spare TTY) while
# omarchy-oem-setup is still finalizing the user and re-keying LUKS. Saving # omarchy-provision-owner is still finalizing the user and re-keying LUKS. Saving
# the base mid-finalization produces a flaky image, so wait for the pending # the base mid-finalization produces a flaky image, so wait for the pending
# flag to clear before powering off. # flag to clear before powering off.
if $OEM; then if $PROVISION; then
log "Waiting for OEM first-boot setup to finish" log "Waiting for provisioning first-boot setup to finish"
local waited=0 local waited=0
while ssh_guest "test -f /var/lib/omarchy/oem/pending" 2>/dev/null; do while ssh_guest "test -f /var/lib/omarchy/provisioning/pending" 2>/dev/null; do
if ((waited >= 900)); then if ((waited >= 900)); then
capture_console "failure-oem-finalization-timeout" capture_console "failure-provision-finalization-timeout"
echo "Timed out waiting for OEM first-boot setup to complete" >&2 echo "Timed out waiting for provisioning first-boot setup to complete" >&2
return 1 return 1
fi fi
sleep 10 sleep 10
((waited += 10)) ((waited += 10))
done done
capture_console "success-oem-06-setup-complete" capture_console "success-provision-06-setup-complete"
fi fi
ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/omarchy-install-timing.json 2>/dev/null" \ ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/omarchy-install-timing.json 2>/dev/null" \
+1 -1
View File
@@ -118,4 +118,4 @@ rm -f /run/omarchy-install/state.json
--encrypt-file /root/user_encrypt_installation.txt \ --encrypt-file /root/user_encrypt_installation.txt \
--authorized-keys-file /root/authorized_keys \ --authorized-keys-file /root/authorized_keys \
--tailscale-authkey-file /root/tailscale_authkey \ --tailscale-authkey-file /root/tailscale_authkey \
--oem-file /root/oem --defer-provisioning-file /root/defer-provisioning
+19 -19
View File
@@ -75,13 +75,13 @@ notice() {
keyboard_form() { keyboard_form() {
# The Ctrl+C hint sits directly under the header (no blank line between), # The Ctrl+C hint sits directly under the header (no blank line between),
# matching the disk-overwrite screen's unencrypted offer. Ctrl+C on the first # matching the disk-overwrite screen's unencrypted offer. Ctrl+C on the first
# screen is the hidden entry into OEM mode; $1 is "true" only for that first # screen is the hidden entry into deferred provisioning; $1 is "true" only for that first
# invocation, so the review-loop re-edit can't flip a user install into OEM. # invocation, so the review-loop re-edit can't flip a user install into it.
local allow_oem="${1:-false}" local allow_defer_provisioning="${1:-false}"
clear_logo clear_logo
echo echo
say "Let's setup your machine..." say "Let's setup your machine..."
if $allow_oem; then if $allow_defer_provisioning; then
say --foreground 8 "Press Ctrl+C to prepare this machine for another owner." say --foreground 8 "Press Ctrl+C to prepare this machine for another owner."
fi fi
echo echo
@@ -139,9 +139,9 @@ Ukrainian|ua'
choice=$(printf '%s\n' "$keyboards" | cut -d'|' -f1 | gum choose --height 10 --selected "English (US)" --header "Select keyboard layout") choice=$(printf '%s\n' "$keyboards" | cut -d'|' -f1 | gum choose --height 10 --selected "English (US)" --header "Select keyboard layout")
local status=$? local status=$?
if $allow_oem && (( status == 130 )); then if $allow_defer_provisioning && (( status == 130 )); then
if confirm_prepare_for_another_owner; then if confirm_prepare_for_another_owner; then
oem_install=true defer_provisioning=true
return 0 return 0
fi fi
keyboard_form true # declined: back to the keyboard picker keyboard_form true # declined: back to the keyboard picker
@@ -171,9 +171,9 @@ confirm_prepare_for_another_owner() {
"Prepare this machine for another owner?" "Prepare this machine for another owner?"
} }
# The user step. OEM installs skip it entirely — the machine's first owner # The user step. Deferred-provisioning installs skip it entirely — the machine's first owner
# picks their keyboard and creates their user in the first-boot setup, so the # picks their keyboard and creates their user in the first-boot setup, so the
# OEM operator sets nothing user-specific. # operator sets nothing user-specific.
user_form() { user_form() {
step "Let's setup your user account..." step "Let's setup your user account..."
@@ -386,14 +386,14 @@ wait_for_device() {
# Save the configurator answers the installer consumes: git identity and # Save the configurator answers the installer consumes: git identity and
# encryption choice as plain files, account details as archinstall credentials. # encryption choice as plain files, account details as archinstall credentials.
# OEM installs defer the user to first boot: no credentials, and on encrypted # Deferred-provisioning installs defer the user to first boot: no credentials, and on encrypted
# targets the orchestrator generates a throwaway LUKS passphrase itself. # targets the orchestrator generates a throwaway LUKS passphrase itself.
write_user_files() { write_user_files() {
echo "$full_name" >user_full_name.txt echo "$full_name" >user_full_name.txt
echo "$email_address" >user_email_address.txt echo "$email_address" >user_email_address.txt
printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt
if $oem_install; then if $defer_provisioning; then
cat <<-_EOF_ >user_credentials.json cat <<-_EOF_ >user_credentials.json
{ {
"users": [] "users": []
@@ -895,7 +895,7 @@ confirm_disk_overwrite() {
# Decide what and how to install: sets install_target (full_disk|free_space), # Decide what and how to install: sets install_target (full_disk|free_space),
# install_target and encrypt_installation. Nothing is written to disk yet — # install_target and encrypt_installation. Nothing is written to disk yet —
# the user step runs between this and the execution below. (OEM mode never # the user step runs between this and the execution below. (deferred provisioning never
# reaches here; it's armed by Ctrl+C on the first screen and goes straight to # reaches here; it's armed by Ctrl+C on the first screen and goes straight to
# disk selection.) # disk selection.)
select_installation() { select_installation() {
@@ -939,15 +939,15 @@ select_installation() {
done done
} }
# STEP 1: KEYBOARD (Ctrl+C here arms OEM mode and jumps to disk selection) # STEP 1: KEYBOARD (Ctrl+C here arms deferred provisioning and jumps to disk selection)
oem_install=false defer_provisioning=false
install_target="full_disk" install_target="full_disk"
keyboard_form true keyboard_form true
if $oem_install; then if $defer_provisioning; then
# OEM: no user setup, and the keyboard is deferred to the machine's first # Deferred provisioning: no user setup, and the keyboard is deferred to the machine's first
# boot. A defined default keymap so it boots before then; the owner picks # boot. A defined default keymap so it boots before then; the owner picks
# their real layout in the OOBE, which re-keys the console and, on encrypted # their real layout in the OOBE, which re-keys the console and, on encrypted
# installs, rebuilds the initramfs so the LUKS prompt matches. # installs, rebuilds the initramfs so the LUKS prompt matches.
@@ -961,7 +961,7 @@ if $oem_install; then
timezone="UTC" timezone="UTC"
# Jump straight to disk selection + overwrite confirmation (encrypted by # Jump straight to disk selection + overwrite confirmation (encrypted by
# default, Ctrl+C toggles unencrypted). OEM is always a full-disk install. # default, Ctrl+C toggles unencrypted). deferred provisioning is always a full-disk install.
disk_form disk_form
until confirm_disk_overwrite; do until confirm_disk_overwrite; do
disk_form disk_form
@@ -1005,9 +1005,9 @@ main_partition_size=$((disk_size_in_mib - main_partition_start - gpt_backup_rese
kernel_choice=$(detect_kernel) kernel_choice=$(detect_kernel)
if [[ $encrypt_installation == true ]]; then if [[ $encrypt_installation == true ]]; then
# OEM installs carry no password: the orchestrator generates a throwaway # Deferred-provisioning installs carry no password: the orchestrator generates a throwaway
# LUKS passphrase and stages it for the first-boot re-key. # LUKS passphrase and stages it for the first-boot re-key.
if $oem_install; then if $defer_provisioning; then
encryption_password_line="" encryption_password_line=""
else else
encryption_password_line=", encryption_password_line=",
@@ -1037,7 +1037,7 @@ cat <<-_EOF_ >user_configuration.json
"custom_commands": [], "custom_commands": [],
"omarchy_install": { "omarchy_install": {
"mode": "full_disk", "mode": "full_disk",
"oem": $oem_install, "defer_provisioning": $defer_provisioning,
"target_mount": "/mnt", "target_mount": "/mnt",
"boot": { "boot": {
"esp_mount": "/boot", "esp_mount": "/boot",
@@ -11,8 +11,8 @@
# Anything less than the pair means this isn't an autoinstall drive, and the # Anything less than the pair means this isn't an autoinstall drive, and the
# caller falls back to the wizard. # caller falls back to the wizard.
# #
# An `oem` marker file stands in for user_credentials.json: OEM installs defer # A `defer-provisioning` marker file stands in for user_credentials.json: deferred-provisioning
# user creation to first boot, so imaging rigs ship no credentials at all. # installs defer user creation to first boot, so imaging rigs ship no credentials.
# #
# The optional argument prefixes every path the script touches; tests use it to # The optional argument prefixes every path the script touches; tests use it to
# run against a sandbox instead of the live system. # run against a sandbox instead of the live system.
@@ -39,17 +39,17 @@ mkdir -p "$mnt"
mount -o ro "$device" "$mnt" || exit 1 mount -o ro "$device" "$mnt" || exit 1
# Clear any inputs a previous load in this live session left in /root before # Clear any inputs a previous load in this live session left in /root before
# copying the current drive: a stale `oem` marker or credentials file would # copying the current drive: a stale `defer-provisioning` marker or credentials file would
# otherwise force a fresh drive's config back into OEM mode or hand it an old # otherwise force a fresh drive back into deferred provisioning or hand it an old
# LUKS passphrase. The interactive wizard writes these same names, but a # LUKS passphrase. The interactive wizard writes these same names, but a
# cidata load always precedes it, so nothing of the user's is discarded here. # cidata load always precedes it, so nothing of the user's is discarded here.
optional_inputs=(user_credentials.json oem user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys tailscale_authkey) optional_inputs=(user_credentials.json defer-provisioning user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys tailscale_authkey)
for file in user_configuration.json "${optional_inputs[@]}"; do for file in user_configuration.json "${optional_inputs[@]}"; do
rm -f "$dest/$file" rm -f "$dest/$file"
done done
loaded=1 loaded=1
if [[ -f $mnt/user_configuration.json ]] && [[ -f $mnt/user_credentials.json || -f $mnt/oem ]]; then if [[ -f $mnt/user_configuration.json ]] && [[ -f $mnt/user_credentials.json || -f $mnt/defer-provisioning ]]; then
files=("$mnt"/user_configuration.json) files=("$mnt"/user_configuration.json)
for file in "${optional_inputs[@]}"; do for file in "${optional_inputs[@]}"; do
[[ -f $mnt/$file ]] && files+=("$mnt/$file") [[ -f $mnt/$file ]] && files+=("$mnt/$file")
@@ -321,7 +321,7 @@ install_progress() {
"Installing Arch + Omarchy") lo=65 hi=700 tau=150 pkg_signal=1 ;; "Installing Arch + Omarchy") lo=65 hi=700 tau=150 pkg_signal=1 ;;
"Configuring hibernation") lo=700 hi=720 tau=10 ;; "Configuring hibernation") lo=700 hi=720 tau=10 ;;
"Configuring system") lo=720 hi=850 tau=120 ;; "Configuring system") lo=720 hi=850 tau=120 ;;
"Staging OEM state") lo=850 hi=855 tau=5 ;; "Staging provisioning") lo=850 hi=855 tau=5 ;;
"Finalizing Limine boot") lo=855 hi=950 tau=90 ;; "Finalizing Limine boot") lo=855 hi=950 tau=90 ;;
"Finalizing user") lo=950 hi=978 tau=30 ;; "Finalizing user") lo=950 hi=978 tau=30 ;;
"Configuring login") lo=978 hi=988 tau=5 ;; "Configuring login") lo=978 hi=988 tau=5 ;;
@@ -18,7 +18,7 @@ while [[ $# -gt 0 ]]; do
--encrypt-file) export OMARCHY_INSTALL_ENCRYPT_FILE="$2"; shift 2 ;; --encrypt-file) export OMARCHY_INSTALL_ENCRYPT_FILE="$2"; shift 2 ;;
--authorized-keys-file) export OMARCHY_INSTALL_AUTHORIZED_KEYS_FILE="$2"; shift 2 ;; --authorized-keys-file) export OMARCHY_INSTALL_AUTHORIZED_KEYS_FILE="$2"; shift 2 ;;
--tailscale-authkey-file) export OMARCHY_INSTALL_TAILSCALE_AUTHKEY_FILE="$2"; shift 2 ;; --tailscale-authkey-file) export OMARCHY_INSTALL_TAILSCALE_AUTHKEY_FILE="$2"; shift 2 ;;
--oem-file) export OMARCHY_INSTALL_OEM_FILE="$2"; shift 2 ;; --defer-provisioning-file) export OMARCHY_INSTALL_DEFER_PROVISIONING_FILE="$2"; shift 2 ;;
*) echo "omarchy-iso-install: unknown arg $1" >&2; exit 2 ;; *) echo "omarchy-iso-install: unknown arg $1" >&2; exit 2 ;;
esac esac
done done
@@ -26,7 +26,7 @@ class InstallContext:
user_credentials: dict user_credentials: dict
arch_config_path: Path arch_config_path: Path
omarchy_install: dict[str, Any] omarchy_install: dict[str, Any]
oem: bool = False defer_provisioning: bool = False
target: Path = Path("/mnt") target: Path = Path("/mnt")
omarchy_path: Path = Path("/usr/share/omarchy") omarchy_path: Path = Path("/usr/share/omarchy")
@@ -50,23 +50,23 @@ class InstallContext:
user_configuration = json.loads(config_path.read_text()) user_configuration = json.loads(config_path.read_text())
omarchy_install = user_configuration.get("omarchy_install") or _default_omarchy_install(user_configuration) omarchy_install = user_configuration.get("omarchy_install") or _default_omarchy_install(user_configuration)
# OEM mode: the whole system installs but user creation is deferred to # Deferred provisioning: the whole system installs but user creation is deferred to
# first boot. Selected by the configurator (omarchy_install.oem) or by # first boot. Selected by the configurator (omarchy_install.defer_provisioning) or by
# an `oem` marker file on an autoinstall drive, which also replaces the # an `defer-provisioning` marker file on an autoinstall drive, which also replaces the
# user_credentials.json requirement. # user_credentials.json requirement.
oem_marker = _optional_path(os.environ.get("OMARCHY_INSTALL_OEM_FILE")) defer_provisioning_marker = _optional_path(os.environ.get("OMARCHY_INSTALL_DEFER_PROVISIONING_FILE"))
oem = bool(omarchy_install.get("oem")) or oem_marker is not None defer_provisioning = bool(omarchy_install.get("defer_provisioning")) or defer_provisioning_marker is not None
omarchy_install["oem"] = oem omarchy_install["defer_provisioning"] = defer_provisioning
if creds_path.exists(): if creds_path.exists():
user_credentials = json.loads(creds_path.read_text()) user_credentials = json.loads(creds_path.read_text())
elif oem: elif defer_provisioning:
user_credentials = {"users": []} user_credentials = {"users": []}
else: else:
raise RuntimeError(f"credentials file missing: {creds_path}") raise RuntimeError(f"credentials file missing: {creds_path}")
if oem: if defer_provisioning:
# OEM promises no account exists until first boot. A supplied # Deferred provisioning promises no account until first boot. A supplied
# credentials file (a rig handing over its LUKS passphrase) must # credentials file (a rig handing over its LUKS passphrase) must
# not smuggle in users or a root password. # not smuggle in users or a root password.
encryption_password = user_credentials.get("encryption_password") encryption_password = user_credentials.get("encryption_password")
@@ -77,7 +77,7 @@ class InstallContext:
arch_configuration = dict(user_configuration) arch_configuration = dict(user_configuration)
arch_configuration.pop("omarchy_install", None) arch_configuration.pop("omarchy_install", None)
if oem: if defer_provisioning:
# The userless invariant covers the archinstall config too: a # The userless invariant covers the archinstall config too: a
# combined/legacy config could carry account authentication that # combined/legacy config could carry account authentication that
# archinstall would still act on (users, a root password). Strip # archinstall would still act on (users, a root password). Strip
@@ -87,14 +87,14 @@ class InstallContext:
state_dir = Path(os.environ.get("OMARCHY_INSTALL_STATE_DIR", "/run/omarchy-install")) state_dir = Path(os.environ.get("OMARCHY_INSTALL_STATE_DIR", "/run/omarchy-install"))
state_dir.mkdir(parents=True, exist_ok=True) state_dir.mkdir(parents=True, exist_ok=True)
if oem: if defer_provisioning:
# Encrypted OEM installs have no user password to protect LUKS # Encrypted deferred-provisioning installs have no user password to protect LUKS
# with. Generate a throwaway passphrase (staged for first-boot # with. Generate a throwaway passphrase (staged for first-boot
# re-key by the stage_oem_state phase) and hand it to archinstall # re-key by the stage_provisioning_state phase) and hand it to archinstall
# through both places it may look: the disk_encryption block and # through both places it may look: the disk_encryption block and
# the credentials file. # the credentials file.
_inject_oem_encryption_password(arch_configuration, user_credentials) _inject_provisioning_encryption_password(arch_configuration, user_credentials)
creds_path = state_dir / "oem-user_credentials.json" creds_path = state_dir / "provisioning-user_credentials.json"
creds_path.write_text(json.dumps(user_credentials, indent=2) + "\n") creds_path.write_text(json.dumps(user_credentials, indent=2) + "\n")
creds_path.chmod(0o600) creds_path.chmod(0o600)
@@ -113,7 +113,7 @@ class InstallContext:
user_credentials=user_credentials, user_credentials=user_credentials,
arch_config_path=arch_config_path, arch_config_path=arch_config_path,
omarchy_install=omarchy_install, omarchy_install=omarchy_install,
oem=oem, defer_provisioning=defer_provisioning,
state_dir=state_dir, state_dir=state_dir,
) )
disk_config = user_configuration.get("disk_config", {}) disk_config = user_configuration.get("disk_config", {})
@@ -127,7 +127,7 @@ class InstallContext:
users = self.user_credentials.get("users") or [] users = self.user_credentials.get("users") or []
if users: if users:
return users[0]["username"] return users[0]["username"]
if self.oem: if self.defer_provisioning:
return "" return ""
raise RuntimeError("user_credentials.json contains no users") raise RuntimeError("user_credentials.json contains no users")
@@ -145,7 +145,7 @@ class InstallContext:
def _strip_account_fields(arch_configuration: dict) -> None: def _strip_account_fields(arch_configuration: dict) -> None:
"""Remove every field archinstall reads to create users or set a root """Remove every field archinstall reads to create users or set a root
password, so an OEM install cannot ship a hidden provisioning account. password, so it cannot ship a hidden provisioning account.
Encryption material lives elsewhere (disk_config.disk_encryption) and is Encryption material lives elsewhere (disk_config.disk_encryption) and is
untouched.""" untouched."""
for key in ("!users", "!root-password", "root_enc_password", "users"): for key in ("!users", "!root-password", "root_enc_password", "users"):
@@ -157,12 +157,12 @@ def _strip_account_fields(arch_configuration: dict) -> None:
auth.pop(key, None) auth.pop(key, None)
def _inject_oem_encryption_password(arch_configuration: dict, user_credentials: dict) -> None: def _inject_provisioning_encryption_password(arch_configuration: dict, user_credentials: dict) -> None:
"""Ensure an encrypted OEM install has a LUKS passphrase archinstall can """Ensure an encrypted deferred-provisioning install has a LUKS passphrase archinstall can
use. Reuse one the input already carries (an autoinstall rig may supply use. Reuse one the input already carries (an autoinstall rig may supply
its own); otherwise generate a throwaway. Mutates the disk_encryption its own); otherwise generate a throwaway. Mutates the disk_encryption
block in place — arch_configuration shares it with user_configuration, so block in place — arch_configuration shares it with user_configuration, so
the stage_oem_state phase can read the effective passphrase back.""" the stage_provisioning_state phase can read the effective passphrase back."""
disk_encryption = (arch_configuration.get("disk_config") or {}).get("disk_encryption") disk_encryption = (arch_configuration.get("disk_config") or {}).get("disk_encryption")
if disk_encryption is None: if disk_encryption is None:
return return
@@ -34,7 +34,7 @@ def build_phases(ctx: InstallContext):
arch_install_system, arch_install_system,
configure_hibernation, configure_hibernation,
run_system_finalizer, run_system_finalizer,
stage_oem_state, stage_provisioning_state,
finalize_limine_boot, finalize_limine_boot,
run_chroot_finalizer, run_chroot_finalizer,
configure_dns_resolver, configure_dns_resolver,
@@ -51,9 +51,9 @@ def build_phases(ctx: InstallContext):
("Installing Arch + Omarchy", arch_install_system), ("Installing Arch + Omarchy", arch_install_system),
("Configuring hibernation", configure_hibernation), ("Configuring hibernation", configure_hibernation),
("Configuring system", run_system_finalizer), ("Configuring system", run_system_finalizer),
# Before finalize_limine_boot: the OEM cryptkey drop-in and keyfile # Before finalize_limine_boot: the deferred-provisioning cryptkey drop-in and keyfile
# must be in place for the final UKI build. # must be in place for the final UKI build.
("Staging OEM state", stage_oem_state), ("Staging provisioning", stage_provisioning_state),
("Finalizing Limine boot", finalize_limine_boot), ("Finalizing Limine boot", finalize_limine_boot),
("Finalizing user", run_chroot_finalizer), ("Finalizing user", run_chroot_finalizer),
("Configuring login", configure_login), ("Configuring login", configure_login),
@@ -72,7 +72,7 @@ def main() -> int:
error(f"Configuration error: {e}") error(f"Configuration error: {e}")
return 2 return 2
who = ctx.username or "OEM (user created at first boot)" who = ctx.username or "deferred provisioning (user created at first boot)"
info(f"Installing Omarchy for {who} → {ctx.target}") info(f"Installing Omarchy for {who} → {ctx.target}")
from .phases_impl import ( from .phases_impl import (
@@ -1136,8 +1136,8 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
def run_system_finalizer(ctx: InstallContext) -> None: def run_system_finalizer(ctx: InstallContext) -> None:
if ctx.oem: if ctx.defer_provisioning:
cmd = ["/usr/bin/omarchy-setup-system", "--oem", "--first-install"] cmd = ["/usr/bin/omarchy-setup-system", "--defer-provisioning", "--first-install"]
else: else:
cmd = ["/usr/bin/omarchy-setup-system", "--install-user", ctx.username, "--first-install"] cmd = ["/usr/bin/omarchy-setup-system", "--install-user", ctx.username, "--first-install"]
@@ -1149,66 +1149,66 @@ def run_system_finalizer(ctx: InstallContext) -> None:
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# stage_oem_state: produce the on-disk "OEM state" the runtime's first-boot # stage_provisioning_state: produce the on-disk "provisioning state" the runtime's first-boot
# setup (omarchy-oem-setup) and factory reset (omarchy-system-factory-reset) consume. # setup (omarchy-provision-owner) and factory reset (omarchy-system-factory-reset) consume.
# #
# Every install stashes the bundled Node tarball in /var/lib/omarchy/oem/ # Every install stashes the bundled Node tarball in /var/lib/omarchy/provisioning/
# so a later factory reset can finalize the new owner's user offline. OEM-mode # so a later factory reset can finalize the new owner's user offline. deferred-provisioning
# installs additionally arm the first-boot setup service and, on encrypted # installs additionally arm the first-boot setup service and, on encrypted
# targets, stage the throwaway LUKS passphrase: the keyfile embedded in the # targets, stage the throwaway LUKS passphrase: the keyfile embedded in the
# initramfs auto-unlocks boot during the OEM window, and first-boot setup # initramfs auto-unlocks boot during the provisioning window, and first-boot setup
# re-keys the volume to the owner's password and removes it. # re-keys the volume to the owner's password and removes it.
# #
# Runs before finalize_limine_boot so the cryptkey cmdline drop-in and the # Runs before finalize_limine_boot so the cryptkey cmdline drop-in and the
# keyfile land in the final UKI build. # keyfile land in the final UKI build.
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
OEM_STATE_DIR = "var/lib/omarchy/oem" PROVISION_STATE_DIR = "var/lib/omarchy/provisioning"
OEM_KEYFILE = "etc/omarchy/oem.key" PROVISION_KEYFILE = "etc/omarchy/provisioning.key"
NODE_PACKAGES_DIR = Path("/opt/packages") NODE_PACKAGES_DIR = Path("/opt/packages")
def stage_oem_state(ctx: InstallContext) -> None: def stage_provisioning_state(ctx: InstallContext) -> None:
# World-readable: first-boot finalization reads the Node tarball as the # World-readable: first-boot finalization reads the Node tarball as the
# new user. The only secret inside (luks-key) is itself 0600 root. # new user. The only secret inside (luks-key) is itself 0600 root.
oem_dir = ctx.target / OEM_STATE_DIR provisioning_dir = ctx.target / PROVISION_STATE_DIR
oem_dir.mkdir(parents=True, exist_ok=True) provisioning_dir.mkdir(parents=True, exist_ok=True)
oem_dir.chmod(0o755) provisioning_dir.chmod(0o755)
_stage_node_tarball(ctx, oem_dir) _stage_node_tarball(ctx, provisioning_dir)
if not ctx.oem: if not ctx.defer_provisioning:
return return
service_src = ctx.target / "usr/share/omarchy/install/oem/omarchy-oem-setup.service" service_src = ctx.target / "usr/share/omarchy/install/provisioning/omarchy-provision-owner.service"
setup_bin = ctx.target / "usr/bin/omarchy-oem-setup" setup_bin = ctx.target / "usr/bin/omarchy-provision-owner"
if not service_src.exists() or not setup_bin.exists(): if not service_src.exists() or not setup_bin.exists():
raise RuntimeError( raise RuntimeError(
"OEM install requested, but the installed Omarchy runtime does not ship " "deferred-provisioning install requested, but the installed Omarchy runtime does not ship "
"first-boot setup (omarchy-oem-setup + install/oem/omarchy-oem-setup.service). " "first-boot setup (omarchy-provision-owner + install/provisioning/omarchy-provision-owner.service). "
"Update the runtime package this ISO bundles before installing in OEM mode." "Update the runtime package this ISO bundles before installing in deferred provisioning."
) )
info("› arming first-boot OEM setup") info("› arming first-boot setup")
(oem_dir / "pending").touch() (provisioning_dir / "pending").touch()
unit_dst = ctx.target / "etc/systemd/system/omarchy-oem-setup.service" unit_dst = ctx.target / "etc/systemd/system/omarchy-provision-owner.service"
unit_dst.parent.mkdir(parents=True, exist_ok=True) unit_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(service_src, unit_dst) shutil.copy2(service_src, unit_dst)
wants_dir = ctx.target / "etc/systemd/system/multi-user.target.wants" wants_dir = ctx.target / "etc/systemd/system/multi-user.target.wants"
wants_dir.mkdir(parents=True, exist_ok=True) wants_dir.mkdir(parents=True, exist_ok=True)
link = wants_dir / "omarchy-oem-setup.service" link = wants_dir / "omarchy-provision-owner.service"
link.unlink(missing_ok=True) link.unlink(missing_ok=True)
link.symlink_to("/etc/systemd/system/omarchy-oem-setup.service") link.symlink_to("/etc/systemd/system/omarchy-provision-owner.service")
if _oem_install_encrypted(ctx): if _provision_install_encrypted(ctx):
_stage_oem_luks_unlock(ctx, oem_dir) _stage_provisioning_luks_unlock(ctx, provisioning_dir)
def _stage_node_tarball(ctx: InstallContext, oem_dir) -> None: def _stage_node_tarball(ctx: InstallContext, provisioning_dir) -> None:
tarballs = sorted(NODE_PACKAGES_DIR.glob("node-v*-linux-x64.tar.gz")) tarballs = sorted(NODE_PACKAGES_DIR.glob("node-v*-linux-x64.tar.gz"))
if not tarballs: if not tarballs:
# Hard error on every install, not just OEM: the stash is what lets a # Hard error on every install, not just deferred-provisioning installs: the stash is what lets a
# later factory reset finalize the next owner offline, and an ISO # later factory reset finalize the next owner offline, and an ISO
# build always bundles the tarball — its absence means a broken build. # build always bundles the tarball — its absence means a broken build.
raise RuntimeError( raise RuntimeError(
@@ -1216,7 +1216,7 @@ def _stage_node_tarball(ctx: InstallContext, oem_dir) -> None:
"and factory reset could not finalize a user offline" "and factory reset could not finalize a user offline"
) )
packages_dir = oem_dir / "packages" packages_dir = provisioning_dir / "packages"
packages_dir.mkdir(parents=True, exist_ok=True) packages_dir.mkdir(parents=True, exist_ok=True)
target_tarball = packages_dir / tarballs[0].name target_tarball = packages_dir / tarballs[0].name
if not target_tarball.exists(): if not target_tarball.exists():
@@ -1224,7 +1224,7 @@ def _stage_node_tarball(ctx: InstallContext, oem_dir) -> None:
shutil.copy2(tarballs[0], target_tarball) shutil.copy2(tarballs[0], target_tarball)
def _oem_install_encrypted(ctx: InstallContext) -> bool: def _provision_install_encrypted(ctx: InstallContext) -> bool:
if _storage_intent(ctx).get("luks_uuid"): if _storage_intent(ctx).get("luks_uuid"):
return True return True
disk_encryption = (ctx.user_configuration.get("disk_config") or {}).get("disk_encryption") disk_encryption = (ctx.user_configuration.get("disk_config") or {}).get("disk_encryption")
@@ -1233,43 +1233,43 @@ def _oem_install_encrypted(ctx: InstallContext) -> bool:
return ctx.encrypt return ctx.encrypt
def _oem_encryption_password(ctx: InstallContext) -> str | None: def _provision_encryption_password(ctx: InstallContext) -> str | None:
disk_encryption = (ctx.user_configuration.get("disk_config") or {}).get("disk_encryption") or {} disk_encryption = (ctx.user_configuration.get("disk_config") or {}).get("disk_encryption") or {}
return disk_encryption.get("encryption_password") or ctx.user_credentials.get("encryption_password") return disk_encryption.get("encryption_password") or ctx.user_credentials.get("encryption_password")
def _stage_oem_luks_unlock(ctx: InstallContext, oem_dir) -> None: def _stage_provisioning_luks_unlock(ctx: InstallContext, provisioning_dir) -> None:
password = _oem_encryption_password(ctx) password = _provision_encryption_password(ctx)
if not password: if not password:
# Full-disk OEM installs get a generated passphrase injected by # Full-disk deferred-provisioning installs get a generated passphrase injected by
# InstallContext; only a pre-mounted (rig-partitioned) LUKS target can # InstallContext; only a pre-mounted (rig-partitioned) LUKS target can
# land here, and it must hand over the passphrase it formatted with. # land here, and it must hand over the passphrase it formatted with.
raise RuntimeError( raise RuntimeError(
"OEM install on a pre-encrypted target requires the LUKS passphrase " "deferred-provisioning install on a pre-encrypted target requires the LUKS passphrase "
"in user_credentials.json (encryption_password) so first boot can re-key" "in user_credentials.json (encryption_password) so first boot can re-key"
) )
info("› staging LUKS auto-unlock for the OEM window") info("› staging LUKS auto-unlock for the provisioning window")
# Byte-for-byte the slot passphrase: no trailing newline anywhere. # Byte-for-byte the slot passphrase: no trailing newline anywhere.
luks_key = oem_dir / "luks-key" luks_key = provisioning_dir / "luks-key"
luks_key.write_text(password) luks_key.write_text(password)
luks_key.chmod(0o600) luks_key.chmod(0o600)
keyfile = ctx.target / OEM_KEYFILE keyfile = ctx.target / PROVISION_KEYFILE
keyfile.parent.mkdir(parents=True, exist_ok=True) keyfile.parent.mkdir(parents=True, exist_ok=True)
keyfile.write_text(password) keyfile.write_text(password)
keyfile.chmod(0o600) keyfile.chmod(0o600)
cmdline_dropin = ctx.target / "etc/limine-entry-tool.d/99-omarchy-oem-unlock.conf" cmdline_dropin = ctx.target / "etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf"
cmdline_dropin.parent.mkdir(parents=True, exist_ok=True) cmdline_dropin.parent.mkdir(parents=True, exist_ok=True)
cmdline_dropin.write_text( cmdline_dropin.write_text(
'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/oem.key"\n' 'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/provisioning.key"\n'
) )
files_dropin = ctx.target / "etc/mkinitcpio.conf.d/99-omarchy-oem-key.conf" files_dropin = ctx.target / "etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf"
files_dropin.parent.mkdir(parents=True, exist_ok=True) files_dropin.parent.mkdir(parents=True, exist_ok=True)
files_dropin.write_text("FILES+=(/etc/omarchy/oem.key)\n") files_dropin.write_text("FILES+=(/etc/omarchy/provisioning.key)\n")
def finalize_limine_boot(ctx: InstallContext) -> None: def finalize_limine_boot(ctx: InstallContext) -> None:
@@ -1365,8 +1365,8 @@ def _limine_kernel_cmdline(config_text: str) -> str:
def run_chroot_finalizer(ctx: InstallContext) -> None: def run_chroot_finalizer(ctx: InstallContext) -> None:
if ctx.oem: if ctx.defer_provisioning:
info("› OEM install: user finalization deferred to first boot") info("› deferred-provisioning install: user finalization deferred to first boot")
return return
_run_target_setup_command( _run_target_setup_command(
@@ -1415,18 +1415,18 @@ def configure_login(ctx: InstallContext) -> None:
) )
autologin_conf = sddm_dir / "autologin.conf" autologin_conf = sddm_dir / "autologin.conf"
if ctx.encrypt and not ctx.oem: if ctx.encrypt and not ctx.defer_provisioning:
autologin_conf.write_text( autologin_conf.write_text(
"[Autologin]\n" "[Autologin]\n"
f"User={ctx.username}\n" f"User={ctx.username}\n"
"Session=omarchy.desktop\n" "Session=omarchy.desktop\n"
) )
else: else:
# OEM installs have no user yet; omarchy-oem-setup writes autologin # deferred-provisioning installs have no user yet; omarchy-provision-owner writes autologin
# and SDDM state at first boot once the owner exists. # and SDDM state at first boot once the owner exists.
autologin_conf.unlink(missing_ok=True) autologin_conf.unlink(missing_ok=True)
if not ctx.oem: if not ctx.defer_provisioning:
state_dir = ctx.target / "var" / "lib" / "sddm" state_dir = ctx.target / "var" / "lib" / "sddm"
state_dir.mkdir(parents=True, exist_ok=True) state_dir.mkdir(parents=True, exist_ok=True)
(state_dir / "state.conf").write_text( (state_dir / "state.conf").write_text(
@@ -1460,14 +1460,14 @@ def configure_ssh_access(ctx: InstallContext) -> None:
keys = _authorized_keys(ctx.authorized_keys_path) keys = _authorized_keys(ctx.authorized_keys_path)
if ctx.oem: if ctx.defer_provisioning:
# No user to authorize yet. Stage the keys in OEM state for # No user to authorize yet. Stage the keys in provisioning state for
# omarchy-oem-setup to install once first boot creates the owner, and # omarchy-provision-owner to install once first boot creates the owner, and
# still open the door (sshd + ufw) below. # still open the door (sshd + ufw) below.
info(f"› staging {len(keys)} SSH key(s) for the first-boot user") info(f"› staging {len(keys)} SSH key(s) for the first-boot user")
oem_dir = ctx.target / OEM_STATE_DIR provisioning_dir = ctx.target / PROVISION_STATE_DIR
oem_dir.mkdir(parents=True, exist_ok=True) provisioning_dir.mkdir(parents=True, exist_ok=True)
staged = oem_dir / "authorized_keys" staged = provisioning_dir / "authorized_keys"
staged.write_text("".join(f"{key}\n" for key in keys)) staged.write_text("".join(f"{key}\n" for key in keys))
staged.chmod(0o600) staged.chmod(0o600)
else: else:
@@ -1695,33 +1695,33 @@ def validate_boot(ctx: InstallContext) -> None:
if ctx.is_protected: if ctx.is_protected:
_validate_pre_mounted_filesystems(ctx) _validate_pre_mounted_filesystems(ctx)
if ctx.oem: if ctx.defer_provisioning:
_validate_oem_state(ctx) _validate_provisioning_state(ctx)
def _validate_oem_state(ctx: InstallContext) -> None: def _validate_provisioning_state(ctx: InstallContext) -> None:
"""An OEM install that boots without a working first-boot setup is a """An deferred-provisioning install that boots without a working first-boot setup is a
user-less brick; insist the armed state is complete before reboot.""" user-less brick; insist the armed state is complete before reboot."""
oem_dir = ctx.target / OEM_STATE_DIR provisioning_dir = ctx.target / PROVISION_STATE_DIR
if not (oem_dir / "pending").exists(): if not (provisioning_dir / "pending").exists():
raise RuntimeError(f"OEM install but {oem_dir / 'pending'} is missing") raise RuntimeError(f"deferred-provisioning install but {provisioning_dir / 'pending'} is missing")
link = ctx.target / "etc/systemd/system/multi-user.target.wants/omarchy-oem-setup.service" link = ctx.target / "etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service"
if not link.is_symlink(): if not link.is_symlink():
raise RuntimeError("OEM install but omarchy-oem-setup.service is not enabled") raise RuntimeError("deferred-provisioning install but omarchy-provision-owner.service is not enabled")
if not list((oem_dir / "packages").glob("node-v*.tar.gz")): if not list((provisioning_dir / "packages").glob("node-v*.tar.gz")):
raise RuntimeError(f"OEM install but no Node tarball staged in {oem_dir / 'packages'}") raise RuntimeError(f"deferred-provisioning install but no Node tarball staged in {provisioning_dir / 'packages'}")
if _oem_install_encrypted(ctx): if _provision_install_encrypted(ctx):
for required in (oem_dir / "luks-key", ctx.target / OEM_KEYFILE): for required in (provisioning_dir / "luks-key", ctx.target / PROVISION_KEYFILE):
if not required.exists(): if not required.exists():
raise RuntimeError(f"encrypted OEM install but {required} is missing") raise RuntimeError(f"encrypted deferred-provisioning install but {required} is missing")
limine_conf_text = ( limine_conf_text = (
ctx.target / _boot_intent(ctx)["esp_mount"].lstrip("/") / "limine.conf" ctx.target / _boot_intent(ctx)["esp_mount"].lstrip("/") / "limine.conf"
).read_text() ).read_text()
if "cryptkey=rootfs:" not in limine_conf_text: if "cryptkey=rootfs:" not in limine_conf_text:
raise RuntimeError("encrypted OEM install but limine.conf has no cryptkey= for auto-unlock") raise RuntimeError("encrypted deferred-provisioning install but limine.conf has no cryptkey= for auto-unlock")
def _assert_boot_hooks_restored(ctx: InstallContext) -> None: def _assert_boot_hooks_restored(ctx: InstallContext) -> None:
@@ -1835,15 +1835,15 @@ def create_factory_snapshot(ctx: InstallContext) -> None:
# Provisioning credentials staged for THIS deployment's first boot must not # Provisioning credentials staged for THIS deployment's first boot must not
# survive into the factory image: a reset years later would otherwise hand the # survive into the factory image: a reset years later would otherwise hand the
# next owner the original deployment's SSH keys or rejoin its tailnet, and a # next owner the original deployment's SSH keys or rejoin its tailnet, and a
# stale OEM LUKS key (dead after the first re-key) has no business lingering. # stale LUKS key (dead after the first re-key) has no business lingering.
# The mkinitcpio/cmdline drop-ins go with the keyfile — a reset rebuild would # The mkinitcpio/cmdline drop-ins go with the keyfile — a reset rebuild would
# otherwise fail on FILES pointing at a scrubbed path. # otherwise fail on FILES pointing at a scrubbed path.
FACTORY_SCRUB_PATHS = ( FACTORY_SCRUB_PATHS = (
"var/lib/omarchy/oem/authorized_keys", "var/lib/omarchy/provisioning/authorized_keys",
"var/lib/omarchy/oem/luks-key", "var/lib/omarchy/provisioning/luks-key",
"etc/omarchy/oem.key", "etc/omarchy/provisioning.key",
"etc/limine-entry-tool.d/99-omarchy-oem-unlock.conf", "etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf",
"etc/mkinitcpio.conf.d/99-omarchy-oem-key.conf", "etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf",
"etc/tailscale/authkey", "etc/tailscale/authkey",
"etc/systemd/system/omarchy-tailscale-join.service", "etc/systemd/system/omarchy-tailscale-join.service",
"etc/systemd/system/multi-user.target.wants/omarchy-tailscale-join.service", "etc/systemd/system/multi-user.target.wants/omarchy-tailscale-join.service",
+24 -24
View File
@@ -124,56 +124,56 @@ run_load || fail "required pair plus authorized_keys loads"
[[ ! -e $sandbox/root/tailscale_authkey ]] || fail "absent tailscale_authkey is not copied" [[ ! -e $sandbox/root/tailscale_authkey ]] || fail "absent tailscale_authkey is not copied"
pass "present optional files are copied, absent ones skipped" pass "present optional files are copied, absent ones skipped"
# An oem marker replaces user_credentials.json: OEM installs defer user # A defer-provisioning marker replaces user_credentials.json: deferred-provisioning installs
# creation to first boot, so imaging rigs ship no credentials at all. # creation to first boot, so imaging rigs ship no credentials at all.
new_sandbox new_sandbox
attach_drive cidata attach_drive cidata
echo '{"disk_config": {}}' >"$sandbox/media/user_configuration.json" echo '{"disk_config": {}}' >"$sandbox/media/user_configuration.json"
: >"$sandbox/media/oem" : >"$sandbox/media/defer-provisioning"
run_load || fail "config plus oem marker loads" run_load || fail "config plus defer-provisioning marker loads"
[[ -f $sandbox/root/oem ]] || fail "oem marker is copied" [[ -f $sandbox/root/defer-provisioning ]] || fail "defer-provisioning marker is copied"
[[ ! -e $sandbox/root/user_credentials.json ]] || fail "oem drive copies no credentials" [[ ! -e $sandbox/root/user_credentials.json ]] || fail "defer-provisioning drive copies no credentials"
pass "oem marker stands in for credentials" pass "defer-provisioning marker stands in for credentials"
# The oem marker and credentials can coexist (rig supplies its own LUKS # The defer-provisioning marker and credentials can coexist (rig supplies its own LUKS
# passphrase in the credentials file); both are copied. # passphrase in the credentials file); both are copied.
new_sandbox new_sandbox
attach_drive cidata attach_drive cidata
write_required_pair write_required_pair
: >"$sandbox/media/oem" : >"$sandbox/media/defer-provisioning"
run_load || fail "oem marker plus credentials loads" run_load || fail "defer-provisioning marker plus credentials loads"
[[ -f $sandbox/root/oem && -f $sandbox/root/user_credentials.json ]] || fail "both oem marker and credentials are copied" [[ -f $sandbox/root/defer-provisioning && -f $sandbox/root/user_credentials.json ]] || fail "both defer-provisioning marker and credentials are copied"
pass "oem marker plus credentials copies both" pass "defer-provisioning marker plus credentials copies both"
# An oem marker without the configuration is still not an autoinstall drive. # An defer-provisioning marker without the configuration is still not an autoinstall drive.
new_sandbox new_sandbox
attach_drive cidata attach_drive cidata
: >"$sandbox/media/oem" : >"$sandbox/media/defer-provisioning"
! run_load || fail "oem marker alone is not an autoinstall drive" ! run_load || fail "defer-provisioning marker alone is not an autoinstall drive"
[[ ! -e $sandbox/root/oem ]] || fail "oem marker alone copies nothing" [[ ! -e $sandbox/root/defer-provisioning ]] || fail "defer-provisioning marker alone copies nothing"
pass "oem marker alone falls back to the wizard" pass "defer-provisioning marker alone falls back to the wizard"
# Stale OEM inputs from a previous load in the same session are cleared before # Stale deferred-provisioning inputs from a previous load in the same session are cleared before
# the current drive is copied: a normal drive must not inherit an old oem # the current drive is copied: a normal drive must not inherit an old defer-provisioning
# marker or credentials. # marker or credentials.
new_sandbox new_sandbox
attach_drive cidata attach_drive cidata
write_required_pair write_required_pair
: >"$sandbox/root/oem" # leftover from a prior OEM load : >"$sandbox/root/defer-provisioning" # leftover from a prior deferred-provisioning load
echo 'old-keys' >"$sandbox/root/authorized_keys" echo 'old-keys' >"$sandbox/root/authorized_keys"
run_load || fail "normal drive after a stale OEM load loads" run_load || fail "normal drive after a stale defer-provisioning load loads"
[[ ! -e $sandbox/root/oem ]] || fail "stale oem marker is cleared" [[ ! -e $sandbox/root/defer-provisioning ]] || fail "stale defer-provisioning marker is cleared"
[[ ! -e $sandbox/root/authorized_keys ]] || fail "stale optional inputs are cleared" [[ ! -e $sandbox/root/authorized_keys ]] || fail "stale optional inputs are cleared"
pass "stale OEM inputs are cleared before loading a normal drive" pass "stale deferred-provisioning inputs are cleared before loading a normal drive"
# A drive that isn't an autoinstall drive at all still clears stale inputs so # A drive that isn't an autoinstall drive at all still clears stale inputs so
# the wizard that follows doesn't inherit them. # the wizard that follows doesn't inherit them.
new_sandbox new_sandbox
attach_drive cidata attach_drive cidata
echo '{"disk_config": {}}' >"$sandbox/media/user_configuration.json" # half a pair echo '{"disk_config": {}}' >"$sandbox/media/user_configuration.json" # half a pair
: >"$sandbox/root/oem" : >"$sandbox/root/defer-provisioning"
! run_load || fail "half-pair drive still falls back" ! run_load || fail "half-pair drive still falls back"
[[ ! -e $sandbox/root/oem ]] || fail "stale oem marker cleared even on fallback" [[ ! -e $sandbox/root/defer-provisioning ]] || fail "stale defer-provisioning marker cleared even on fallback"
pass "stale inputs are cleared even when falling back to the wizard" pass "stale inputs are cleared even when falling back to the wizard"
# Half the required pair is not an autoinstall drive: unmount and fall back. # Half the required pair is not an autoinstall drive: unmount and fall back.
+1 -1
View File
@@ -60,7 +60,7 @@ class ConfigureSshAccessTest(unittest.TestCase):
if authorized_keys is not None: if authorized_keys is not None:
authorized_keys_path = self.target / "authorized_keys" authorized_keys_path = self.target / "authorized_keys"
authorized_keys_path.write_text(authorized_keys) authorized_keys_path.write_text(authorized_keys)
return types.SimpleNamespace(target=self.target, username="jeff", authorized_keys_path=authorized_keys_path, oem=False) return types.SimpleNamespace(target=self.target, username="jeff", authorized_keys_path=authorized_keys_path, defer_provisioning=False)
def configure(self, **kwargs): def configure(self, **kwargs):
phases_impl.configure_ssh_access(self.ctx(**kwargs)) phases_impl.configure_ssh_access(self.ctx(**kwargs))
@@ -1,7 +1,7 @@
"""Unit tests for OEM install support in the orchestrator. """Unit tests for deferred-provisioning install support in the orchestrator.
Covers the InstallContext OEM plumbing (marker file, credential-less input, Covers the InstallContext deferred-provisioning plumbing (marker file, credential-less input,
throwaway LUKS passphrase injection) and the stage_oem_state / throwaway LUKS passphrase injection) and the stage_provisioning_state /
create_factory_snapshot / configure_login phases against a temp target with create_factory_snapshot / configure_login phases against a temp target with
subprocess mocked out. subprocess mocked out.
""" """
@@ -26,7 +26,7 @@ from orchestrator import phases_impl # noqa: E402
from orchestrator.context import InstallContext # noqa: E402 from orchestrator.context import InstallContext # noqa: E402
class ContextOemTest(unittest.TestCase): class ContextDeferProvisioningTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.tmp = tempfile.TemporaryDirectory() self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup) self.addCleanup(self.tmp.cleanup)
@@ -48,51 +48,51 @@ class ContextOemTest(unittest.TestCase):
def from_env(self, **extra_env): def from_env(self, **extra_env):
env = {**self.env, **extra_env} env = {**self.env, **extra_env}
with mock.patch.dict(os.environ, env, clear=False): with mock.patch.dict(os.environ, env, clear=False):
for key in ("OMARCHY_INSTALL_OEM_FILE",): for key in ("OMARCHY_INSTALL_DEFER_PROVISIONING_FILE",):
if key not in env: if key not in env:
os.environ.pop(key, None) os.environ.pop(key, None)
return InstallContext.from_env() return InstallContext.from_env()
def base_config(self, oem=None, disk_encryption=None): def base_config(self, defer_provisioning=None, disk_encryption=None):
config = { config = {
"disk_config": {"config_type": "default_layout"}, "disk_config": {"config_type": "default_layout"},
"omarchy_install": {"mode": "full_disk", "target_mount": "/mnt"}, "omarchy_install": {"mode": "full_disk", "target_mount": "/mnt"},
} }
if oem is not None: if defer_provisioning is not None:
config["omarchy_install"]["oem"] = oem config["omarchy_install"]["defer_provisioning"] = defer_provisioning
if disk_encryption is not None: if disk_encryption is not None:
config["disk_config"]["disk_encryption"] = disk_encryption config["disk_config"]["disk_encryption"] = disk_encryption
return config return config
def test_oem_flag_from_config(self): def test_defer_provisioning_flag_from_config(self):
self.write_config(self.base_config(oem=True)) self.write_config(self.base_config(defer_provisioning=True))
ctx = self.from_env() ctx = self.from_env()
self.assertTrue(ctx.oem) self.assertTrue(ctx.defer_provisioning)
self.assertEqual(ctx.user_credentials, {"users": []}) self.assertEqual(ctx.user_credentials, {"users": []})
self.assertEqual(ctx.username, "") self.assertEqual(ctx.username, "")
def test_oem_marker_file_arms_oem_without_credentials(self): def test_marker_file_arms_deferred_provisioning_without_credentials(self):
self.write_config(self.base_config()) self.write_config(self.base_config())
marker = self.dir / "oem" marker = self.dir / "defer_provisioning"
marker.touch() marker.touch()
ctx = self.from_env(OMARCHY_INSTALL_OEM_FILE=str(marker)) ctx = self.from_env(OMARCHY_INSTALL_DEFER_PROVISIONING_FILE=str(marker))
self.assertTrue(ctx.oem) self.assertTrue(ctx.defer_provisioning)
self.assertTrue(ctx.omarchy_install["oem"]) self.assertTrue(ctx.omarchy_install["defer_provisioning"])
def test_absent_marker_file_is_not_oem(self): def test_absent_marker_file_is_not_deferred_provisioning(self):
self.write_config(self.base_config()) self.write_config(self.base_config())
self.write_creds({"users": [{"username": "jeff"}]}) self.write_creds({"users": [{"username": "jeff"}]})
ctx = self.from_env(OMARCHY_INSTALL_OEM_FILE=str(self.dir / "missing-oem")) ctx = self.from_env(OMARCHY_INSTALL_DEFER_PROVISIONING_FILE=str(self.dir / "missing-defer_provisioning"))
self.assertFalse(ctx.oem) self.assertFalse(ctx.defer_provisioning)
self.assertEqual(ctx.username, "jeff") self.assertEqual(ctx.username, "jeff")
def test_missing_credentials_without_oem_raises(self): def test_missing_credentials_without_defer_provisioning_raises(self):
self.write_config(self.base_config()) self.write_config(self.base_config())
with self.assertRaisesRegex(RuntimeError, "credentials file missing"): with self.assertRaisesRegex(RuntimeError, "credentials file missing"):
self.from_env() self.from_env()
def test_oem_generates_throwaway_luks_passphrase(self): def test_deferred_provisioning_generates_throwaway_luks_passphrase(self):
self.write_config(self.base_config(oem=True, disk_encryption={ self.write_config(self.base_config(defer_provisioning=True, disk_encryption={
"encryption_type": "luks", "partitions": ["x"], "encryption_type": "luks", "partitions": ["x"],
})) }))
ctx = self.from_env() ctx = self.from_env()
@@ -110,8 +110,8 @@ class ContextOemTest(unittest.TestCase):
synthesized = json.loads(ctx.creds_path.read_text()) synthesized = json.loads(ctx.creds_path.read_text())
self.assertEqual(synthesized["encryption_password"], password) self.assertEqual(synthesized["encryption_password"], password)
def test_oem_reuses_rig_supplied_passphrase(self): def test_deferred_provisioning_reuses_rig_supplied_passphrase(self):
self.write_config(self.base_config(oem=True, disk_encryption={ self.write_config(self.base_config(defer_provisioning=True, disk_encryption={
"encryption_type": "luks", "partitions": ["x"], "encryption_type": "luks", "partitions": ["x"],
})) }))
self.write_creds({"users": [], "encryption_password": "rig-secret"}) self.write_creds({"users": [], "encryption_password": "rig-secret"})
@@ -121,9 +121,9 @@ class ContextOemTest(unittest.TestCase):
"rig-secret", "rig-secret",
) )
def test_oem_discards_account_material_from_supplied_credentials(self): def test_deferred_provisioning_discards_account_material(self):
# A rig credentials file must not smuggle in users or a root password. # A rig credentials file must not smuggle in users or a root password.
self.write_config(self.base_config(oem=True)) self.write_config(self.base_config(defer_provisioning=True))
self.write_creds({ self.write_creds({
"users": [{"username": "backdoor", "enc_password": "x", "sudo": True}], "users": [{"username": "backdoor", "enc_password": "x", "sudo": True}],
"root_enc_password": "x", "root_enc_password": "x",
@@ -137,8 +137,8 @@ class ContextOemTest(unittest.TestCase):
self.assertEqual(synthesized["users"], []) self.assertEqual(synthesized["users"], [])
self.assertNotIn("root_enc_password", synthesized) self.assertNotIn("root_enc_password", synthesized)
def test_oem_strips_account_fields_from_arch_config(self): def test_defer_provisioning_strips_account_fields(self):
config = self.base_config(oem=True) config = self.base_config(defer_provisioning=True)
config["!users"] = [{"username": "rig"}] config["!users"] = [{"username": "rig"}]
config["!root-password"] = "hunter2" config["!root-password"] = "hunter2"
config["root_enc_password"] = "x" config["root_enc_password"] = "x"
@@ -151,8 +151,8 @@ class ContextOemTest(unittest.TestCase):
self.assertNotIn(key, arch_config) self.assertNotIn(key, arch_config)
self.assertEqual(arch_config["auth_config"], {}) self.assertEqual(arch_config["auth_config"], {})
def test_oem_unencrypted_touches_nothing(self): def test_deferred_provisioning_unencrypted_touches_nothing(self):
self.write_config(self.base_config(oem=True)) self.write_config(self.base_config(defer_provisioning=True))
ctx = self.from_env() ctx = self.from_env()
self.assertNotIn("encryption_password", ctx.user_credentials) self.assertNotIn("encryption_password", ctx.user_credentials)
@@ -160,10 +160,10 @@ class ContextOemTest(unittest.TestCase):
def make_ctx(target, **overrides): def make_ctx(target, **overrides):
defaults = dict( defaults = dict(
target=target, target=target,
oem=True, defer_provisioning=True,
encrypt=False, encrypt=False,
username="", username="",
omarchy_install={"mode": "full_disk", "oem": True, "storage": {}}, omarchy_install={"mode": "full_disk", "defer_provisioning": True, "storage": {}},
user_configuration={"disk_config": {}}, user_configuration={"disk_config": {}},
user_credentials={"users": []}, user_credentials={"users": []},
state_dir=target / "state", state_dir=target / "state",
@@ -173,7 +173,7 @@ def make_ctx(target, **overrides):
return types.SimpleNamespace(**defaults) return types.SimpleNamespace(**defaults)
class StageOemStateTest(unittest.TestCase): class StageProvisioningStateTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.tmp = tempfile.TemporaryDirectory() self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup) self.addCleanup(self.tmp.cleanup)
@@ -192,63 +192,63 @@ class StageOemStateTest(unittest.TestCase):
node_patch.start() node_patch.start()
self.addCleanup(node_patch.stop) self.addCleanup(node_patch.stop)
def install_runtime_oem_support(self): def install_runtime_provisioning_support(self):
service = self.target / "usr/share/omarchy/install/oem/omarchy-oem-setup.service" service = self.target / "usr/share/omarchy/install/provisioning/omarchy-provision-owner.service"
service.parent.mkdir(parents=True, exist_ok=True) service.parent.mkdir(parents=True, exist_ok=True)
service.write_text("[Unit]\n") service.write_text("[Unit]\n")
setup_bin = self.target / "usr/bin/omarchy-oem-setup" setup_bin = self.target / "usr/bin/omarchy-provision-owner"
setup_bin.parent.mkdir(parents=True, exist_ok=True) setup_bin.parent.mkdir(parents=True, exist_ok=True)
setup_bin.write_text("#!/bin/bash\n") setup_bin.write_text("#!/bin/bash\n")
def oem_dir(self): def provisioning_dir(self):
return self.target / "var/lib/omarchy/oem" return self.target / "var/lib/omarchy/provisioning"
def test_non_oem_install_stages_only_the_node_tarball(self): def test_normal_install_stages_only_the_node_tarball(self):
ctx = make_ctx(self.target, oem=False) ctx = make_ctx(self.target, defer_provisioning=False)
phases_impl.stage_oem_state(ctx) phases_impl.stage_provisioning_state(ctx)
self.assertTrue((self.oem_dir() / "packages/node-v24.0.0-linux-x64.tar.gz").exists()) self.assertTrue((self.provisioning_dir() / "packages/node-v24.0.0-linux-x64.tar.gz").exists())
self.assertFalse((self.oem_dir() / "pending").exists()) self.assertFalse((self.provisioning_dir() / "pending").exists())
self.assertFalse((self.target / "etc/systemd/system/omarchy-oem-setup.service").exists()) self.assertFalse((self.target / "etc/systemd/system/omarchy-provision-owner.service").exists())
def test_oem_against_runtime_without_support_fails_clearly(self): def test_deferred_provisioning_against_runtime_without_support_fails_clearly(self):
ctx = make_ctx(self.target) ctx = make_ctx(self.target)
with self.assertRaisesRegex(RuntimeError, "does not ship"): with self.assertRaisesRegex(RuntimeError, "does not ship"):
phases_impl.stage_oem_state(ctx) phases_impl.stage_provisioning_state(ctx)
def test_oem_without_node_tarball_fails(self): def test_deferred_provisioning_without_node_tarball_fails(self):
for tarball in self.packages.glob("*.tar.gz"): for tarball in self.packages.glob("*.tar.gz"):
tarball.unlink() tarball.unlink()
self.install_runtime_oem_support() self.install_runtime_provisioning_support()
ctx = make_ctx(self.target) ctx = make_ctx(self.target)
with self.assertRaisesRegex(RuntimeError, "Node tarball"): with self.assertRaisesRegex(RuntimeError, "Node tarball"):
phases_impl.stage_oem_state(ctx) phases_impl.stage_provisioning_state(ctx)
def test_missing_node_tarball_fails_normal_installs_too(self): def test_missing_node_tarball_fails_normal_installs_too(self):
# The stash is what makes a later factory reset work offline. # The stash is what makes a later factory reset work offline.
for tarball in self.packages.glob("*.tar.gz"): for tarball in self.packages.glob("*.tar.gz"):
tarball.unlink() tarball.unlink()
ctx = make_ctx(self.target, oem=False) ctx = make_ctx(self.target, defer_provisioning=False)
with self.assertRaisesRegex(RuntimeError, "Node tarball"): with self.assertRaisesRegex(RuntimeError, "Node tarball"):
phases_impl.stage_oem_state(ctx) phases_impl.stage_provisioning_state(ctx)
def test_oem_arms_first_boot_setup(self): def test_deferred_provisioning_arms_first_boot_setup(self):
self.install_runtime_oem_support() self.install_runtime_provisioning_support()
ctx = make_ctx(self.target) ctx = make_ctx(self.target)
phases_impl.stage_oem_state(ctx) phases_impl.stage_provisioning_state(ctx)
self.assertTrue((self.oem_dir() / "pending").exists()) self.assertTrue((self.provisioning_dir() / "pending").exists())
self.assertTrue((self.target / "etc/systemd/system/omarchy-oem-setup.service").exists()) self.assertTrue((self.target / "etc/systemd/system/omarchy-provision-owner.service").exists())
link = self.target / "etc/systemd/system/multi-user.target.wants/omarchy-oem-setup.service" link = self.target / "etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service"
self.assertTrue(link.is_symlink()) self.assertTrue(link.is_symlink())
self.assertEqual( self.assertEqual(
os.readlink(link), "/etc/systemd/system/omarchy-oem-setup.service" os.readlink(link), "/etc/systemd/system/omarchy-provision-owner.service"
) )
# Unencrypted: no LUKS staging. # Unencrypted: no LUKS staging.
self.assertFalse((self.oem_dir() / "luks-key").exists()) self.assertFalse((self.provisioning_dir() / "luks-key").exists())
def test_oem_encrypted_stages_luks_auto_unlock(self): def test_deferred_provisioning_encrypted_stages_luks_auto_unlock(self):
self.install_runtime_oem_support() self.install_runtime_provisioning_support()
ctx = make_ctx( ctx = make_ctx(
self.target, self.target,
user_configuration={"disk_config": {"disk_encryption": { user_configuration={"disk_config": {"disk_encryption": {
@@ -256,30 +256,30 @@ class StageOemStateTest(unittest.TestCase):
"encryption_password": "throwaway-secret", "encryption_password": "throwaway-secret",
}}}, }}},
) )
phases_impl.stage_oem_state(ctx) phases_impl.stage_provisioning_state(ctx)
# Byte-for-byte the slot passphrase — no trailing newline. # Byte-for-byte the slot passphrase — no trailing newline.
self.assertEqual((self.oem_dir() / "luks-key").read_text(), "throwaway-secret") self.assertEqual((self.provisioning_dir() / "luks-key").read_text(), "throwaway-secret")
keyfile = self.target / "etc/omarchy/oem.key" keyfile = self.target / "etc/omarchy/provisioning.key"
self.assertEqual(keyfile.read_text(), "throwaway-secret") self.assertEqual(keyfile.read_text(), "throwaway-secret")
self.assertEqual(keyfile.stat().st_mode & 0o777, 0o600) self.assertEqual(keyfile.stat().st_mode & 0o777, 0o600)
cmdline = (self.target / "etc/limine-entry-tool.d/99-omarchy-oem-unlock.conf").read_text() cmdline = (self.target / "etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf").read_text()
self.assertIn("cryptkey=rootfs:/etc/omarchy/oem.key", cmdline) self.assertIn("cryptkey=rootfs:/etc/omarchy/provisioning.key", cmdline)
files = (self.target / "etc/mkinitcpio.conf.d/99-omarchy-oem-key.conf").read_text() files = (self.target / "etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf").read_text()
self.assertIn("FILES+=(/etc/omarchy/oem.key)", files) self.assertIn("FILES+=(/etc/omarchy/provisioning.key)", files)
def test_oem_pre_encrypted_without_passphrase_fails(self): def test_deferred_provisioning_pre_encrypted_without_passphrase_fails(self):
self.install_runtime_oem_support() self.install_runtime_provisioning_support()
ctx = make_ctx( ctx = make_ctx(
self.target, self.target,
omarchy_install={"mode": "protected", "oem": True, "storage": {"luks_uuid": "abc"}}, omarchy_install={"mode": "protected", "defer_provisioning": True, "storage": {"luks_uuid": "abc"}},
) )
with self.assertRaisesRegex(RuntimeError, "passphrase"): with self.assertRaisesRegex(RuntimeError, "passphrase"):
phases_impl.stage_oem_state(ctx) phases_impl.stage_provisioning_state(ctx)
class ConfigureLoginOemTest(unittest.TestCase): class ConfigureLoginDeferProvisioningTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.tmp = tempfile.TemporaryDirectory() self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup) self.addCleanup(self.tmp.cleanup)
@@ -293,7 +293,7 @@ class ConfigureLoginOemTest(unittest.TestCase):
run_patch.start() run_patch.start()
self.addCleanup(run_patch.stop) self.addCleanup(run_patch.stop)
def test_oem_login_leaves_user_state_to_first_boot(self): def test_deferred_provisioning_login_leaves_user_state_to_first_boot(self):
ctx = make_ctx(self.target, encrypt=True) ctx = make_ctx(self.target, encrypt=True)
phases_impl.configure_login(ctx) phases_impl.configure_login(ctx)
@@ -303,7 +303,7 @@ class ConfigureLoginOemTest(unittest.TestCase):
self.assertTrue(any("sddm.service" in cmd for cmd in self.calls)) self.assertTrue(any("sddm.service" in cmd for cmd in self.calls))
def test_normal_encrypted_login_still_autologs_in(self): def test_normal_encrypted_login_still_autologs_in(self):
ctx = make_ctx(self.target, oem=False, encrypt=True, username="jeff") ctx = make_ctx(self.target, defer_provisioning=False, encrypt=True, username="jeff")
phases_impl.configure_login(ctx) phases_impl.configure_login(ctx)
autologin = (self.target / "etc/sddm.conf.d/autologin.conf").read_text() autologin = (self.target / "etc/sddm.conf.d/autologin.conf").read_text()
@@ -312,7 +312,7 @@ class ConfigureLoginOemTest(unittest.TestCase):
self.assertIn("User=jeff", state) self.assertIn("User=jeff", state)
class ConfigureSshAccessOemTest(unittest.TestCase): class ConfigureSshAccessDeferProvisioningTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.tmp = tempfile.TemporaryDirectory() self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup) self.addCleanup(self.tmp.cleanup)
@@ -336,14 +336,14 @@ class ConfigureSshAccessOemTest(unittest.TestCase):
run_patch.start() run_patch.start()
self.addCleanup(run_patch.stop) self.addCleanup(run_patch.stop)
def test_oem_stages_keys_for_first_boot(self): def test_deferred_provisioning_stages_keys_for_first_boot(self):
keys = Path(self.tmp.name) / "authorized_keys" keys = Path(self.tmp.name) / "authorized_keys"
keys.write_text("ssh-ed25519 AAAA rig@host\n") keys.write_text("ssh-ed25519 AAAA rig@host\n")
ctx = make_ctx(self.target, authorized_keys_path=keys) ctx = make_ctx(self.target, authorized_keys_path=keys)
phases_impl.configure_ssh_access(ctx) phases_impl.configure_ssh_access(ctx)
staged = self.target / "var/lib/omarchy/oem/authorized_keys" staged = self.target / "var/lib/omarchy/provisioning/authorized_keys"
self.assertEqual(staged.read_text(), "ssh-ed25519 AAAA rig@host\n") self.assertEqual(staged.read_text(), "ssh-ed25519 AAAA rig@host\n")
self.assertEqual(staged.stat().st_mode & 0o777, 0o600) self.assertEqual(staged.stat().st_mode & 0o777, 0o600)
# No user yet — nothing under /home, no chown. # No user yet — nothing under /home, no chown.
@@ -387,7 +387,7 @@ class CreateFactorySnapshotTest(unittest.TestCase):
self.addCleanup(run_patch.stop) self.addCleanup(run_patch.stop)
def ctx(self): def ctx(self):
return make_ctx(self.target, oem=False, state_dir=self.state_dir) return make_ctx(self.target, defer_provisioning=False, state_dir=self.state_dir)
def test_snapshots_root_as_factory(self): def test_snapshots_root_as_factory(self):
phases_impl.create_factory_snapshot(self.ctx()) phases_impl.create_factory_snapshot(self.ctx())
@@ -410,11 +410,11 @@ class CreateFactorySnapshotTest(unittest.TestCase):
top = self.state_dir / "factory-top" top = self.state_dir / "factory-top"
factory = top / "@factory" factory = top / "@factory"
secrets = [ secrets = [
factory / "var/lib/omarchy/oem/authorized_keys", factory / "var/lib/omarchy/provisioning/authorized_keys",
factory / "etc/tailscale/authkey", factory / "etc/tailscale/authkey",
factory / "etc/omarchy/oem.key", factory / "etc/omarchy/provisioning.key",
] ]
keep = factory / "var/lib/omarchy/oem/groups" keep = factory / "var/lib/omarchy/provisioning/groups"
for path in [*secrets, keep]: for path in [*secrets, keep]:
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("secret") path.write_text("secret")