diff --git a/.gitignore b/.gitignore index 85bc657..41d2d77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .bootstrap/ .direnv/ +dump/ diff --git a/flake.nix b/flake.nix index 1f1d05f..d3dc134 100644 --- a/flake.nix +++ b/flake.nix @@ -153,13 +153,13 @@ type = "app"; program = "${pkgs.writeShellScript "bootstrap-host" ( builtins.replaceStrings - [ "@ADMIN_KEYS@" ] [ - (lib.concatMapStringsSep "\n" (key: " \"${key}\"") ( - lib.flatten ( - lib.mapAttrsToList (_: userCfg: userCfg.openssh.authorizedKeys.keys or [ ]) siteConfig.adminUsers - ) - )) + "@ADMIN_USERS_JSON@" + "@JQ@" + ] + [ + (builtins.toJSON siteConfig.adminUsers) + "${pkgs.jq}/bin/jq" ] (builtins.readFile ./scripts/bootstrap-host.sh) )}"; diff --git a/scripts/bootstrap-host.sh b/scripts/bootstrap-host.sh index 5795eb4..be9f27b 100644 --- a/scripts/bootstrap-host.sh +++ b/scripts/bootstrap-host.sh @@ -3,29 +3,39 @@ set -euo pipefail usage() { cat <<'USAGE' Usage: - nix run .#bootstrap-host -- [ssh-identity-file] - nix run .#bootstrap-host -- [ssh-identity-file] + nix run .#bootstrap-host -- [--admin ] [ssh-identity-file] + nix run .#bootstrap-host -- [--admin ] [ssh-identity-file] -Examples: - nix run .#bootstrap-host -- main-wiki root@203.0.113.10 ~/.ssh/do-bootstrap - nix run .#bootstrap-host -- root@203.0.113.10 root@203.0.113.11 ~/.ssh/do-bootstrap USAGE } -if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then - usage - exit 1 -fi - repo_root="$(pwd)" if [ ! -f "$repo_root/flake.nix" ]; then printf 'Run bootstrap-host from the repo root\n' >&2 exit 1 fi +admin_users_json='@ADMIN_USERS_JSON@' + +bootstrap_admin="jet" ssh_identity_file="" main_target="" replica_target="" +failures=() + +if [ "${1:-}" = "--admin" ]; then + if [ "$#" -lt 4 ]; then + usage + exit 1 + fi + bootstrap_admin="$2" + shift 2 +fi + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + usage + exit 1 +fi case "$1" in main-wiki|replica-wiki) @@ -43,8 +53,22 @@ case "$1" in ;; esac +admin_keys() { + printf '%s' "$admin_users_json" | @JQ@ -r --arg user "$1" '.[$user].openssh.authorizedKeys.keys[]? | " \"" + . + "\""' +} + +admin_exists() { + printf '%s' "$admin_users_json" | @JQ@ -e --arg user "$1" 'has($user)' >/dev/null +} + +if ! admin_exists "$bootstrap_admin"; then + printf 'Unknown admin user for bootstrap: %s\n' "$bootstrap_admin" >&2 + exit 1 +fi + make_host_module() { local module_file="$1" + local admin_name="$2" cat > "$module_file" <<'MODULE' { ... }: @@ -73,7 +97,7 @@ make_host_module() { }; }; - users.users.jet = { + users.users.BOOTSTRAP_ADMIN = { isNormalUser = true; extraGroups = [ "wheel" ]; openssh.authorizedKeys.keys = [ @@ -86,6 +110,9 @@ make_host_module() { services.do-agent.enable = false; } MODULE + + sed -i "s/BOOTSTRAP_ADMIN/$admin_name/" "$module_file" + perl -0pi -e 's/\n@ADMIN_KEYS@/\n'"$(admin_keys "$admin_name" | sed 's/[\/&]/\\&/g')"'/g' "$module_file" } run_bootstrap() { @@ -97,12 +124,14 @@ run_bootstrap() { local try local ssh_cmd=(ssh -o StrictHostKeyChecking=accept-new) local scp_cmd=(scp -o StrictHostKeyChecking=accept-new) + local admin_target work_dir="$(mktemp -d)" module_file="$work_dir/host-bootstrap.nix" remote_target="$target_host:/etc/nixos/host-bootstrap.nix" + admin_target="${bootstrap_admin}@${target_host#*@}" - make_host_module "$module_file" + make_host_module "$module_file" "$bootstrap_admin" if [ -n "$ssh_identity_file" ]; then ssh_cmd+=( -i "$ssh_identity_file" ) @@ -118,27 +147,48 @@ run_bootstrap() { printf 'Waiting for %s to reboot into NixOS\n' "$host_name" for try in $(seq 1 60); do - if "${ssh_cmd[@]}" -o ConnectTimeout=5 "$target_host" 'grep -q "^ID=nixos" /etc/os-release'; then + if "${ssh_cmd[@]}" -o ConnectTimeout=5 "$admin_target" 'grep -q "^ID=nixos" /etc/os-release' 2>/dev/null; then break fi sleep 5 done + if ! "${ssh_cmd[@]}" -o ConnectTimeout=5 "$admin_target" 'grep -q "^ID=nixos" /etc/os-release' 2>/dev/null; then + printf 'Bootstrap failed for %s: host did not come back as NixOS with %s access\n' "$host_name" "$bootstrap_admin" >&2 + failures+=( "$host_name" ) + rm -rf "$work_dir" + return 1 + fi + printf 'Finalizing network config on %s\n' "$host_name" - "${ssh_cmd[@]}" "$target_host" ' - sed -i "/defaultGateway6 = {/,/};/d" /etc/nixos/networking.nix 2>/dev/null || true - sed -i "/ipv6.routes = \[ { address = \"\"; prefixLength = 128; } \];/d" /etc/nixos/networking.nix 2>/dev/null || true - nixos-rebuild switch + "${ssh_cmd[@]}" "$admin_target" ' + sudo sed -i "/defaultGateway6 = {/,/};/d" /etc/nixos/networking.nix 2>/dev/null || true + sudo sed -i "/ipv6.routes = \[ { address = \"\"; prefixLength = 128; } \];/d" /etc/nixos/networking.nix 2>/dev/null || true + sudo nixos-rebuild switch ' + if ! "${ssh_cmd[@]}" "$admin_target" 'sudo -n true >/dev/null && test "$(systemctl is-system-running || true)" = running' 2>/dev/null; then + printf 'Bootstrap verification failed for %s: host is not healthy after first switch\n' "$host_name" >&2 + failures+=( "$host_name" ) + rm -rf "$work_dir" + return 1 + fi + + printf 'Bootstrap verified for %s\n' "$host_name" + rm -rf "$work_dir" } if [ -n "${main_target:-}" ]; then - run_bootstrap main-wiki "$main_target" + run_bootstrap main-wiki "$main_target" || true fi if [ -n "${replica_target:-}" ]; then - run_bootstrap replica-wiki "$replica_target" + run_bootstrap replica-wiki "$replica_target" || true fi -printf '\nBootstrap complete. The hosts should now be reachable as minimal NixOS systems over public SSH.\n' +if [ "${#failures[@]}" -ne 0 ]; then + printf '\nBootstrap failed for: %s\n' "${failures[*]}" >&2 + exit 1 +fi + +printf '\nBootstrap complete. The hosts should now be reachable as NixOS systems over public SSH as %s.\n' "$bootstrap_admin"