Compare commits

..
Author SHA1 Message Date
beatzaplentyandClaude Sonnet 4.6 539bdf9833 fix(ha): resolve data disk device via by-id symlink even in dry-run
Check NixOS configurations / eval-hosts (pull_request) Successful in 10m21s
The by-id lookup is read-only so it's safe to run in dry-run mode.
Previously it was gated behind `if ! $DRY_RUN`, which always triggered
the sdb fallback warning in dry-run — making it look like the device
path wasn't reliable when the symlink actually exists on both servers.

Now the lookup always runs and the script errors out with a clear message
if the by-id symlink is genuinely missing, instead of silently falling
back to a guessed /dev/sd* name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 13:58:53 +10:00
beatzaplentyandClaude Sonnet 4.6 ced1657407 fix(ha): prefix qm commands with sudo for non-root Proxmox SSH user
Check NixOS configurations / eval-hosts (pull_request) Successful in 10m25s
qm lives at /usr/sbin/qm, which is not in the default PATH for
non-interactive SSH sessions as a non-root user.  Add PVE_SUDO (set to
"sudo" when PVE_SSH_USER != root, matching create-proxmox-resource.sh's
own sudo_prefix pattern) and prepend it to all three qm invocations in
the script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 13:53:41 +10:00
beatzaplenty fdf41c659c Merge pull request 'Worktree crispy churning kernighan' (#102) from worktree-crispy-churning-kernighan into main
Check NixOS configurations / eval-hosts (push) Successful in 10m50s
Reviewed-on: #102
2026-07-29 03:46:05 +00:00
beatzaplentyandClaude Sonnet 4.6 0ef8259225 fix(gc-hosts): source nix-daemon profile on pve1 before running gc
Check NixOS configurations / eval-hosts (pull_request) Successful in 10m34s
BatchMode SSH sessions don't source /etc/profile on non-NixOS hosts, so
nix-collect-garbage isn't on PATH for the wayne user. Source the nix-daemon
profile script explicitly, matching the pattern in scripts/lib/nix-bootstrap.sh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 13:45:48 +10:00
beatzaplentyandClaude Sonnet 4.6 629c1457a9 refactor(gc-hosts): discover running pve1 guests dynamically each run
Replace the static host list with dynamic discovery: workstation (nixos)
and pve1 are hard-wired first and second; remaining hosts are discovered
on every run by SSHing to pve1, listing running VMs/containers via
pct/qm list, and resolving their NixOS hostnames from a single flake eval.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 13:44:26 +10:00
beatzaplenty 7ba603c005 Merge pull request 'feat(scripts): add gc-hosts.sh for parallel nix gc across all live hosts' (#101) from worktree-crispy-churning-kernighan into main
Check NixOS configurations / eval-hosts (push) Successful in 10m32s
Reviewed-on: #101
2026-07-29 03:36:09 +00:00
beatzaplenty 4490dfab7d Merge pull request 'feat(nix-cache): authorize pve1's wayne key as remote builder client' (#100) from worktree-pve1-nix-cache-builder into main
Check NixOS configurations / eval-hosts (push) Successful in 10m32s
Reviewed-on: #100
2026-07-29 03:16:53 +00:00
beatzaplentyandClaude Sonnet 4.6 fa163b613d feat(nix-cache): authorize pve1's wayne key as remote builder client
Check NixOS configurations / eval-hosts (pull_request) Successful in 10m31s
Generated /home/wayne/.ssh/id_ed25519 on pve1 (single-user Nix install
owned by wayne, no daemon — builds run as wayne not root). Adds the
public key to remoteBuilderAuthorizedKeys so nix-cache accepts it.

After deploying nix-cache, re-run configure-nix-cache-client.sh as
wayne on pve1 to wire in the builder line and known_hosts entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 13:16:30 +10:00
3 changed files with 142 additions and 126 deletions

No files matched your search

+131 -114
View File
@@ -1,159 +1,182 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# gc-hosts.sh — Run nix-collect-garbage -d on all live NixOS hosts and pve1. # gc-hosts.sh — Run nix-collect-garbage -d on all live NixOS hosts.
# #
# nix-cache is excluded: it is the shared binary cache for all other hosts, so # The host list is rebuilt on every run:
# gc-ing it would evict cached store paths and force costly rebuilds elsewhere. # 1. This workstation (nixos) — always first
# 2. pve1 — always second (non-NixOS Proxmox node with Nix installed)
# 3. Every NixOS guest currently running on pve1 (discovered via pct/qm list)
# #
# Runs SSH jobs in parallel (up to MAX_JOBS at a time) and prints a summary. # nix-cache is excluded: gc-ing the shared binary cache evicts store paths
# that other hosts depend on for substitution.
#
# NixOS hosts: tries "sudo -n nix-collect-garbage -d" first (works when
# wheelNeedsPassword = false, e.g. the HA cluster). Falls back to user-level
# "nix-collect-garbage -d" if sudo needs a password — still collects
# unreferenced store paths and old nixos-user profile generations, but leaves
# old system generations in place.
# pve1: runs "nix-collect-garbage -d" as the login user (no system generations
# on a non-NixOS host).
# #
# Usage (from repo root): # Usage (from repo root):
# bash scripts/gc-hosts.sh [--dry-run] [<hostname> ...] # bash scripts/gc-hosts.sh [--dry-run]
#
# Options:
# --dry-run Print the SSH commands without executing them.
# <hostname> Limit to the given hostnames (bare names, no domain suffix).
# Default: all hosts in the list below.
#
# Sudo notes:
# NixOS hosts: tries "sudo -n nix-collect-garbage -d" first (non-interactive,
# works when wheelNeedsPassword = false such as on the HA cluster). Falls
# back to "nix-collect-garbage -d" as the nixos user if sudo requires a
# password — this still collects unreferenced store paths and removes old
# nixos-user profile generations, but will not remove old system generations.
# pve1: non-NixOS Proxmox node, runs nix-collect-garbage -d as wayne (no
# system generations to delete).
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
source scripts/env.sh 2>/dev/null || true source scripts/env.sh 2>/dev/null || true
source scripts/lib/nix-eval.sh 2>/dev/null || true
# ── config ──────────────────────────────────────────────────────────────────── # ── config ────────────────────────────────────────────────────────────────────
: "${MAX_JOBS:=6}" : "${MAX_JOBS:=8}"
: "${NIXOS_USER:=nixos}" : "${NIXOS_USER:=nixos}"
: "${PVE1_SSH_USER:=${PROXMOX_SSH_USER:-wayne}}"
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=10) SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=10)
# Map: hostname → ssh-user
# Update this list when new hosts are added/removed.
declare -A HOSTS=(
[docker]="$NIXOS_USER"
[ha-server-1]="$NIXOS_USER"
[ha-server-2]="$NIXOS_USER"
[nix-minimal]="$NIXOS_USER"
[nixos]="$NIXOS_USER"
[pxe-boot]="$NIXOS_USER"
[server]="$NIXOS_USER"
[tailscale-router]="$NIXOS_USER"
[tor-relay]="$NIXOS_USER"
[pve1]="${PROXMOX_SSH_USER:-wayne}"
)
# ── argument parsing ──────────────────────────────────────────────────────────
DRY_RUN=0 DRY_RUN=0
FILTER=()
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--dry-run) DRY_RUN=1 ;; --dry-run) DRY_RUN=1 ;;
--*) echo "Unknown option: $arg" >&2; exit 1 ;; *) echo "Unknown option: $arg" >&2; exit 1 ;;
*) FILTER+=("$arg") ;;
esac esac
done done
# Resolve the set of (hostname, user) pairs to process. # ── build the host list ───────────────────────────────────────────────────────
declare -A TARGET_USERS
if [[ ${#FILTER[@]} -gt 0 ]]; then # ORDERED_HOSTS: names in display/execution order.
for h in "${FILTER[@]}"; do # HOST_TARGET[name]: SSH target string (user@host).
if [[ -z "${HOSTS[$h]+_}" ]]; then # HOST_TYPE[name]: "nixos" (try sudo gc, fallback user) | "nix" (user gc only).
echo "Unknown hostname: $h (not in the gc-hosts list)" >&2 declare -a ORDERED_HOSTS=()
exit 1 declare -A HOST_TARGET=()
fi declare -A HOST_TYPE=()
TARGET_USERS[$h]="${HOSTS[$h]}" declare -A _SEEN_HOSTNAMES=() # dedup tracker
done
else _add_host() {
for h in "${!HOSTS[@]}"; do local name="$1" target="$2" type="$3"
TARGET_USERS[$h]="${HOSTS[$h]}" if [[ -n "${_SEEN_HOSTNAMES[$name]+_}" ]]; then return; fi
done _SEEN_HOSTNAMES[$name]=1
ORDERED_HOSTS+=("$name")
HOST_TARGET[$name]="$target"
HOST_TYPE[$name]="$type"
}
# 1. Workstation (hard-wired first)
_add_host "nixos" "${NIXOS_USER}@nixos" "nixos"
# 2. pve1 (hard-wired second; non-NixOS, no system generations)
_add_host "pve1" "${PVE1_SSH_USER}@${PVE1_HOST}" "nix"
# 3. Dynamically discover running NixOS guests on pve1
echo "Discovering running guests on ${PVE1_HOST}..."
# Evaluate the full flake hostname map in one shot.
hostname_map="{}"
if ! hostname_map="$(
nix eval --json "${NIX_EVAL_FLAGS[@]}" .#nixosConfigurations \
--apply 'cfgs: builtins.mapAttrs (_: cfg: cfg.config.networking.hostName) cfgs' \
2>/dev/null
)"; then
echo " warning: flake eval failed — skipping dynamic host discovery" >&2
fi fi
SORTED_HOSTS=($(printf '%s\n' "${!TARGET_USERS[@]}" | sort)) # Get names of all currently running guests from pve1.
if ssh "${SSH_OPTS[@]}" "${PVE1_SSH_USER}@${PVE1_HOST}" "true" 2>/dev/null; then
running_guests="$(
ssh "${SSH_OPTS[@]}" "${PVE1_SSH_USER}@${PVE1_HOST}" bash <<'REMOTE'
{ sudo pct list 2>/dev/null | awk 'NR>1 && $2=="running" { print $NF }';
sudo qm list 2>/dev/null | awk 'NR>1 && $3=="running" { print $2 }'; } | sort -u
REMOTE
)" || running_guests=""
# ── helpers ─────────────────────────────────────────────────────────────────── while IFS= read -r guest; do
[[ -z "$guest" ]] && continue
# Resolve flake target name → NixOS hostname.
hostname="$(printf '%s' "$hostname_map" \
| jq -r --arg g "$guest" '.[$g] // empty' 2>/dev/null || true)"
[[ -z "$hostname" ]] && continue
# Exclude nix-cache and any target whose hostname is already in our list.
case "$hostname" in nix-cache) continue ;; esac
if [[ -n "${_SEEN_HOSTNAMES[$hostname]+_}" ]]; then continue; fi
echo " + $guest → $hostname"
_add_host "$hostname" "${NIXOS_USER}@${hostname}" "nixos"
done <<< "$running_guests"
else
echo " warning: ${PVE1_HOST} unreachable — skipping dynamic host discovery" >&2
fi
echo ""
echo "Hosts: ${ORDERED_HOSTS[*]}"
echo ""
# ── dry-run ───────────────────────────────────────────────────────────────────
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "[dry-run] commands that would run:"
for host in "${ORDERED_HOSTS[@]}"; do
target="${HOST_TARGET[$host]}"
type="${HOST_TYPE[$host]}"
if [[ "$type" == "nixos" ]]; then
echo " ssh ${SSH_OPTS[*]} $target 'sudo -n nix-collect-garbage -d'"
echo " # fallback: ssh ... $target 'nix-collect-garbage -d'"
else
echo " ssh ${SSH_OPTS[*]} $target '. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && nix-collect-garbage -d'"
fi
done
exit 0
fi
# ── gc worker ─────────────────────────────────────────────────────────────────
gc_one() { gc_one() {
local host="$1" user="$2" logfile="$3" local host="$1" target="${HOST_TARGET[$1]}" type="${HOST_TYPE[$1]}" logfile="$2"
local target="${user}@${host}"
if ! ssh "${SSH_OPTS[@]}" "$target" "true" 2>>"$logfile"; then if ! ssh "${SSH_OPTS[@]}" "$target" "true" 2>>"$logfile"; then
echo "unreachable" echo "unreachable"; return
return
fi fi
# NixOS hosts: try passwordless sudo first. if [[ "$type" == "nixos" ]]; then
if [[ "$user" == "$NIXOS_USER" ]]; then
if ssh "${SSH_OPTS[@]}" "$target" "sudo -n nix-collect-garbage -d" \ if ssh "${SSH_OPTS[@]}" "$target" "sudo -n nix-collect-garbage -d" \
>>"$logfile" 2>>"$logfile"; then >>"$logfile" 2>>"$logfile"; then
echo "ok(sudo)" echo "ok(sudo)"; return
return
fi fi
# sudo required a password — fall back to user-level gc. echo "[sudo needs password — falling back to user-level gc]" >>"$logfile"
echo "[sudo needs password, falling back to user-level gc]" >>"$logfile"
if ssh "${SSH_OPTS[@]}" "$target" "nix-collect-garbage -d" \ if ssh "${SSH_OPTS[@]}" "$target" "nix-collect-garbage -d" \
>>"$logfile" 2>>"$logfile"; then >>"$logfile" 2>>"$logfile"; then
echo "ok(user)" echo "ok(user)"; return
return
fi fi
else else
# Non-NixOS node (pve1): no system generations; just gc as the login user. # Non-NixOS node: BatchMode SSH doesn't source the Nix daemon profile, so
if ssh "${SSH_OPTS[@]}" "$target" "nix-collect-garbage -d" \ # nix-collect-garbage won't be on PATH unless we source it explicitly.
local nix_profile='. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh 2>/dev/null || true'
if ssh "${SSH_OPTS[@]}" "$target" "$nix_profile && nix-collect-garbage -d" \
>>"$logfile" 2>>"$logfile"; then >>"$logfile" 2>>"$logfile"; then
echo "ok" echo "ok"; return
return
fi fi
fi fi
echo "failed:$?" echo "failed:$?"
} }
# ── dry-run ───────────────────────────────────────────────────────────────────
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "[dry-run] would run gc on: ${SORTED_HOSTS[*]}"
for host in "${SORTED_HOSTS[@]}"; do
user="${TARGET_USERS[$host]}"
if [[ "$user" == "$NIXOS_USER" ]]; then
echo " ssh ${SSH_OPTS[*]} ${user}@${host} 'sudo -n nix-collect-garbage -d'"
echo " # fallback: ssh ... 'nix-collect-garbage -d'"
else
echo " ssh ${SSH_OPTS[*]} ${user}@${host} 'nix-collect-garbage -d'"
fi
done
exit 0
fi
# ── parallel execution ──────────────────────────────────────────────────────── # ── parallel execution ────────────────────────────────────────────────────────
echo "Running gc on ${#ORDERED_HOSTS[@]} hosts (up to ${MAX_JOBS} parallel)..."
echo ""
TMPDIR_GC="$(mktemp -d)" TMPDIR_GC="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_GC"' EXIT trap 'rm -rf "$TMPDIR_GC"' EXIT
declare -A LOGS declare -A LOGS=()
echo "Running gc on ${#SORTED_HOSTS[@]} hosts (up to ${MAX_JOBS} parallel)..."
echo ""
job_count=0 job_count=0
for host in "${SORTED_HOSTS[@]}"; do
user="${TARGET_USERS[$host]}" for host in "${ORDERED_HOSTS[@]}"; do
logfile="${TMPDIR_GC}/${host}.log" logfile="${TMPDIR_GC}/${host}.log"
resultfile="${TMPDIR_GC}/${host}.result" resultfile="${TMPDIR_GC}/${host}.result"
LOGS[$host]="$logfile" LOGS[$host]="$logfile"
: > "$logfile" : > "$logfile"
( ( result="$(gc_one "$host" "$logfile")"; echo "$result" > "$resultfile" ) &
result="$(gc_one "$host" "$user" "$logfile")"
echo "$result" > "$resultfile"
) &
(( job_count++ )) || true (( job_count++ )) || true
if [[ "$job_count" -ge "$MAX_JOBS" ]]; then if [[ "$job_count" -ge "$MAX_JOBS" ]]; then
@@ -173,30 +196,24 @@ ok_hosts=()
warn_hosts=() warn_hosts=()
fail_hosts=() fail_hosts=()
for host in "${SORTED_HOSTS[@]}"; do for host in "${ORDERED_HOSTS[@]}"; do
resultfile="${TMPDIR_GC}/${host}.result" result="$(cat "${TMPDIR_GC}/${host}.result" 2>/dev/null || echo "failed:missing")"
result="$(cat "$resultfile" 2>/dev/null || echo "failed:missing")"
case "$result" in case "$result" in
ok|"ok(sudo)"|"ok(user)") ok|"ok(sudo)"|"ok(user)")
printf " %-20s %s\n" "$host" "$result" printf " %-22s %s\n" "$host" "$result"
ok_hosts+=("$host") ok_hosts+=("$host") ;;
;;
unreachable) unreachable)
printf " %-20s UNREACHABLE\n" "$host" printf " %-22s UNREACHABLE\n" "$host"
warn_hosts+=("$host") warn_hosts+=("$host") ;;
;;
*) *)
printf " %-20s FAILED (%s)\n" "$host" "$result" printf " %-22s FAILED (%s)\n" "$host" "$result"
fail_hosts+=("$host") fail_hosts+=("$host") ;;
;;
esac esac
done done
echo "" echo ""
echo " ${#ok_hosts[@]} succeeded, ${#warn_hosts[@]} unreachable, ${#fail_hosts[@]} failed" echo " ${#ok_hosts[@]} succeeded, ${#warn_hosts[@]} unreachable, ${#fail_hosts[@]} failed"
# Print logs for any non-ok host.
for host in "${warn_hosts[@]+"${warn_hosts[@]}"}" "${fail_hosts[@]+"${fail_hosts[@]}"}"; do for host in "${warn_hosts[@]+"${warn_hosts[@]}"}" "${fail_hosts[@]+"${fail_hosts[@]}"}"; do
logfile="${LOGS[$host]}" logfile="${LOGS[$host]}"
if [[ -s "$logfile" ]]; then if [[ -s "$logfile" ]]; then
+10 -12
View File
@@ -30,6 +30,8 @@ DATA_DISK_SLOT="${DATA_DISK_SLOT:-scsi1}" # Proxmox disk name (scsi1 = data di
HA_USER="${HA_USER:-nixos}" HA_USER="${HA_USER:-nixos}"
PVE_HOST="${PVE_HOST:-${PVE1_HOST}}" PVE_HOST="${PVE_HOST:-${PVE1_HOST}}"
PVE_SSH_USER="${PVE_SSH_USER:-${PROXMOX_SSH_USER}}" PVE_SSH_USER="${PVE_SSH_USER:-${PROXMOX_SSH_USER}}"
PVE_SUDO=""
[[ "$PVE_SSH_USER" != "root" ]] && PVE_SUDO="sudo"
# By-id symlink for the data disk; basename resolves to the raw block device. # By-id symlink for the data disk; basename resolves to the raw block device.
# matches variables.nix's haServerDrbdDisk. # matches variables.nix's haServerDrbdDisk.
DATA_DISK_BYID="${DATA_DISK_BYID:-scsi-0QEMU_QEMU_HARDDISK_drive-${DATA_DISK_SLOT}}" DATA_DISK_BYID="${DATA_DISK_BYID:-scsi-0QEMU_QEMU_HARDDISK_drive-${DATA_DISK_SLOT}}"
@@ -140,7 +142,7 @@ fi
echo "" echo ""
echo "Looking up VM IDs on ${PVE_HOST}..." echo "Looking up VM IDs on ${PVE_HOST}..."
QM_LIST=$(pve "qm list 2>/dev/null" || true) QM_LIST=$(pve "$PVE_SUDO qm list 2>/dev/null" || true)
VMID1=$(echo "$QM_LIST" | awk -v name="$NODE1" '$0 ~ name {print $1}' | head -1) VMID1=$(echo "$QM_LIST" | awk -v name="$NODE1" '$0 ~ name {print $1}' | head -1)
VMID2=$(echo "$QM_LIST" | awk -v name="$NODE2" '$0 ~ name {print $1}' | head -1) VMID2=$(echo "$QM_LIST" | awk -v name="$NODE2" '$0 ~ name {print $1}' | head -1)
@@ -182,12 +184,12 @@ echo "── Phase 1 — Proxmox disk resize (${DATA_DISK_SLOT} ${SIZE} on both
echo " ${DRY_PREFIX}qm resize $VMID1 ${DATA_DISK_SLOT} ${SIZE} ($NODE1 on ${PVE_HOST})" echo " ${DRY_PREFIX}qm resize $VMID1 ${DATA_DISK_SLOT} ${SIZE} ($NODE1 on ${PVE_HOST})"
if ! $DRY_RUN; then if ! $DRY_RUN; then
pve "qm resize $VMID1 ${DATA_DISK_SLOT} ${SIZE}" pve "$PVE_SUDO qm resize $VMID1 ${DATA_DISK_SLOT} ${SIZE}"
fi fi
echo " ${DRY_PREFIX}qm resize $VMID2 ${DATA_DISK_SLOT} ${SIZE} ($NODE2 on ${PVE_HOST})" echo " ${DRY_PREFIX}qm resize $VMID2 ${DATA_DISK_SLOT} ${SIZE} ($NODE2 on ${PVE_HOST})"
if ! $DRY_RUN; then if ! $DRY_RUN; then
pve "qm resize $VMID2 ${DATA_DISK_SLOT} ${SIZE}" pve "$PVE_SUDO qm resize $VMID2 ${DATA_DISK_SLOT} ${SIZE}"
fi fi
echo " Phase 1 done." echo " Phase 1 done."
@@ -202,17 +204,13 @@ rescan_node() {
local node_name=$1 run_fn=$2 local node_name=$1 run_fn=$2
# Resolve block device name from the stable by-id symlink on the guest. # Resolve block device name from the stable by-id symlink on the guest.
# Read-only lookup — safe to run even in dry-run so we show the real device.
local blk_dev="" local blk_dev=""
if ! $DRY_RUN; then blk_dev=$($run_fn "bash -c 'basename \$(readlink -f /dev/disk/by-id/${DATA_DISK_BYID})'" 2>/dev/null || true)
blk_dev=$($run_fn "bash -c 'basename \$(readlink -f /dev/disk/by-id/${DATA_DISK_BYID})'" 2>/dev/null || true)
fi
if [[ -z "$blk_dev" ]]; then if [[ -z "$blk_dev" ]]; then
# Fall back: scsi1 → index 1 → sdb, scsi2 → sdc, etc. echo " ERROR: /dev/disk/by-id/${DATA_DISK_BYID} not found on $node_name" >&2
local slot_idx echo " Check DATA_DISK_BYID or DATA_DISK_SLOT configuration." >&2
slot_idx=$(echo "$DATA_DISK_SLOT" | grep -oE '[0-9]+$' || echo "1") exit 1
blk_dev=$(printf "sd%s" "$(echo "abcdefghij" | cut -c$((slot_idx + 1)))")
echo " WARNING: could not resolve /dev/disk/by-id/${DATA_DISK_BYID} on $node_name;" \
"falling back to /dev/${blk_dev}" >&2
fi fi
echo " ${DRY_PREFIX}Rescanning /dev/${blk_dev} on ${node_name}..." echo " ${DRY_PREFIX}Rescanning /dev/${blk_dev} on ${node_name}..."
+1
View File
@@ -58,6 +58,7 @@
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBZ9WKKAlP9Z7GQdgaZ1Xgw9C+vja2lqEZO5rJFpVqYN root@ha-server-1" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBZ9WKKAlP9Z7GQdgaZ1Xgw9C+vja2lqEZO5rJFpVqYN root@ha-server-1"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEGNKlaaMckd8nLWNGz4B2QokXjnnIvM+rEUv+R6h0sp root@ha-server-2" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEGNKlaaMckd8nLWNGz4B2QokXjnnIvM+rEUv+R6h0sp root@ha-server-2"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK+XeMco7OxUpjrjZm54HogMs9QB5xlcKmElASRvrmlW root@nixos" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK+XeMco7OxUpjrjZm54HogMs9QB5xlcKmElASRvrmlW root@nixos"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMseQwPpmaa6cgV5U8KhUsiVSYARG85zGa9rho0LJWks wayne@pve1"
]; ];
# Admin SSH public key, authorized on the primary user of every host and # Admin SSH public key, authorized on the primary user of every host and