Archived
Check NixOS configurations / eval-hosts (pull_request) Successful in 10m19s
disko's --mode ...,mount leaves the pool imported (needed for nixos-install to write into /mnt), and the script rebooted straight into the newly-installed system without exporting it. That pool is still stamped with the live installer's own hostid, which never matches the target host's declared networking.hostId, and since boot.zfs.forceImportRoot is false (the recommended setting, not a bug), the first real boot refuses to force-import an unexported pool from a different hostid -- which is exactly the ZFS-import stall baremetal-gui was hitting after install. Exporting all pools right before reboot (a no-op for non-ZFS hosts) clears the in-use state so import succeeds regardless of hostid. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
189 lines
9.1 KiB
Bash
Executable File
189 lines
9.1 KiB
Bash
Executable File
#!/usr/bin/env nix-shell
|
|
#!nix-shell -i bash -p jq disko nixos-install-tools zfs
|
|
# shellcheck shell=bash
|
|
# The only genuinely external tools this script calls directly: `jq`
|
|
# (parsing the `nix eval` host list), `disko`/`nixos-install` (the
|
|
# install itself), and `zpool` (exporting a ZFS root pool before reboot,
|
|
# see the comment above that call below). Everything disko shells out to
|
|
# internally (parted/sgdisk/mkfs.*/zfs/...) is self-contained -- disko's
|
|
# own generated scripts hardcode absolute Nix store paths for those, they
|
|
# don't rely on this script's PATH at all (confirmed by inspecting a
|
|
# generated system.build.formatScript). The built installer image
|
|
# (modules/installer/common.nix, plus the upstream
|
|
# installation-cd-minimal.nix it imports via iso.nix) already has all
|
|
# four in environment.systemPackages, so this nix-shell wrapper is a
|
|
# fast no-op there; it's what makes the script also work standalone
|
|
# (e.g. run directly from a checkout on a stock ISO), where they aren't
|
|
# guaranteed.
|
|
set -eux
|
|
|
|
set -euo pipefail
|
|
|
|
# shellcheck source=../env.sh
|
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/env.sh"
|
|
|
|
export FLAKE_BASE_URL="git+https://${LAN_DOMAIN}/beatzaplenty/nixos.git"
|
|
|
|
echo "Fetching available NixOS hosts from flake..."
|
|
# Two categories deliberately excluded from the menu:
|
|
# lxc-* — these build a config.system.build.tarball meant for
|
|
# `pct restore` on Proxmox directly, not an install.
|
|
# Running nixos-install against one here would
|
|
# bind-mount / onto /mnt and then refuse to touch the
|
|
# filesystem it's currently running on — see
|
|
# docs/auto-installer.md.
|
|
# installer — this *is* the installer image's own flake target,
|
|
# not a deployable host; "installing" it means
|
|
# nixos-install-ing a copy of the installer into
|
|
# itself.
|
|
mapfile -t options < <(
|
|
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
|
|
"${FLAKE_BASE_URL}#nixosConfigurations" \
|
|
--apply builtins.attrNames \
|
|
| jq -r '.[]
|
|
| select(startswith("lxc-") | not)
|
|
| select(. != "installer")'
|
|
)
|
|
|
|
if [[ ${#options[@]} -eq 0 ]]; then
|
|
echo "ERROR: No NixOS hosts found in ${FLAKE_BASE_URL}#nixosConfigurations" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Note: lxc-* targets aren't installed this way — build them with"
|
|
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
|
|
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
|
|
|
|
echo "Choose the flake profile to install:"
|
|
select choice in "${options[@]}"; do
|
|
if [[ -n "$choice" ]]; then
|
|
echo "You selected: $choice"
|
|
break
|
|
else
|
|
echo "Invalid selection. Try again."
|
|
fi
|
|
done
|
|
|
|
echo "Starting install with flake: ${FLAKE_BASE_URL}#${choice}"
|
|
|
|
# Optional: confirm before proceeding
|
|
read -rp "Proceed with installation? (y/N): " confirm
|
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
echo "Aborted."
|
|
exit 1
|
|
fi
|
|
|
|
# A nix-cache host is *the* substituter/remote-builder for every other
|
|
# host once installed (its own config explicitly excludes itself from
|
|
# using either — see buildType != "nix-cache" in the nixos flake.nix).
|
|
# Installing one shouldn't depend on a nix-cache substituter either,
|
|
# for the same reason — plus in practice "nix-cache" only resolves over
|
|
# Tailscale, which a fresh installer environment was never connected to
|
|
# anyway, so it's dead weight even for non-nix-cache installs until
|
|
# that's sorted out. Override it away here specifically for nix-cache
|
|
# targets to keep install-time behaviour consistent with run-time.
|
|
nix_extra_opts=()
|
|
if [[ "${choice}" == *-nix-cache ]]; then
|
|
echo "Installing a nix-cache host — skipping the nix-cache substituter."
|
|
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
|
|
fi
|
|
|
|
# Every host reachable through this menu has a Disko config (lxc-*
|
|
# is filtered out above, and is the only category that doesn't —
|
|
# see docs/auto-installer.md), so this can run unconditionally: no
|
|
# need to probe the flake first and branch on whether Disko applies.
|
|
disko --mode destroy,format,mount \
|
|
--flake "${FLAKE_BASE_URL}#${choice}" "${nix_extra_opts[@]}" --yes-wipe-all-disks
|
|
|
|
# sops-nix derives this host's decryption key from its own SSH host key
|
|
# at *activation* time, which runs before systemd would otherwise
|
|
# generate one on first boot. Without pre-seeding it here, secrets
|
|
# (including the login password) fail to decrypt on first boot.
|
|
# Generate the key with scripts/secrets/prepare-host-key.sh first.
|
|
#
|
|
# Two places a key can come from, checked in order:
|
|
# /etc/host-keys — baked into this image at build time (see
|
|
# modules/installer/host-keys.nix; only present
|
|
# if built with NIXOS_HOST_KEYS_DIR set)
|
|
# /root/host-keys — scp'd in manually after boot (older fallback,
|
|
# still supported for images built without keys)
|
|
mkdir -p /root/host-keys
|
|
if [[ -f "/etc/host-keys/${choice}_ssh_host_ed25519_key" ]]; then
|
|
echo "Found baked-in SSH host key for ${choice}, installing to target..."
|
|
install -D -m 0600 "/etc/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
|
install -D -m 0644 "/etc/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
|
elif [[ -f "/root/host-keys/${choice}_ssh_host_ed25519_key" ]]; then
|
|
echo "Found pre-seeded SSH host key for ${choice}, installing to target..."
|
|
install -D -m 0600 "/root/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
|
install -D -m 0644 "/root/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
|
else
|
|
# Third place a key can come from: an arbitrary path the operator
|
|
# points at interactively (e.g. a USB stick, a mount from another
|
|
# machine) -- only offered when there's an actual human at the other
|
|
# end of stdin to ask, never in a non-interactive run.
|
|
key_copied=0
|
|
if [[ -t 0 ]]; then
|
|
echo "No SSH host key found for ${choice} (checked /etc/host-keys and /root/host-keys)."
|
|
read -rp "Path to a directory containing ${choice}_ssh_host_ed25519_key(.pub) (blank to skip): " key_src_dir
|
|
if [[ -n "$key_src_dir" && -f "${key_src_dir}/${choice}_ssh_host_ed25519_key" && -f "${key_src_dir}/${choice}_ssh_host_ed25519_key.pub" ]]; then
|
|
cp "${key_src_dir}/${choice}_ssh_host_ed25519_key" "${key_src_dir}/${choice}_ssh_host_ed25519_key.pub" /root/host-keys/
|
|
key_copied=1
|
|
elif [[ -n "$key_src_dir" ]]; then
|
|
echo "WARNING: ${choice}_ssh_host_ed25519_key(.pub) not found in ${key_src_dir}."
|
|
fi
|
|
fi
|
|
|
|
if [[ "$key_copied" -eq 1 ]]; then
|
|
echo "Copied SSH host key for ${choice} from ${key_src_dir}, installing to target..."
|
|
install -D -m 0600 "/root/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
|
install -D -m 0644 "/root/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
|
else
|
|
echo "WARNING: no SSH host key found for ${choice} (checked /etc/host-keys and /root/host-keys)"
|
|
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
|
|
echo "Run scripts/secrets/prepare-host-key.sh for host ${choice} on your admin workstation first,"
|
|
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, scp the result to"
|
|
echo "/root/host-keys/ on this machine, or point at it when prompted above."
|
|
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
|
|
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
|
|
echo "Aborted."
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
mkdir -p /mnt/install-tmp
|
|
export TMPDIR=/mnt/install-tmp
|
|
|
|
nixos-install \
|
|
--flake "${FLAKE_BASE_URL}#${choice}" \
|
|
"${nix_extra_opts[@]}" \
|
|
--no-root-password
|
|
|
|
|
|
rm -rf /mnt/install-tmp
|
|
# Redundant copy of the host's private key — the real one is now at
|
|
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
|
|
# up on its own since it was written imperatively, not declaratively.
|
|
rm -rf /root/host-keys
|
|
|
|
# disko's --mode ...,mount left any ZFS root pool imported (that's what
|
|
# let nixos-install write into /mnt). If we reboot with it still
|
|
# imported, it isn't just "not exported" -- it's stamped with *this*
|
|
# live installer environment's hostid, which almost never matches the
|
|
# target's own networking.hostId (see hosts/*/host.nix; the installer
|
|
# itself sets none). modules/services/zfs/enable-service.nix and
|
|
# modules/common/configuration.nix both set boot.zfs.forceImportRoot =
|
|
# false deliberately (the safe option per that setting's own docs), so
|
|
# the freshly-installed system's first real boot sees a pool "in use by
|
|
# another system" and refuses to import it without -f -- which is what
|
|
# makes boot stall waiting on the ZFS import. Exporting here (a no-op
|
|
# if the chosen host has no ZFS root, e.g. proxmox-*/linode-*) clears
|
|
# that in-use state so the next import, from any hostid, succeeds.
|
|
if [[ -n "$(zpool list -H -o name 2>/dev/null)" ]]; then
|
|
echo "Exporting ZFS pool(s) before reboot..."
|
|
zpool export -a
|
|
fi
|
|
|
|
sleep 10
|
|
reboot
|