Files
configs/ai/skills/homelab-infrastructure-review/scripts/homelab-infrastructure-review.sh

128 lines
6.6 KiB
Bash

#!/usr/bin/env bash
#
# homelab-infrastructure-review.sh
# Triggers a full, intelligent, context-aware reconciliation audit.
#
set -euo pipefail
# ── Configuration & Logging ──────────────────────────────────────────────────
AUDIT_CONFIG_FILE="/srv/configs/ai/skills/homelab-infrastructure-review/scripts/audit_config.yaml"
INFRA_FILE="/srv/docs/public/projects/homelab/homelab_Infra.md"
LOCK_FILE="/srv/configs/ai/.audit.lock"
log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
# ── Pre-flight & Lock ────────────────────────────────────────────────────────
acquire_lock() {
if [ -f "$LOCK_FILE" ]; then
local pid; pid=$(cat "$LOCK_FILE" 2>/dev/null)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
log_error "Audit already running (PID $pid). Aborting."; exit 1
fi
log_info "Stale lock file found. Cleaning up."; rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"; trap 'rm -f "$LOCK_FILE"' EXIT
}
preflight_checks() {
if [ ! -f "$AUDIT_CONFIG_FILE" ]; then log_error "audit_config.yaml not found. Aborting."; exit 1; fi
log_info "All pre-flight checks passed."
}
# ── Data Sources ─────────────────────────────────────────────────────────────
get_infra_docs() {
local in_services_section=false
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" =~ ^##[[:space:]]+(Docker|Truenas)[[:space:]]+services ]]; then
in_services_section=true
elif [[ "$line" =~ ^##[[:space:]] && ! "$line" =~ ^##[[:space:]]+(Docker|Truenas)[[:space:]]+services ]]; then
in_services_section=false
fi
if $in_services_section && [[ "$line" =~ ^\|[[:space:]]*\*\*([a-zA-Z0-9_-]+)\*\* ]]; then
IFS='|' read -r _ name _ <<< "$line"; printf '%s\n' "$(echo "$name" | tr -d '*' | xargs)"
fi
done < "$INFRA_FILE"
}
get_live_docker_containers() {
while IFS= read -r line; do
local name; name=$(echo "$line" | jq -r '.Names')
local health; health=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no_health_check{{end}}' "$name" 2>/dev/null || echo "error")
local status;
case "$health" in
healthy) status="healthy";;
unhealthy) status="UNHEALTHY";;
*) status="no_health_check";;
esac
printf '%s|%s\n' "$name" "$status"
done <<< "$(docker ps --format '{{json .}}')"
}
get_mcp_nodes() {
local service_key=""; if [ -f "/srv/configs/docker_compose/homelabel/.env" ]; then service_key=$(grep -E "^MCP_SERVICE_KEY=" "/srv/configs/docker_compose/homelabel/.env" | cut -d'=' -f2-); fi
curl -s -H "X-MCP-Service-Key: $service_key" "http://localhost:9445/api/v1/nodes" | jq -r '.[]? | .label // .name // "unknown"'
}
# ── Reconciliation Engine ────────────────────────────────────────────────────
if [ -f "$AUDIT_CONFIG_FILE" ]; then
source <(python3 /srv/configs/ai/skills/homelab-infrastructure-review/scripts/load_config.py "$AUDIT_CONFIG_FILE")
fi
reconcile() {
local doc_services=(); while IFS= read -r name; do doc_services+=("$(echo "$name"|tr '[:upper:]' '[:lower:]')"); done < <(get_infra_docs)
local live_services=(); local untracked_report=""; local unhealthy_report=""
while IFS='|' read -r name status; do
local lower_name; lower_name=$(echo "$name" | tr '[:upper:]' '[:lower:]')
live_services+=("$lower_name")
if [[ " ${IGNORE_CONTAINERS} " == *" ${lower_name} "* ]]; then continue; fi
local found=false; for doc in "${doc_services[@]}"; do [ "$lower_name" = "$doc" ] && found=true && break; done
local alias_var="SERVICE_ALIAS_${lower_name//-/_}"
if ! $found && [[ -v $alias_var ]]; then
local target="${!alias_var}"; for doc in "${doc_services[@]}"; do [ "$target" = "$doc" ] && found=true && break; done
fi
if ! $found; then untracked_report+=" ⚠ **$name** is running but not documented.\n"; fi
if [[ "$status" == "UNHEALTHY" ]]; then unhealthy_report+=" ✗ **$name** is **UNHEALTHY**.\n"; fi
done < <(get_live_docker_containers)
local missing_report=""
for doc in "${doc_services[@]}"; do
if [ "$doc" = "dev" ] || [ "$doc" = "calibre" ]; then continue; fi # Exceptions
local found=false; for live in "${live_services[@]}"; do [ "$doc" = "$live" ] && found=true && break; done
if ! $found; then missing_report+=" ✗ **$doc** is documented but not running.\n"; fi
done
local mcp_nodes=(); while IFS= read -r label; do mcp_nodes+=("$(echo "$label"|tr '[:upper:]' '[:lower:]'|tr ' _-' ' ')"); done < <(get_mcp_nodes)
local canvas_report=""
for name in "${live_services[@]}"; do
if [[ " ${IGNORE_CONTAINERS} " == *" ${name} "* ]]; then continue; fi
local found=false; for node in "${mcp_nodes[@]}"; do [[ "$node" == *"$name"* || "$name" == *"$node"* ]] && found=true && break; done
local alias_var="CANVAS_ALIAS_${name//-/_}"
if ! $found && [[ -v $alias_var ]]; then
local target="${!alias_var}"; for node in "${mcp_nodes[@]}"; do [ "$target" = "$node" ] && found=true && break; done
fi
if ! $found; then canvas_report+=" ⚠ **$name** is running but not on the canvas.\n"; fi
done
printf "## Homelab Audit Report\nGenerated: $(date)\n\n"
printf "### Docker vs. Documentation\n"
if [ -z "$untracked_report$missing_report$unhealthy_report" ]; then
printf " ✅ All services are aligned and healthy.\n"
else
printf "%s%s%s" "$untracked_report" "$missing_report" "$unhealthy_report"
fi
printf "\n### Docker vs. Canvas\n"
if [ -z "$canvas_report" ]; then printf " ✅ All services are mapped.\n"; else printf "%s" "$canvas_report"; fi
}
# ── Main Execution ───────────────────────────────────────────────────────────
main() {
log_info "Starting Homelab Infrastructure Audit"
acquire_lock
preflight_checks
log_info "Running reconciliation..."
reconcile
}
main "$@"