fd1c9287cd
BatchMode=yes (which the scripts set) implies strict host key checking and refuses to prompt — so first-time SSH to a host that isn't in known_hosts fails with "Host key verification failed". Every new host we register needs a manual `ssh <host>` round-trip first to store the key before the refresh scripts can reach it. accept-new fixes that: unknown hosts are auto-accepted into known_hosts on first connect; subsequent key CHANGES still fail loudly (as they should — that'd be a MITM signal). Matches the pattern already used by deploy-stack.sh. Affects only refresh-server-info.sh and refresh-proxmox-info.sh; deploy-stack.sh + sync-stacks.sh use their own targets that may or may not want the same treatment (leaving alone for now).
257 lines
7.9 KiB
Bash
Executable File
257 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# refresh-proxmox-info.sh — pull a fresh proxmox-details.txt from PVE nodes.
|
|
#
|
|
# Companion to refresh-server-info.sh. Same discovery pattern (hosts under
|
|
# servers/*/, ssh-target fallback), but pipes scripts/proxmox_inspect.sh
|
|
# over SSH and writes to servers/<host>/proxmox-details.txt.
|
|
#
|
|
# Host selection:
|
|
# - `all` expands to every servers/*/ dir name matching `*-pve` or whose
|
|
# existing proxmox-details.txt suggests it's a PVE node. Use the name
|
|
# explicitly to target a PVE host that doesn't match the pattern.
|
|
# - Explicit names are never filtered — they're trusted.
|
|
#
|
|
# For each host:
|
|
# 1. Run scripts/proxmox_inspect.sh on the remote via `ssh <target> 'bash -s'`.
|
|
# 2. Write output atomically to servers/<host>/proxmox-details.txt.
|
|
# A failed SSH/run never clobbers the previous good snapshot.
|
|
#
|
|
# Exit status is non-zero if any host failed.
|
|
#
|
|
# Usage:
|
|
# scripts/refresh-proxmox-info.sh show this help
|
|
# scripts/refresh-proxmox-info.sh all refresh every PVE host
|
|
# scripts/refresh-proxmox-info.sh pfi-pve refresh one host
|
|
# scripts/refresh-proxmox-info.sh pfi-pve nh3-pve refresh several
|
|
# scripts/refresh-proxmox-info.sh --dry-run all preview, no ssh
|
|
# scripts/refresh-proxmox-info.sh --validate-only all checks only
|
|
#
|
|
# Validation surfaces problems in the captured snapshot — missing `=====
|
|
# DONE =====` trailer, absent `pveversion` output (wrong script pointed at
|
|
# a non-PVE host), empty guest list when /etc/pve/jobs.cfg mentions vmids
|
|
# (backup job targeting retired VMs), etc.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
INSPECT="$SCRIPT_DIR/proxmox_inspect.sh"
|
|
SERVERS_DIR="$REPO_ROOT/servers"
|
|
|
|
if [ ! -f "$INSPECT" ]; then
|
|
echo "error: $INSPECT not found" >&2
|
|
exit 2
|
|
fi
|
|
|
|
DRY_RUN=0
|
|
VALIDATE_ONLY=0
|
|
REQUESTED=()
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--dry-run) DRY_RUN=1 ;;
|
|
--validate-only|--validate) VALIDATE_ONLY=1 ;;
|
|
-h|--help)
|
|
sed -n '2,32p' "$0"
|
|
exit 0
|
|
;;
|
|
-*) echo "error: unknown flag $arg" >&2; exit 2 ;;
|
|
*) REQUESTED+=("$arg") ;;
|
|
esac
|
|
done
|
|
|
|
discover_proxmox_hosts() {
|
|
# Default discovery: match names containing `-pve` either at the end
|
|
# (e.g. pfi-pve, nh3-pve, esh-pve) or as a segment (e.g. esh-pve-nas).
|
|
# Hosts with other naming that happen to be PVE should be called by
|
|
# name; they're outside the fleet-wide `all` sweep.
|
|
find "$SERVERS_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' \
|
|
| awk '/-pve(-|$)/' | sort
|
|
}
|
|
|
|
# No positional args → show help. Fleet-wide runs must be explicit.
|
|
if [ "${#REQUESTED[@]}" -eq 0 ]; then
|
|
sed -n '2,32p' "$0"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "${#REQUESTED[@]}" -eq 1 ] && [ "${REQUESTED[0]}" = "all" ]; then
|
|
mapfile -t HOSTS < <(discover_proxmox_hosts)
|
|
else
|
|
for r in "${REQUESTED[@]}"; do
|
|
if [ "$r" = "all" ]; then
|
|
echo "error: 'all' must be the only argument when used" >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
HOSTS=("${REQUESTED[@]}")
|
|
fi
|
|
|
|
if [ "${#HOSTS[@]}" -eq 0 ]; then
|
|
echo "error: no PVE hosts found (expected servers/*-pve/ dirs under $SERVERS_DIR)" >&2
|
|
exit 2
|
|
fi
|
|
|
|
resolve_target() {
|
|
# Identical logic to refresh-server-info.sh: ssh-target file wins when
|
|
# present, then ssh_config + /etc/hosts, finally the dir name as-is.
|
|
# Proxmox hosts in this fleet typically use `root@<name>` via key auth.
|
|
local host="$1"
|
|
local fallback="$SERVERS_DIR/$host/ssh-target"
|
|
if [ -f "$fallback" ]; then
|
|
local target
|
|
target=$(awk 'NF{print $1; exit}' "$fallback")
|
|
if [ -n "$target" ]; then echo "$target"; return; fi
|
|
fi
|
|
local effective
|
|
effective=$(ssh -G "$host" 2>/dev/null | awk '/^hostname /{print $2; exit}')
|
|
if [ -n "$effective" ] && getent hosts "$effective" >/dev/null 2>&1; then
|
|
echo "$host"
|
|
return
|
|
fi
|
|
echo "$host"
|
|
}
|
|
|
|
validate_host() {
|
|
local host="$1"
|
|
local dir="$SERVERS_DIR/$host"
|
|
|
|
if [ ! -d "$dir" ]; then
|
|
printf '%s\n' "dir missing: $dir"
|
|
return
|
|
fi
|
|
if [ ! -r "$dir" ] || [ ! -x "$dir" ]; then
|
|
printf '%s\n' "dir not readable/searchable (check permissions)"
|
|
return
|
|
fi
|
|
|
|
local stf="$dir/ssh-target"
|
|
if [ -e "$stf" ]; then
|
|
if [ ! -f "$stf" ]; then
|
|
printf '%s\n' "ssh-target is not a regular file"
|
|
elif [ ! -r "$stf" ]; then
|
|
printf '%s\n' "ssh-target not readable"
|
|
elif [ ! -s "$stf" ]; then
|
|
printf '%s\n' "ssh-target is empty"
|
|
else
|
|
local t
|
|
t=$(awk 'NF{print $1; exit}' "$stf" 2>/dev/null || true)
|
|
if [ -z "$t" ]; then
|
|
printf '%s\n' "ssh-target has no non-blank content"
|
|
elif [[ "$t" =~ [[:space:]] ]]; then
|
|
printf '%s\n' "ssh-target first token contains whitespace ('$t')"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
local effective=""
|
|
effective=$(ssh -G "$host" 2>/dev/null | awk '/^hostname /{print $2; exit}')
|
|
local resolves=0
|
|
if [ -n "$effective" ] && getent hosts "$effective" >/dev/null 2>&1; then
|
|
resolves=1
|
|
fi
|
|
if [ "$resolves" -eq 0 ] && [ ! -s "$stf" ]; then
|
|
printf '%s\n' "name '$host' does not resolve and no ssh-target fallback present"
|
|
fi
|
|
|
|
[ -f "$dir/README.md" ] || printf '%s\n' "README.md missing"
|
|
if [ ! -f "$dir/proxmox-details.txt" ]; then
|
|
printf '%s\n' "proxmox-details.txt missing (never refreshed)"
|
|
else
|
|
validate_snapshot "$dir/proxmox-details.txt"
|
|
fi
|
|
}
|
|
|
|
validate_snapshot() {
|
|
local file="$1"
|
|
if ! grep -q '^===== DONE =====' "$file"; then
|
|
printf '%s\n' "snapshot appears truncated (missing trailing '===== DONE =====' marker)"
|
|
fi
|
|
if ! grep -q '^PVE:' "$file"; then
|
|
printf '%s\n' "PVE version line missing — snapshot may have been captured against a non-PVE host"
|
|
fi
|
|
if grep -q 'pveversion not found' "$file"; then
|
|
printf '%s\n' "remote reports pveversion not found — target is not a Proxmox node"
|
|
fi
|
|
# Backup coverage sanity: flag any VERDICT=NO lines so gaps surface.
|
|
if grep -qE '^\s*[0-9]+\s+NO\s' "$file"; then
|
|
local n
|
|
n=$(grep -cE '^\s*[0-9]+\s+NO\s' "$file")
|
|
printf '%s\n' "backup coverage: $n guest(s) marked NO — review 'per-guest coverage' section"
|
|
fi
|
|
}
|
|
|
|
print_warnings() {
|
|
local indent="$1"; shift
|
|
local w
|
|
for w in "$@"; do
|
|
printf '%s! %s\n' "$indent" "$w"
|
|
done
|
|
}
|
|
|
|
pad=0
|
|
for h in "${HOSTS[@]}"; do (( ${#h} > pad )) && pad=${#h}; done
|
|
|
|
if [ "$VALIDATE_ONLY" -eq 1 ]; then
|
|
printf 'Validating %d PVE host(s):\n' "${#HOSTS[@]}"
|
|
total_warn=0
|
|
for host in "${HOSTS[@]}"; do
|
|
mapfile -t warnings < <(validate_host "$host")
|
|
if [ "${#warnings[@]}" -eq 0 ]; then
|
|
printf ' %-*s ok\n' "$pad" "$host"
|
|
else
|
|
printf ' %-*s %d warning(s)\n' "$pad" "$host" "${#warnings[@]}"
|
|
print_warnings " " "${warnings[@]}"
|
|
total_warn=$((total_warn + ${#warnings[@]}))
|
|
fi
|
|
done
|
|
if [ "$total_warn" -gt 0 ]; then exit 1; fi
|
|
exit 0
|
|
fi
|
|
|
|
printf 'Refreshing %d PVE host(s):\n' "${#HOSTS[@]}"
|
|
failed=()
|
|
for host in "${HOSTS[@]}"; do
|
|
out="$SERVERS_DIR/$host/proxmox-details.txt"
|
|
tmp="$out.new"
|
|
|
|
mapfile -t warnings < <(validate_host "$host")
|
|
|
|
target=$(resolve_target "$host")
|
|
if [ "$target" = "$host" ]; then
|
|
label="$host"
|
|
else
|
|
label="$host → $target"
|
|
fi
|
|
|
|
printf ' %-*s ' "$pad" "$label"
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
printf 'would run: ssh %s bash -s < %s > %s\n' "$target" "$INSPECT" "$out"
|
|
[ "${#warnings[@]}" -gt 0 ] && print_warnings " " "${warnings[@]}"
|
|
continue
|
|
fi
|
|
|
|
mkdir -p "$SERVERS_DIR/$host"
|
|
|
|
if ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "$target" 'bash -s' < "$INSPECT" > "$tmp" 2> "$tmp.err"; then
|
|
mv "$tmp" "$out"
|
|
rm -f "$tmp.err"
|
|
bytes=$(wc -c < "$out")
|
|
printf 'ok (%s bytes)\n' "$bytes"
|
|
else
|
|
rc=$?
|
|
rm -f "$tmp"
|
|
err=$(head -n 1 "$tmp.err" 2>/dev/null || true)
|
|
rm -f "$tmp.err"
|
|
printf 'FAIL (rc=%d) %s\n' "$rc" "$err"
|
|
failed+=("$host")
|
|
fi
|
|
|
|
[ "${#warnings[@]}" -gt 0 ] && print_warnings " " "${warnings[@]}"
|
|
done
|
|
|
|
if [ "${#failed[@]}" -gt 0 ]; then
|
|
printf '\n%d host(s) failed: %s\n' "${#failed[@]}" "${failed[*]}" >&2
|
|
exit 1
|
|
fi
|