From 10bb0c8e34d6ef02c232ad7893810b9067b0d753 Mon Sep 17 00:00:00 2001 From: steffen Date: Sat, 14 Mar 2026 12:08:30 +0100 Subject: [PATCH] add deploy/create/install apps, fix templates and docs - Add apps: create (scaffold host from template), deploy (multi-host deployment with -n filter), install (NixOS installation from live ISO) - Register all apps in flake.nix (create, deploy, install, rebuild) - Add deploy.json config (cryodev-main, SSH port 2299) - Fix generic-server template: was using Pi hardware/boot config, now correct x86_64 with systemd-boot, UEFI, ROOT/BOOT/SWAP labels - Fix template networking.nix: use HOSTNAME placeholder instead of hardcoded cryodev-pi (both templates) - Fix headplane upstream pnpm-deps hash mismatch via overlay - Fix all docs: replace root@ with user@, --ssh-option with NIX_SSHOPTS, add deploy app references, update first-install guide to use create app and document service deactivation steps --- AGENTS.md | 24 ++++- apps/create/create.sh | 95 ++++++++++++++++++ apps/create/default.nix | 20 ++++ apps/deploy/default.nix | 18 ++++ apps/deploy/deploy.sh | 123 ++++++++++++++++++++++++ deploy.json | 10 ++ docs/deployment/cd.md | 5 +- docs/getting-started/first-install.md | 40 ++++---- docs/getting-started/new-client.md | 5 +- docs/getting-started/reinstall.md | 5 +- flake.nix | 2 + modules/nixos/headplane/default.nix | 8 ++ templates/generic-server/boot.nix | 9 +- templates/generic-server/hardware.nix | 53 +++++++--- templates/generic-server/networking.nix | 2 +- templates/raspberry-pi/networking.nix | 2 +- 16 files changed, 366 insertions(+), 55 deletions(-) create mode 100644 apps/create/create.sh create mode 100644 apps/create/default.nix create mode 100644 apps/deploy/default.nix create mode 100644 apps/deploy/deploy.sh create mode 100644 deploy.json diff --git a/AGENTS.md b/AGENTS.md index 2fdf139..26737a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,13 +43,31 @@ nix develop ### Deployment ```bash +# Deploy all hosts via deploy app (uses deploy.json) +nix run .#deploy + # Deploy to cryodev-main via deploy-rs nix run github:serokell/deploy-rs -- .#cryodev-main # Manual deployment via SSH -nixos-rebuild switch --flake .# \ - --target-host @ --use-remote-sudo \ - --ssh-option="-p 2299" +NIX_SSHOPTS="-p 2299" nixos-rebuild switch --flake .# \ + --target-host @ --use-remote-sudo +``` + +### Apps + +```bash +# Create a new host from template +nix run .#create -- -t generic-server -n + +# Install NixOS on a new machine (run from NixOS live ISO) +nix run .#install -- -n -r + +# Deploy to all configured hosts +nix run .#deploy + +# Rebuild NixOS/Home Manager configuration +nix run .#rebuild -- nixos ``` ## Code Style & Conventions diff --git a/apps/create/create.sh b/apps/create/create.sh new file mode 100644 index 0000000..2089fbb --- /dev/null +++ b/apps/create/create.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +# Create a new host from a template + +FLAKE_DIR="." +TEMPLATE="" +HOSTNAME="" +SYSTEM="" + +SEPARATOR="________________________________________" + +usage() { + cat <&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -t|--template) TEMPLATE="$2"; shift 2 ;; + -n|--hostname) HOSTNAME="$2"; shift 2 ;; + -s|--system) SYSTEM="$2"; shift 2 ;; + -f|--flake) FLAKE_DIR="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) error "Unknown option: $1" ;; + esac +done + +# Validate +[[ -z "$TEMPLATE" ]] && error "Template is required (-t)" +[[ -z "$HOSTNAME" ]] && error "Hostname is required (-n)" + +TEMPLATE_DIR="$FLAKE_DIR/templates/$TEMPLATE" +HOST_DIR="$FLAKE_DIR/hosts/$HOSTNAME" + +[[ ! -d "$TEMPLATE_DIR" ]] && error "Template '$TEMPLATE' not found in $TEMPLATE_DIR" +[[ -d "$HOST_DIR" ]] && error "Host '$HOSTNAME' already exists in $HOST_DIR" + +# Derive system from template if not specified +if [[ -z "$SYSTEM" ]]; then + case "$TEMPLATE" in + generic-server) SYSTEM="x86_64-linux" ;; + raspberry-pi) SYSTEM="aarch64-linux" ;; + *) error "Cannot derive system for template '$TEMPLATE'. Use -s to specify." ;; + esac +fi + +echo "$SEPARATOR" +echo "Creating host '$HOSTNAME' from template '$TEMPLATE'" +echo " System: $SYSTEM" +echo " Target: $HOST_DIR" +echo "$SEPARATOR" + +# Copy template +cp -r "$TEMPLATE_DIR" "$HOST_DIR" + +# Remove template flake.nix (not needed in host dir) +rm -f "$HOST_DIR/flake.nix" + +# Replace hostname in networking.nix +sed -i "s/networking.hostName = \".*\"/networking.hostName = \"$HOSTNAME\"/" "$HOST_DIR/networking.nix" + +# Create empty secrets.yaml placeholder +touch "$HOST_DIR/secrets.yaml" + +# Add to git +git -C "$FLAKE_DIR" add "$HOST_DIR" + +echo "$SEPARATOR" +echo "Host '$HOSTNAME' created successfully." +echo "" +echo "Next steps:" +echo " 1. Add to flake.nix:" +echo "" +echo " $HOSTNAME = mkNixosConfiguration \"$SYSTEM\" [ ./hosts/$HOSTNAME ];" +echo "" +echo " 2. Update hardware.nix and disks.sh for your hardware" +echo " 3. Update .sops.yaml with creation rules for hosts/$HOSTNAME/secrets.yaml" +echo " 4. Follow the first-install guide: docs/getting-started/first-install.md" diff --git a/apps/create/default.nix b/apps/create/default.nix new file mode 100644 index 0000000..238f1d2 --- /dev/null +++ b/apps/create/default.nix @@ -0,0 +1,20 @@ +{ + writeShellApplication, + git, + gnused, + ... +}: + +let + name = "create"; + text = builtins.readFile ./${name}.sh; +in +writeShellApplication { + inherit name text; + meta.mainProgram = name; + + runtimeInputs = [ + git + gnused + ]; +} diff --git a/apps/deploy/default.nix b/apps/deploy/default.nix new file mode 100644 index 0000000..e52dc58 --- /dev/null +++ b/apps/deploy/default.nix @@ -0,0 +1,18 @@ +{ + writeShellApplication, + jq, + ... +}: + +let + name = "deploy"; + text = builtins.readFile ./${name}.sh; +in +writeShellApplication { + inherit name text; + meta.mainProgram = name; + + runtimeInputs = [ + jq + ]; +} diff --git a/apps/deploy/deploy.sh b/apps/deploy/deploy.sh new file mode 100644 index 0000000..37894f3 --- /dev/null +++ b/apps/deploy/deploy.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +# defaults +FLAKE_URI="." +CONFIG_FILE="./deploy.json" +ACTION="switch" +USE_SUDO=true +DO_BUILD=true +FILTER_HOSTS=() + +usage() { + cat < $1\033[0m"; } +success() { echo -e "\033[0;32m$1\033[0m"; } +error() { echo -e "\033[0;31mError: $1\033[0m" >&2; exit 1; } + +while [[ $# -gt 0 ]]; do + case "$1" in + switch|boot|test) ACTION="$1"; shift ;; + -n|--host) FILTER_HOSTS+=("$2"); shift 2 ;; + -f|--flake) FLAKE_URI="$2"; shift 2 ;; + -c|--config) CONFIG_FILE="$2"; shift 2 ;; + --no-sudo) USE_SUDO=false; shift ;; + --skip-build) DO_BUILD=false; shift ;; + -h|--help) usage; exit 0 ;; + *) error "Invalid argument '$1'" ;; + esac +done + +command -v jq &> /dev/null || error "jq is not installed." +[ -f "$CONFIG_FILE" ] || error "Config '$CONFIG_FILE' not found." + +BUILD_HOST=$(jq -r '.buildHost // "localhost"' "$CONFIG_FILE") +[[ "$BUILD_HOST" =~ ^(127\.0\.0\.1|::1)$ ]] && BUILD_HOST="localhost" + +SSH_PORT=$(jq -r '.sshPort // "22"' "$CONFIG_FILE") +export NIX_SSHOPTS="-p $SSH_PORT" + +mapfile -t ALL_ENTRIES < <(jq -r '.hosts[] | "\(.name) \(.address)"' "$CONFIG_FILE") +[ ${#ALL_ENTRIES[@]} -eq 0 ] && error "No hosts defined in $CONFIG_FILE" + +# Filter hosts if -n was provided +HOST_ENTRIES=() +if [ ${#FILTER_HOSTS[@]} -gt 0 ]; then + for entry in "${ALL_ENTRIES[@]}"; do + read -r name _address <<< "$entry" + for filter in "${FILTER_HOSTS[@]}"; do + if [[ "$name" == "$filter" ]]; then + HOST_ENTRIES+=("$entry") + break + fi + done + done + # Check for unknown hosts + for filter in "${FILTER_HOSTS[@]}"; do + found=false + for entry in "${ALL_ENTRIES[@]}"; do + read -r name _ <<< "$entry" + [[ "$name" == "$filter" ]] && found=true && break + done + [[ "$found" == false ]] && error "Host '$filter' not found in $CONFIG_FILE" + done + [ ${#HOST_ENTRIES[@]} -eq 0 ] && error "No matching hosts found" +else + HOST_ENTRIES=("${ALL_ENTRIES[@]}") +fi + +echo "Action: $ACTION" +echo "Flake: $FLAKE_URI" +echo "Builder: $BUILD_HOST" +echo "SSH Port: $SSH_PORT" +echo "Hosts: $(printf '%s ' "${HOST_ENTRIES[@]}" | sed 's/ [^ ]*//g; s/ */, /g')" + +if [ "$DO_BUILD" = true ]; then + _status "Building configurations..." + for entry in "${HOST_ENTRIES[@]}"; do + read -r name address <<< "$entry" + echo "------------------------------------------------" + echo "Building host '$name':" + + CMD=("nixos-rebuild" "build" "--flake" "${FLAKE_URI}#${name}") + [[ "$BUILD_HOST" != "localhost" ]] && CMD+=("--build-host" "$BUILD_HOST") + + "${CMD[@]}" || error "Build failed for $name" + success "Build for host '$name' successful." + done +fi + +_status "Deploying to targets..." +for entry in "${HOST_ENTRIES[@]}"; do + read -r name address <<< "$entry" + echo "------------------------------------------------" + echo "Deploying to host '$name' ($address):" + + CMD=("nixos-rebuild" "$ACTION" "--flake" "${FLAKE_URI}#${name}" "--target-host" "$address") + [[ "$BUILD_HOST" != "localhost" ]] && CMD+=("--build-host" "$BUILD_HOST") + [[ "$USE_SUDO" = true ]] && CMD+=("--use-remote-sudo") + + "${CMD[@]}" || error "Activation failed for $name" + success "Host '$name' updated." +done + +success "Deployment complete." diff --git a/deploy.json b/deploy.json new file mode 100644 index 0000000..2723ad0 --- /dev/null +++ b/deploy.json @@ -0,0 +1,10 @@ +{ + "sshPort": "2299", + "buildHost": "localhost", + "hosts": [ + { + "name": "cryodev-main", + "address": "steffen@cryodev.xyz" + } + ] +} diff --git a/docs/deployment/cd.md b/docs/deployment/cd.md index f50b568..ca9900e 100644 --- a/docs/deployment/cd.md +++ b/docs/deployment/cd.md @@ -152,9 +152,8 @@ For hosts not using automated deployment: nix build .#nixosConfigurations..config.system.build.toplevel # Deploy with nixos-rebuild -nixos-rebuild switch --flake .# \ - --target-host @ --use-remote-sudo \ - --ssh-option="-p 2299" +NIX_SSHOPTS="-p 2299" nixos-rebuild switch --flake .# \ + --target-host @ --use-remote-sudo # Or using deploy-rs nix run github:serokell/deploy-rs -- .# diff --git a/docs/getting-started/first-install.md b/docs/getting-started/first-install.md index e8680bc..aa81dcd 100644 --- a/docs/getting-started/first-install.md +++ b/docs/getting-started/first-install.md @@ -22,26 +22,21 @@ Bei der Erstinstallation gibt es ein Henne-Ei-Problem: ## Schritt 1: Host-Konfiguration vorbereiten -> Falls der Host bereits in `hosts/` und `flake.nix` existiert, ueberspringe 1.1-1.3. +> Falls der Host bereits in `hosts/` und `flake.nix` existiert, ueberspringe 1.1-1.2. -### 1.1 Template kopieren +### 1.1 Host aus Template erstellen ```bash -cp -r templates/generic-server hosts/ +nix run .#create -- -t generic-server -n ``` -### 1.2 Hostname setzen +Das Script: +- Kopiert das Template nach `hosts//` +- Setzt den Hostname in `networking.nix` +- Erstellt eine leere `secrets.yaml` +- Fuegt die Dateien zu Git hinzu -`hosts//networking.nix`: - -```nix -{ - networking.hostName = ""; - networking.domain = "cryodev.xyz"; -} -``` - -### 1.3 In flake.nix registrieren +### 1.2 In flake.nix registrieren ```nix nixosConfigurations = { @@ -49,6 +44,8 @@ nixosConfigurations = { }; ``` +Ausserdem `hardware.nix` und `disks.sh` fuer die Zielhardware anpassen. + ### 1.4 Services temporaer deaktivieren Alle Services, die SOPS-Secrets referenzieren, muessen fuer die Erstinstallation deaktiviert werden. Andernfalls schlaegt die Installation fehl, weil die Secrets noch nicht entschluesselt werden koennen. @@ -252,9 +249,14 @@ Ebenso in `hosts//services/sops.nix` die Secrets-Definitionen wieder e ### 3.5 Deployen ```bash -nixos-rebuild switch --flake .# \ - --target-host @ --use-remote-sudo \ - --ssh-option="-p 2299" +nix run .#deploy -- -n +``` + +Dies nutzt die Konfiguration aus `deploy.json`. Alternativ manuell: + +```bash +NIX_SSHOPTS="-p 2299" nixos-rebuild switch --flake .# \ + --target-host @ --use-remote-sudo ``` ## Schritt 4: Platzhalter-Secrets ersetzen @@ -291,9 +293,7 @@ Nachdem der Server mit Headscale und Forgejo laeuft, die Platzhalter durch echte 5. **Erneut deployen**: ```bash - nixos-rebuild switch --flake .# \ - --target-host @ --use-remote-sudo \ - --ssh-option="-p 2299" + nix run .#deploy -- -n ``` ## Naechste Schritte diff --git a/docs/getting-started/new-client.md b/docs/getting-started/new-client.md index abb6f60..e8d6c44 100644 --- a/docs/getting-started/new-client.md +++ b/docs/getting-started/new-client.md @@ -244,9 +244,8 @@ Da Comin auf dem Pi läuft, wird er die neue Konfiguration automatisch pullen. Alternativ manuell: ```bash -nixos-rebuild switch --flake .#neuer-pi \ - --target-host @ --use-remote-sudo \ - --ssh-option="-p 2299" +NIX_SSHOPTS="-p 2299" nixos-rebuild switch --flake .#neuer-pi \ + --target-host @ --use-remote-sudo ``` --- diff --git a/docs/getting-started/reinstall.md b/docs/getting-started/reinstall.md index 8f5040b..7c546ef 100644 --- a/docs/getting-started/reinstall.md +++ b/docs/getting-started/reinstall.md @@ -142,9 +142,8 @@ sops updatekeys hosts//secrets.yaml Dann Konfiguration neu deployen: ```bash -nixos-rebuild switch --flake .# \ - --target-host @ --use-remote-sudo \ - --ssh-option="-p 2299" +NIX_SSHOPTS="-p 2299" nixos-rebuild switch --flake .# \ + --target-host @ --use-remote-sudo ``` ## Häufige Probleme diff --git a/flake.nix b/flake.nix index 4c41123..0dba405 100644 --- a/flake.nix +++ b/flake.nix @@ -77,6 +77,8 @@ }; in { + create = mkApp "create"; + deploy = mkApp "deploy"; install = mkApp "install"; rebuild = mkApp "rebuild"; } diff --git a/modules/nixos/headplane/default.nix b/modules/nixos/headplane/default.nix index 420b8cf..498141a 100644 --- a/modules/nixos/headplane/default.nix +++ b/modules/nixos/headplane/default.nix @@ -31,6 +31,14 @@ in config = mkIf cfg.enable { nixpkgs.overlays = [ inputs.headplane.overlays.default + # Fix upstream pnpm-deps hash mismatch (https://github.com/tale/headplane) + (final: prev: { + headplane = prev.headplane.overrideAttrs (old: { + pnpmDeps = old.pnpmDeps.overrideAttrs { + outputHash = "sha256-lk/ezsrW6JHh5nXPSstqHUbaMTeOARBGZcBSoG1S5ns="; + }; + }); + }) ]; services.headplane = { diff --git a/templates/generic-server/boot.nix b/templates/generic-server/boot.nix index a459d88..53a9686 100644 --- a/templates/generic-server/boot.nix +++ b/templates/generic-server/boot.nix @@ -1,8 +1,7 @@ { - boot = { - loader = { - grub.enable = false; - generic-extlinux-compatible.enable = true; - }; + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; }; + boot.loader.efi.canTouchEfiVariables = true; } diff --git a/templates/generic-server/hardware.nix b/templates/generic-server/hardware.nix index a0d751a..810fa36 100644 --- a/templates/generic-server/hardware.nix +++ b/templates/generic-server/hardware.nix @@ -1,23 +1,44 @@ -{ pkgs, lib, ... }: +{ + config, + lib, + pkgs, + modulesPath, + ... +}: { - boot = { - kernelPackages = pkgs.linuxKernel.packages.linux_rpi4; - initrd.availableKernelModules = [ - "xhci_pci" - "usbhid" - "usb_storage" + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "ahci" + "nvme" + "sd_mod" + "usb_storage" + "xhci_pci" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" ]; }; - fileSystems = { - "/" = { - device = "/dev/disk/by-label/NIXOS_SD"; - fsType = "ext4"; - options = [ "noatime" ]; - }; - }; + swapDevices = [ { device = "/dev/disk/by-label/SWAP"; } ]; - nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; - hardware.enableRedistributableFirmware = true; + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; } diff --git a/templates/generic-server/networking.nix b/templates/generic-server/networking.nix index c00bcf5..7bebf69 100644 --- a/templates/generic-server/networking.nix +++ b/templates/generic-server/networking.nix @@ -1,4 +1,4 @@ { - networking.hostName = "cryodev-pi"; + networking.hostName = "HOSTNAME"; networking.domain = "cryodev.xyz"; } diff --git a/templates/raspberry-pi/networking.nix b/templates/raspberry-pi/networking.nix index c00bcf5..7bebf69 100644 --- a/templates/raspberry-pi/networking.nix +++ b/templates/raspberry-pi/networking.nix @@ -1,4 +1,4 @@ { - networking.hostName = "cryodev-pi"; + networking.hostName = "HOSTNAME"; networking.domain = "cryodev.xyz"; }