deploy: make the local reinstall path actually work, and fail closed
The `install <config> localhost` auto-path added in 0ea9020 could not have
completed. Fixed three blockers plus the guard that was silently not guarding.
Inside installer-iso the run died before doing anything:
- systemd sets no $HOME for a service without User= (SetLoginEnvironment=
defaults to false), and this script runs under `set -u`, so it aborted on
the bare $HOME with "unbound variable". Added $KEYDIR + Environment=HOME.
- the host key it needs to seed /etc/ssh isn't on the ISO at all — that is
built from a public repo and carries no credentials on purpose. It now
travels on the boot partition, located via homelab.keypart=<PARTUUID> on
the kernel cmdline, and dies with the disko wipe minutes later. Without
it sops can't decrypt on boot #1 and mutableUsers locks darman for good.
- installation-cd-minimal leaves experimental-features unset, so both
`nix run` and `nixos-install --flake` failed. (The nixos-images kexec
installer sets them itself, which is why the same branch worked after
kexec-local but not from the ISO.)
The staging-dir guard passed everything on btrfs: findmnt prints the
subvolume as /dev/sdb2[/@], lsblk can't open that, and an empty parent was
treated as "different disk" — so it allowed staging the iso on the very disk
disko then wiped. terra's current CachyOS root is exactly that layout. Now
uses --nofsroot, resolves EVERY whole-disk ancestor (LVM/RAID span several:
/mnt/ssd_01 -> sdd+sde), and treats "can't tell" as a hard error. btrfs
staging is refused outright — stage-1 mounts a btrfs volume's top level, so
an iso inside a subvolume is unreachable.
findiso= lost its leading slash whenever the staging mountpoint was /,
giving /findisovar/tmp/x.iso and an emergency shell after the reboot.
Also:
- confirm before rebooting, like flash/kexec-local already do; --yes skips
it and is what the ISO passes itself
- $BOOT from `bootctl --print-boot-path`, not a hardcoded /boot
- free-space checks on both target partitions before the ~1GB copy
- `nix run .#disko` / `.#nixos-anywhere` from locked inputs instead of
github:... master-of-the-day, resolved while a disk is being wiped
- one_match warns instead of silently taking [0]; require_tracked covers
every hosts/<config>/*.nix; flash traps its mount
- drop nixos-images' `inputs.nixpkgs.follows` — that input doesn't exist,
it only printed a warning on every nix command
Verified: the prepare path exercised under stubs against this box's real
disks (btrfs-on-OS-disk, tmpfs, LVM, subdirectory), shellcheck clean, all
six configs evaluate, checks.kexec-local still passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+195
-55
@@ -17,7 +17,8 @@
|
||||
# /var/tmp) must be exec-capable and hold
|
||||
# ~3x the tarball.
|
||||
# Then run `install <config> localhost`.
|
||||
# ./deploy install <config> <host> first install. Wipes the OS disk. Ships the
|
||||
# ./deploy install <config> <host> [--yes]
|
||||
# first install. Wipes the OS disk. Ships the
|
||||
# host's sops key. <host>=localhost/127.0.0.1
|
||||
# skips nixos-anywhere/ssh and runs disko +
|
||||
# nixos-install directly against /mnt — but
|
||||
@@ -27,16 +28,21 @@
|
||||
# Run from the REAL running OS instead (e.g.
|
||||
# a box where kexec-local doesn't work),
|
||||
# it builds installer-iso, stages its
|
||||
# kernel/initrd on the ESP + the iso file on
|
||||
# a non-OS-disk partition, sets a systemd-boot
|
||||
# one-shot entry with homelab.install=<config>
|
||||
# on its kernel cmdline, and reboots — a real
|
||||
# ACPI reboot, not a kexec jump. The booted
|
||||
# installer's homelab-auto-install.service
|
||||
# reads that cmdline param and re-runs this
|
||||
# exact command itself once its repo checkout
|
||||
# (homelab-checkout.service) succeeds, finishing
|
||||
# the install unattended. See CLAUDE.md.
|
||||
# kernel/initrd + the host key on the boot
|
||||
# partition and the iso file on a non-OS-disk
|
||||
# partition, sets a systemd-boot one-shot
|
||||
# entry with homelab.install=<config> +
|
||||
# homelab.keypart=<PARTUUID> on its kernel
|
||||
# cmdline, and reboots — a real ACPI reboot,
|
||||
# not a kexec jump. The booted installer's
|
||||
# homelab-auto-install.service reads those
|
||||
# cmdline params, picks the host key back up
|
||||
# and re-runs this exact command itself once
|
||||
# its repo checkout (homelab-checkout.service)
|
||||
# succeeds, finishing the install unattended.
|
||||
# It confirms before rebooting; --yes skips
|
||||
# that (it is what the ISO passes itself).
|
||||
# See CLAUDE.md.
|
||||
# ./deploy switch <config> <host> rebuild + activate on a running host.
|
||||
# ./deploy boot <config> <host> stage for next boot, don't activate now.
|
||||
# ./deploy test <config> <host> activate without adding a boot entry.
|
||||
@@ -75,6 +81,17 @@ REPO="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || dirname "$
|
||||
cd "$REPO"
|
||||
export PATH="/nix/var/nix/profiles/default/bin:$PATH"
|
||||
|
||||
# Off-repo material keyed by <config>: pre-generated SSH host keys (install)
|
||||
# and per-config sops age keys (flash).
|
||||
#
|
||||
# Resolved defensively rather than as a bare $HOME, because this script also
|
||||
# runs from installer-iso's homelab-auto-install.service, and systemd does not
|
||||
# set $HOME for a system service without User= (systemd.exec(5):
|
||||
# SetLoginEnvironment= "defaults to true if User=, DynamicUser= or PAMName= are
|
||||
# set, false otherwise"). Under `set -u` that aborted the whole unattended run
|
||||
# with an "unbound variable" that read like a bug in this script.
|
||||
KEYDIR="${HOMELAB_KEY_DIR:-${HOME:-/root}/.config/homelab}"
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
|
||||
need() { command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1"; }
|
||||
@@ -85,6 +102,11 @@ need() { command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1"; }
|
||||
require_root() {
|
||||
[ "$(id -u)" = 0 ] && return 0
|
||||
echo ">> $1 needs root — re-executing under sudo" >&2
|
||||
# $KEYDIR is derived from $HOME, and whether sudo carries $HOME across
|
||||
# depends on the local sudoers policy (env_reset/always_set_home). Pin the
|
||||
# resolved value so the re-exec looks for host keys where the invoking user
|
||||
# has them, not under /root.
|
||||
export HOMELAB_KEY_DIR="$KEYDIR"
|
||||
exec sudo -E -- "$SCRIPT_PATH" "${SCRIPT_ARGS[@]}"
|
||||
}
|
||||
|
||||
@@ -95,9 +117,21 @@ one_match() {
|
||||
local what="$1"; shift
|
||||
local f=("$@") # caller expands the glob (nullglob is on)
|
||||
[ "${#f[@]}" -gt 0 ] || die "no $what found — did the build actually produce one?"
|
||||
# Say so instead of silently taking [0]: a stale result-sd/ symlink from an
|
||||
# earlier config is exactly how you flash the wrong image without a word.
|
||||
[ "${#f[@]}" -eq 1 ] \
|
||||
|| echo ">> warning: ${#f[@]} candidates for $what, using ${f[0]} (rm the stale ones)" >&2
|
||||
printf '%s\n' "${f[0]}"
|
||||
}
|
||||
|
||||
# Every whole-disk device backing a block device or a mounted path, one per
|
||||
# line. LVM/RAID/LUKS can sit on several at once (verified on terra:
|
||||
# /mnt/ssd_01 -> sdd AND sde), so a single lookup is not enough. Empty output
|
||||
# means "could not determine" — which callers must treat as unsafe, not as OK.
|
||||
disks_backing() {
|
||||
lsblk -rnso NAME,TYPE "$1" 2>/dev/null | awk '$2 == "disk" { print "/dev/" $1 }'
|
||||
}
|
||||
|
||||
# Sets tb / cpio / bbox — the kexec tarball plus the static cpio+gzip that
|
||||
# kexec-run.sh needs on PATH to rebuild its initrd.
|
||||
#
|
||||
@@ -137,23 +171,38 @@ is_live_installer() {
|
||||
}
|
||||
|
||||
# `install <config> localhost` run on a REAL running OS (not already inside a
|
||||
# live installer): builds installer-iso, stages its kernel/initrd + iso file
|
||||
# locally, points a systemd-boot one-shot entry at them with
|
||||
# homelab.install=<config> on the kernel cmdline, and reboots — a real ACPI
|
||||
# reboot through firmware POST, deliberately NOT a kexec jump (see terra's
|
||||
# kexec-local gotcha in CLAUDE.md). The booted installer's
|
||||
# homelab-auto-install.service reads that cmdline param and re-runs this exact
|
||||
# `install <config> localhost` command itself (now genuinely inside the
|
||||
# installer) once homelab-checkout.service has fetched the repo, finishing the
|
||||
# job unattended.
|
||||
# live installer): builds installer-iso, stages its kernel/initrd + the host's
|
||||
# pre-generated ssh key on the boot partition and the iso file on a non-OS
|
||||
# disk, points a systemd-boot one-shot entry at them with
|
||||
# homelab.install=<config> + homelab.keypart=<PARTUUID> on the kernel cmdline,
|
||||
# and reboots — a real ACPI reboot through firmware POST, deliberately NOT a
|
||||
# kexec jump (see terra's kexec-local gotcha in CLAUDE.md). The booted
|
||||
# installer's homelab-auto-install.service reads those params, picks the host
|
||||
# key back up and re-runs this exact `install <config> localhost` command
|
||||
# itself (now genuinely inside the installer) once homelab-checkout.service has
|
||||
# fetched the repo, finishing the job unattended.
|
||||
local_install_prepare_and_reboot() {
|
||||
local config="$1"
|
||||
local config="$1" hostkey="$2" assume_yes="$3"
|
||||
require_root "preparing a local reinstall"
|
||||
[ -d /sys/firmware/efi ] || die "not booted UEFI — the one-shot boot entry needs systemd-boot"
|
||||
need bootctl
|
||||
need nix
|
||||
need lsblk
|
||||
need findmnt
|
||||
need awk
|
||||
need realpath
|
||||
need stat
|
||||
need df
|
||||
|
||||
# systemd-boot keeps its entries on $BOOT — the XBOOTLDR partition when there
|
||||
# is one, the ESP otherwise — which is not always /boot. Hardcoding /boot on
|
||||
# a box that mounts its ESP elsewhere just creates a directory on the root
|
||||
# filesystem, and then reboots into an entry the firmware never sees.
|
||||
local boot
|
||||
boot="$(bootctl --print-boot-path 2>/dev/null)" \
|
||||
|| die "bootctl couldn't locate the boot partition — is systemd-boot installed here?"
|
||||
[ -d "$boot/loader/entries" ] \
|
||||
|| die "$boot/loader/entries doesn't exist — systemd-boot isn't installed on this box"
|
||||
|
||||
# No default/auto-picked location — the wrong disk here is destroyed
|
||||
# mid-install (see the OS-disk check below), so this always asks rather
|
||||
@@ -168,48 +217,124 @@ local_install_prepare_and_reboot() {
|
||||
[ -n "$stagedir" ] || die "no staging path given"
|
||||
[ -d "$stagedir" ] \
|
||||
|| die "staging dir $stagedir doesn't exist — needs to be an existing partition that is NOT the OS disk being wiped"
|
||||
# Absolute + symlink-free: findiso= below is computed by stripping the
|
||||
# mountpoint prefix off this, and a relative answer at the prompt would
|
||||
# produce a path the initrd can never resolve.
|
||||
stagedir="$(realpath "$stagedir")"
|
||||
|
||||
# Refuse if the staging partition turns out to live on the same disk
|
||||
# disko is about to wipe — the iso file (and the running installer
|
||||
# loopback-mounted from it) would be destroyed mid-install.
|
||||
local osdisk osdisk_real stage_src stage_pkname stage_disk_real
|
||||
local osdisk osdisk_real stage_src stage_fstype stage_disks d
|
||||
osdisk="$(nix eval --raw ".#nixosConfigurations.$config.config.disko.devices.disk" \
|
||||
--apply 'd: (builtins.head (builtins.attrValues d)).device' 2>/dev/null)" \
|
||||
|| die "couldn't read the OS disk device from hosts/$config/disk-config.nix"
|
||||
osdisk_real="$(readlink -f "$osdisk")"
|
||||
stage_src="$(findmnt -no SOURCE --target "$stagedir")" \
|
||||
|
||||
# --nofsroot matters: on btrfs, findmnt prints the subvolume as
|
||||
# `/dev/sdb2[/@]`, which is not a path lsblk can open. Without it the lookup
|
||||
# came back empty and the guard below was skipped entirely — i.e. it silently
|
||||
# allowed staging on the very disk about to be wiped. terra's current
|
||||
# CachyOS root is exactly that layout.
|
||||
stage_src="$(findmnt -no SOURCE --nofsroot --target "$stagedir")" \
|
||||
|| die "$stagedir doesn't resolve to a mounted filesystem"
|
||||
stage_pkname="$(lsblk -no PKNAME "$stage_src" 2>/dev/null || true)"
|
||||
if [ -n "$stage_pkname" ]; then
|
||||
stage_disk_real="$(readlink -f "/dev/$stage_pkname")"
|
||||
[ "$stage_disk_real" = "$osdisk_real" ] \
|
||||
&& die "$stagedir is on the OS disk ($osdisk) that install would wipe — re-run and pick a different disk"
|
||||
# `|| true` so the explicit check below is what reports the problem: lsblk
|
||||
# exits nonzero on a device it can't parse, and under `set -e` + pipefail a
|
||||
# bare assignment from a failing substitution kills the script silently,
|
||||
# right past the fail-closed message.
|
||||
stage_disks="$(disks_backing "$stage_src" || true)"
|
||||
# Fail closed. "Couldn't determine the disk" is not "different disk".
|
||||
[ -n "$stage_disks" ] \
|
||||
|| die "couldn't determine which physical disk $stagedir ($stage_src) is on — refusing to guess, since being wrong destroys the install mid-flight"
|
||||
for d in $stage_disks; do
|
||||
if [ "$d" = "$osdisk_real" ]; then
|
||||
die "$stagedir is on the OS disk ($osdisk -> $osdisk_real) that install would wipe — re-run and pick a different disk"
|
||||
fi
|
||||
done
|
||||
|
||||
# stage-1 resolves findiso= by mounting each blkid-visible partition and
|
||||
# testing `-e /findiso$isoPath` (nixos/modules/system/boot/stage-1-init.sh).
|
||||
# For btrfs it mounts the volume's TOP level, so a path that lives inside a
|
||||
# subvolume (/@/...) is simply not there and the box boots to an emergency
|
||||
# shell — after it has already rebooted out of the working OS.
|
||||
stage_fstype="$(findmnt -no FSTYPE --target "$stagedir")"
|
||||
[ "$stage_fstype" != btrfs ] \
|
||||
|| die "$stagedir is btrfs: findiso= mounts the volume's top level, so a path inside a subvolume never resolves. Stage on a non-btrfs partition (ext4/vfat/ntfs)."
|
||||
|
||||
# Last chance to back out. This is the most destructive command in the
|
||||
# script — it reboots the machine you are typing at and the wipe that
|
||||
# follows is unattended — so it confirms just like `flash` and `kexec-local`
|
||||
# do, both of which are less final than this.
|
||||
if [ "$assume_yes" != "--yes" ]; then
|
||||
echo ">> about to REINSTALL this machine from scratch:"
|
||||
echo " hostname: $(uname -n)"
|
||||
echo " config: $config"
|
||||
echo " OS disk: $osdisk"
|
||||
echo " -> $osdisk_real ** WIPED, unattended, after the reboot **"
|
||||
# Unquoted on purpose: collapses the one-per-line list onto one line.
|
||||
echo " staging: $stagedir (on $(echo $stage_disks))"
|
||||
echo " entry: $boot/loader/entries/homelab-installer.conf (one-shot)"
|
||||
read -rp ">> type 'yes' to build the installer, reboot into it and wipe $osdisk_real: " ok
|
||||
[ "$ok" = yes ] || die "aborted"
|
||||
fi
|
||||
|
||||
echo ">> building installer-iso (kernel + initrd + iso image)"
|
||||
local kernel initrd isodir iso mnt_point iso_relpath
|
||||
local kernel initrd isodir iso mnt_point iso_relpath boot_src boot_partuuid
|
||||
kernel="$(nix build --no-link --print-out-paths .#nixosConfigurations.installer-iso.config.system.build.kernel)/bzImage"
|
||||
initrd="$(nix build --no-link --print-out-paths .#nixosConfigurations.installer-iso.config.system.build.initialRamdisk)/initrd"
|
||||
isodir="$(nix build --no-link --print-out-paths .#nixosConfigurations.installer-iso.config.system.build.isoImage)"
|
||||
iso="$(one_match 'installer iso' "$isodir"/iso/*.iso)"
|
||||
|
||||
echo ">> staging kernel/initrd on the ESP, iso image on $stagedir"
|
||||
install -Dm644 "$kernel" /boot/homelab-installer/bzImage
|
||||
install -Dm644 "$initrd" /boot/homelab-installer/initrd
|
||||
# A short write is not visible until the reboot, when findiso finds a
|
||||
# truncated iso and drops to an emergency shell. Check first — `install`
|
||||
# prints no progress and the iso is ~1GB.
|
||||
local need_stage need_boot avail_stage avail_boot
|
||||
need_stage="$(stat -Lc %s "$iso")"
|
||||
need_boot="$(( $(stat -Lc %s "$kernel") + $(stat -Lc %s "$initrd") + $(stat -Lc %s "$hostkey") ))"
|
||||
avail_stage="$(df -B1 --output=avail "$stagedir" | tail -1 | tr -d ' ')"
|
||||
avail_boot="$(df -B1 --output=avail "$boot" | tail -1 | tr -d ' ')"
|
||||
[ "$avail_stage" -ge "$(( need_stage + 64 * 1024 * 1024 ))" ] \
|
||||
|| die "$stagedir has $(( avail_stage / 1024 / 1024 ))MB free, the iso needs $(( need_stage / 1024 / 1024 ))MB — pick another partition"
|
||||
[ "$avail_boot" -ge "$(( need_boot + 16 * 1024 * 1024 ))" ] \
|
||||
|| die "$boot has $(( avail_boot / 1024 / 1024 ))MB free, kernel+initrd need $(( need_boot / 1024 / 1024 ))MB"
|
||||
|
||||
echo ">> staging kernel/initrd/host key on $boot, iso image on $stagedir"
|
||||
install -Dm644 "$kernel" "$boot/homelab-installer/bzImage"
|
||||
install -Dm644 "$initrd" "$boot/homelab-installer/initrd"
|
||||
install -Dm644 "$iso" "$stagedir/homelab-installer.iso"
|
||||
|
||||
# findiso= is a path relative to whatever partition the initrd finds it on
|
||||
# (it mounts every blkid-visible partition looking for it) — not necessarily
|
||||
# relative to `/`, if $stagedir is a subdirectory of a bigger filesystem
|
||||
# rather than a mountpoint itself.
|
||||
mnt_point="$(findmnt -no TARGET --target "$stagedir")"
|
||||
iso_relpath="${stagedir#"$mnt_point"}/homelab-installer.iso"
|
||||
# The ISO is built from a PUBLIC repo and deliberately carries no
|
||||
# credentials, so the host key has to travel with the staged installer or
|
||||
# the auto-install run has nothing to seed /etc/ssh with — and without that,
|
||||
# sops can't decrypt on boot #1, /etc/shadow gets written once with a locked
|
||||
# darman, and no later `deploy switch` can fix it (README).
|
||||
#
|
||||
# $boot lives on the OS disk, so disko destroys this copy minutes later. The
|
||||
# mode is advisory on vfat (permissions come from the mount's fmask, 0077 on
|
||||
# a NixOS/systemd-boot ESP) — it is the wipe, not the mode, doing the work.
|
||||
install -Dm600 "$hostkey" "$boot/homelab-installer/ssh_host_ed25519_key"
|
||||
install -Dm644 "$hostkey.pub" "$boot/homelab-installer/ssh_host_ed25519_key.pub"
|
||||
boot_src="$(findmnt -no SOURCE --nofsroot --target "$boot")" \
|
||||
|| die "couldn't resolve $boot to a device"
|
||||
boot_partuuid="$(lsblk -no PARTUUID "$boot_src" 2>/dev/null | head -1 | tr -d ' ' || true)"
|
||||
[ -n "$boot_partuuid" ] \
|
||||
|| die "couldn't read a PARTUUID for $boot ($boot_src) — the installer needs it to find the host key"
|
||||
|
||||
cat >/boot/loader/entries/homelab-installer.conf <<EOF
|
||||
# findiso= is a path relative to whatever partition the initrd finds it on
|
||||
# (it mounts every blkid-visible partition looking for it), not to `/`, if
|
||||
# $stagedir is a subdirectory of a bigger filesystem rather than a mountpoint
|
||||
# itself. It must KEEP its leading slash: stage-1 tests `-e /findiso$isoPath`,
|
||||
# so a bare `var/tmp/x.iso` becomes `/findisovar/tmp/x.iso` and never matches.
|
||||
# Prefixing then squeezing handles both ends: stagedir == the mountpoint
|
||||
# (strip leaves "") and mnt_point == "/" (strip leaves a relative path).
|
||||
mnt_point="$(findmnt -no TARGET --target "$stagedir")"
|
||||
iso_relpath="$(printf '/%s/%s' "${stagedir#"$mnt_point"}" homelab-installer.iso | tr -s /)"
|
||||
|
||||
cat >"$boot/loader/entries/homelab-installer.conf" <<EOF
|
||||
title Homelab Installer ($config, findiso)
|
||||
linux /homelab-installer/bzImage
|
||||
initrd /homelab-installer/initrd
|
||||
options nohibernate root=fstab loglevel=4 lsm=landlock,yama,bpf findiso=$iso_relpath homelab.install=$config
|
||||
options nohibernate root=fstab loglevel=4 lsm=landlock,yama,bpf findiso=$iso_relpath homelab.install=$config homelab.keypart=$boot_partuuid
|
||||
EOF
|
||||
|
||||
echo ">> one-shot boot into the installer, then rebooting — it will finish this install itself"
|
||||
@@ -221,14 +346,20 @@ EOF
|
||||
# invisible to `nix build`/`nixos-install`, which then fails obscurely or builds
|
||||
# a stale config. Check before doing anything destructive.
|
||||
require_tracked() {
|
||||
local config="$1" cfgfile="hosts/$1/configuration.nix"
|
||||
local config="$1" cfgfile="hosts/$1/configuration.nix" f
|
||||
[ -e "$cfgfile" ] || die "no $cfgfile in the repo"
|
||||
# No .git at all (e.g. a tarball export of the repo, no working tree) means
|
||||
# there's nothing that CAN be untracked — nothing to check. Only skip on a
|
||||
# MISSING .git, not on any other git failure.
|
||||
# No .git at all (e.g. a tarball export of the repo, no working tree), or no
|
||||
# git binary, means there's nothing that CAN be untracked — nothing to check.
|
||||
# Only skip on that, not on any other git failure.
|
||||
command -v git >/dev/null 2>&1 || return 0
|
||||
git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
|
||||
git -C "$REPO" ls-files --error-unmatch "$cfgfile" >/dev/null 2>&1 \
|
||||
|| die "$cfgfile is untracked — 'git add hosts/$config' first (flakes ignore untracked files)"
|
||||
# Every .nix in hosts/<config>/, not just configuration.nix: an untracked
|
||||
# disk-config.nix is exactly as invisible to the flake, and it is the file
|
||||
# that decides which disk gets wiped.
|
||||
for f in "hosts/$config"/*.nix; do
|
||||
git -C "$REPO" ls-files --error-unmatch "$f" >/dev/null 2>&1 \
|
||||
|| die "$f is untracked — 'git add hosts/$config' first (flakes ignore untracked files)"
|
||||
done
|
||||
}
|
||||
|
||||
# The password field of a Proton Pass item ("--field password" prints the bare
|
||||
@@ -469,9 +600,12 @@ case "$cmd" in
|
||||
;;
|
||||
|
||||
install)
|
||||
config="${2:-}"; host="${3:-}"
|
||||
{ [ -n "$config" ] && [ -n "$host" ]; } || die "usage: ./deploy install <config> <host>"
|
||||
hostkey="$HOME/.config/homelab/$config/ssh_host_ed25519_key"
|
||||
config="${2:-}"; host="${3:-}"; assume_yes="${4:-}"
|
||||
{ [ -n "$config" ] && [ -n "$host" ]; } || die "usage: ./deploy install <config> <host> [--yes]"
|
||||
# $KEYDIR, not a bare $HOME — see its definition. This same check runs
|
||||
# inside installer-iso, where homelab-auto-install.service has no $HOME and
|
||||
# has just dropped the key into /root/.config/homelab/<config>/.
|
||||
hostkey="$KEYDIR/$config/ssh_host_ed25519_key"
|
||||
[ -f "$hostkey" ] || die "missing host key: $hostkey"
|
||||
[ -d "./hosts/$config" ] || die "no ./hosts/$config directory in the repo"
|
||||
require_tracked "$config"
|
||||
@@ -481,7 +615,7 @@ case "$cmd" in
|
||||
# Not already inside a live installer: build one, stage it, one-shot
|
||||
# boot into it, and let it finish this exact command itself. See
|
||||
# local_install_prepare_and_reboot above and CLAUDE.md.
|
||||
local_install_prepare_and_reboot "$config"
|
||||
local_install_prepare_and_reboot "$config" "$hostkey" "$assume_yes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -492,7 +626,10 @@ case "$cmd" in
|
||||
[ -f "./hosts/$config/disk-config.nix" ] || die "no ./hosts/$config/disk-config.nix"
|
||||
|
||||
echo ">> disko .#$config onto this box's OS disk (WILL be wiped)"
|
||||
nix run github:nix-community/disko -- \
|
||||
# `.#disko`, not github:nix-community/disko — the revision comes from this
|
||||
# repo's flake.lock rather than upstream master-of-the-day, and resolves
|
||||
# from the local store. See the nixos-anywhere input in flake.nix.
|
||||
nix run ".#disko" -- \
|
||||
--mode disko "./hosts/$config/disk-config.nix"
|
||||
|
||||
echo ">> installing sops host key so it can decrypt on boot #1"
|
||||
@@ -524,11 +661,11 @@ case "$cmd" in
|
||||
echo ">> root ssh password from Proton Pass ($root_item)"
|
||||
export SSHPASS="$root_pw"
|
||||
unset root_pw
|
||||
nix run github:nix-community/nixos-anywhere -- \
|
||||
nix run ".#nixos-anywhere" -- \
|
||||
--env-password "${anywhere[@]}"
|
||||
unset SSHPASS
|
||||
else
|
||||
nix run github:nix-community/nixos-anywhere -- "${anywhere[@]}"
|
||||
nix run ".#nixos-anywhere" -- "${anywhere[@]}"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
@@ -600,7 +737,7 @@ case "$cmd" in
|
||||
# partition at /var/lib/sops-nix/age.txt so sops decrypts on first boot.
|
||||
# (The Pi's vfat partition isn't mounted at runtime, so the key can't live
|
||||
# there.) Key stays off-repo, out of the nix store, and out of the image.
|
||||
keyfile="$HOME/.config/homelab/$config/age.txt"
|
||||
keyfile="$KEYDIR/$config/age.txt"
|
||||
if [ -f "$keyfile" ]; then
|
||||
echo ">> installing sops age key onto the root partition"
|
||||
sudo partprobe "$dev" 2>/dev/null || sudo blockdev --rereadpt "$dev" 2>/dev/null || true
|
||||
@@ -613,10 +750,13 @@ case "$cmd" in
|
||||
| sort -rn | head -1 | cut -d' ' -f2)"
|
||||
[ -n "$rootpart" ] || die "no ext4 root partition found on $dev — place $keyfile at /var/lib/sops-nix/age.txt manually"
|
||||
mnt="$(mktemp -d)"
|
||||
# Unmount + remove even if the install fails, so a retry doesn't trip
|
||||
# over the card still being mounted on a stale temp dir.
|
||||
trap 'sudo umount "$mnt" 2>/dev/null || true; rmdir "$mnt" 2>/dev/null || true' EXIT
|
||||
sudo mount "$rootpart" "$mnt"
|
||||
sudo install -Dm600 "$keyfile" "$mnt/var/lib/sops-nix/age.txt"
|
||||
sudo sync
|
||||
sudo umount "$mnt"; rmdir "$mnt"
|
||||
sudo umount "$mnt"; rmdir "$mnt"; trap - EXIT
|
||||
echo ">> age key installed (/var/lib/sops-nix/age.txt)"
|
||||
fi
|
||||
echo ">> done — insert the card into the Pi and boot."
|
||||
|
||||
Reference in New Issue
Block a user