From 12a2354fad464e3d8ccb03b52f2c4a158ffc0fad Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Wed, 22 Jul 2026 01:59:49 +0000 Subject: [PATCH 1/2] Move auto-install.sh out of Nix config into a real script file Moves the auto-installer's shell script from an inline Nix string in modules/installer/common.nix to scripts/installer/auto-install.sh, a real, version-controlled, directly-editable/shellcheck-able file. common.nix now wires it in with pkgs.replaceVars, substituting the one value that actually needs to come from variables.nix (lanDomain) -- every other `${...}` in the script is a literal bash reference, left untouched. replaceVars fails the build if any @name@-shaped placeholder is left unsubstituted, so a typo'd or renamed variable is caught at eval time rather than silently shipping broken. Verified: built the substituted derivation and diffed it against the source template -- identical except for the one substituted line, no leftover unsubstituted placeholders. Full codex-maintenance.sh (secret grep, fmt, statix, full eval of every host/package including the installer/pxe artifacts that consume this) passes clean. --- CLAUDE.md | 29 +++++-- modules/installer/common.nix | 139 ++---------------------------- scripts/installer/auto-install.sh | 128 +++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 138 deletions(-) create mode 100644 scripts/installer/auto-install.sh diff --git a/CLAUDE.md b/CLAUDE.md index 49c4dc3..5d0be5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -164,13 +164,28 @@ before committing. Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` is organized by purpose: `scripts/secrets/` (sops/age + SSH host-key -management), `scripts/proxmox/` (Proxmox deployment), `scripts/lib/` -(shared helpers, sourced by the scripts below — not run directly), and a -handful of repo-wide scripts left at the top level (`env.sh`, -`bump-nixpkgs-release.sh`, plus `codex-setup.sh`/`codex-maintenance.sh` -above). When adding a new script, put it in the matching subfolder rather -than the top level, and if it duplicates logic another script already has, -lift the shared part into `scripts/lib/` instead of copying it. +management), `scripts/proxmox/` (Proxmox deployment), `scripts/installer/` +(the auto-installer's own shell script, templated into the image — see +below), `scripts/lib/` (shared helpers, sourced by the scripts below — not +run directly), and a handful of repo-wide scripts left at the top level +(`env.sh`, `bump-nixpkgs-release.sh`, plus `codex-setup.sh`/ +`codex-maintenance.sh` above). When adding a new script, put it in the +matching subfolder rather than the top level, and if it duplicates logic +another script already has, lift the shared part into `scripts/lib/` +instead of copying it. + +### `scripts/installer/` + +- `scripts/installer/auto-install.sh` — the interactive install script + baked into the auto-installer image (see `docs/auto-installer.md`), kept + as a real, version-controlled shell file rather than inline in + `modules/installer/common.nix`'s Nix. `common.nix` wires it in with + `pkgs.replaceVars ../../scripts/installer/auto-install.sh { inherit (vars) lanDomain; }`, + substituting the single `@lanDomain@` placeholder from `variables.nix` — + every other `${...}` in the script is a literal bash reference, + untouched by this. `replaceVars` fails the build if any `@name@`-shaped + placeholder is left unsubstituted, so a typo'd or renamed variable is + caught at eval time rather than silently shipping broken. ### `scripts/secrets/` diff --git a/modules/installer/common.nix b/modules/installer/common.nix index 26a7d7e..9cab712 100644 --- a/modules/installer/common.nix +++ b/modules/installer/common.nix @@ -45,138 +45,15 @@ disko ]; - # Write auto-install script to /root + # Auto-install script, kept as a real, version-controlled shell file at + # scripts/installer/auto-install.sh rather than an inline Nix string -- + # replaceVars only substitutes the one value (lanDomain) that genuinely + # needs to come from variables.nix; every other "@..." pattern in the + # script is a literal `${...}` bash reference, untouched by this. etc."auto-install.sh" = { - text = '' - #!/run/current-system/sw/bin/bash - set -eux - - set -euo pipefail - - export FLAKE_BASE_URL="git+https://${vars.lanDomain}/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..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 - 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, or scp the result to" - echo "/root/host-keys/ on this machine." - read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key - if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then - echo "Aborted." - exit 1 - 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 - sleep 10 - reboot - ''; + source = pkgs.replaceVars ../../scripts/installer/auto-install.sh { + inherit (vars) lanDomain; + }; mode = "0755"; }; diff --git a/scripts/installer/auto-install.sh b/scripts/installer/auto-install.sh new file mode 100644 index 0000000..b1f08aa --- /dev/null +++ b/scripts/installer/auto-install.sh @@ -0,0 +1,128 @@ +#!/run/current-system/sw/bin/bash +set -eux + +set -euo pipefail + +export FLAKE_BASE_URL="git+https://@lanDomain@/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..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 + 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, or scp the result to" + echo "/root/host-keys/ on this machine." + read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key + if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 1 + 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 + sleep 10 + reboot From 60c155327dcb885fbe4910921acc6f389bd80974 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Wed, 22 Jul 2026 02:15:37 +0000 Subject: [PATCH 2/2] Restore guiRootDisk1/guiRootDisk2, lost in a merge conflict on main These were dropped from variables.nix by a stash/merge conflict resolution on main (commit fb6ee27) that kept the new wifiSsid value but discarded the two disk-path variables entirely, leaving unresolved `<<<<<<< Updated upstream` markers in an intermediate commit before being cleaned up. modules/disko/baremetal.nix references both directly with no fallback, so baremetal-gui has been failing to evaluate on main since that commit ("attribute 'guiRootDisk1' missing") -- confirmed by cloning main fresh and evaluating config.disko.devices.disk.disk1.device directly. This commit is rebased onto latest main (through "updated secrets", which registered baremetal-gui's real sops recipient) rather than the older base this branch started from. --- variables.nix | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/variables.nix b/variables.nix index 0390ae8..7236a8c 100644 --- a/variables.nix +++ b/variables.nix @@ -45,12 +45,20 @@ # the installer image's nixos/root users. adminSshKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream"; - # Prestaged wifi credentials for the gui host's NetworkManager profile - # (modules/networking/wifi.nix). Leave blank until the bare-metal - # hardware profile is wired up — an empty ssid disables the profile - # rather than creating a broken empty one. + # Prestaged wifi SSID for the gui host's NetworkManager profile + # (modules/networking/wifi.nix). The password is not here -- it's + # sops-encrypted in secrets/gui.yaml (wifi-password) instead, since this + # file isn't a secret store. wifiSsid = "nbn-fttp-net-5G"; + # Bare-metal gui host's two disks for a ZFS RAID0 (striped) root pool + # (modules/disko/baremetal.nix). Only used transiently at disko-format + # time (partitioning); the resulting fileSystems/zpool import reference + # by-partlabel/by-id paths afterward regardless, same as + # modules/disko/proxmox.nix's own plain "/dev/sda". + guiRootDisk1 = "/dev/sda"; + guiRootDisk2 = "/dev/sdb"; + # System timeZone = "Australia/Brisbane";