From 41498150ea9782758d236a9c14b3ad3ba298cb7d Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Mon, 29 Jun 2026 17:25:30 -0600 Subject: [PATCH] Add release snapshot command --- README.md | 5 +++ README.md.tmpl | 5 +++ docs/jeannie.1.md | 5 +++ jeannie | 10 ++++- scripts/release-snapshot | 97 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100755 scripts/release-snapshot diff --git a/README.md b/README.md index 8e43857..4924a2d 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,7 @@ Run a read-only health snapshot from the Debian server with: ./jeannie recover-plan ./jeannie gitops-status ./jeannie cert-check +./jeannie release-snapshot ``` It reports host memory/disk, systemd services, Docker Compose stacks, @@ -309,6 +310,10 @@ and recent repo-server/application-controller errors. `cert-check` verifies public DNS, TLS certificate expiry, public website/Gitea HTTP status, and DuckDNS-to-OCI edge IP drift from the canonical inventory. +`release-snapshot` writes a timestamped pre-change report under the homelab +state directory with Git state, Kubernetes/Argo CD/Helm state, workload images, +public URL status, and pointers to the latest Gitea/OpenTofu backups. + Focused doctor commands run narrower read-only checks and print the most likely next step: diff --git a/README.md.tmpl b/README.md.tmpl index fcb82fa..2e0d62e 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -274,6 +274,7 @@ Run a read-only health snapshot from the Debian server with: ./{{ main_script }} recover-plan ./{{ main_script }} gitops-status ./{{ main_script }} cert-check +./{{ main_script }} release-snapshot ``` It reports host memory/disk, systemd services, Docker Compose stacks, @@ -309,6 +310,10 @@ and recent repo-server/application-controller errors. `cert-check` verifies public DNS, TLS certificate expiry, public website/Gitea HTTP status, and DuckDNS-to-OCI edge IP drift from the canonical inventory. +`release-snapshot` writes a timestamped pre-change report under the homelab +state directory with Git state, Kubernetes/Argo CD/Helm state, workload images, +public URL status, and pointers to the latest Gitea/OpenTofu backups. + Focused doctor commands run narrower read-only checks and print the most likely next step: diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index 703270a..532af6a 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -71,6 +71,11 @@ repo-server/application-controller errors. : Verify public DNS, TLS certificate expiry, public website/Gitea HTTP status, and DuckDNS-to-OCI edge IP drift from the canonical inventory. +`release-snapshot` +: Write a timestamped pre-change report with Git state, Kubernetes/Argo CD/Helm +state, workload images, public URL status, and latest Gitea/OpenTofu backup +pointers. + Prometheus alerts : High-signal alert rules are managed as the `apps/homelab-alerts` GitOps app. They cover node readiness, restart storms, unavailable deployments, storage diff --git a/jeannie b/jeannie index 7d8568d..630d77a 100755 --- a/jeannie +++ b/jeannie @@ -5981,6 +5981,11 @@ cert_check() { "${REPO_ROOT}/scripts/cert-check" } +release_snapshot() { + require_debian_server "release-snapshot" + "${REPO_ROOT}/scripts/release-snapshot" +} + case "${1:-}" in up) up @@ -6012,6 +6017,9 @@ case "${1:-}" in cert-check) cert_check ;; + release-snapshot) + release_snapshot + ;; validate) validate_homelab ;; @@ -6156,7 +6164,7 @@ case "${1:-}" in echo "Log: ${JEANNIE_LOG_FILE}" ;; *) - echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|capacity|recover-plan|gitops-status|cert-check|validate|access-audit|kubeconfig-readonly|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-restore|drill-gitea-restore|drill-pihole-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|security-scan|security-prepare|security-zap|security-k8s|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-runtime|security-attack-path|openwrt|nuke}" + echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|capacity|recover-plan|gitops-status|cert-check|release-snapshot|validate|access-audit|kubeconfig-readonly|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-restore|drill-gitea-restore|drill-pihole-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|security-scan|security-prepare|security-zap|security-k8s|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-runtime|security-attack-path|openwrt|nuke}" exit 1 ;; esac diff --git a/scripts/release-snapshot b/scripts/release-snapshot new file mode 100755 index 0000000..4f4f756 --- /dev/null +++ b/scripts/release-snapshot @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HOMELAB_STATE_DIR="${HOMELAB_STATE_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/homelab}" +SNAPSHOT_DIR="${HOMELAB_RELEASE_SNAPSHOT_DIR:-${HOMELAB_STATE_DIR}/release-snapshots}" +TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)" +REPORT="${SNAPSHOT_DIR}/release-snapshot-${TIMESTAMP}.txt" + +mkdir -p "$SNAPSHOT_DIR" +exec > >(tee "$REPORT") 2>&1 + +section() { + printf '\n== %s ==\n' "$1" +} + +have() { + command -v "$1" >/dev/null 2>&1 +} + +run_optional() { + local description="$1" + shift + + section "$description" + if "$@"; then + return 0 + fi + + printf 'warn: %s unavailable or failed\n' "$description" +} + +latest_file() { + local dir="$1" + local pattern="$2" + + if [ -d "$dir" ]; then + find "$dir" -maxdepth 1 -type f -name "$pattern" -print 2>/dev/null | sort | tail -1 + fi +} + +section "Snapshot Metadata" +printf 'created_utc=%s\n' "$TIMESTAMP" +printf 'report=%s\n' "$REPORT" +printf 'repo=%s\n' "$REPO_ROOT" +printf 'host=%s\n' "$(hostname 2>/dev/null || echo unknown)" + +section "Git State" +git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD +git -C "$REPO_ROOT" rev-parse HEAD +git -C "$REPO_ROOT" status --short +git -C "$REPO_ROOT" log --oneline -5 + +if have kubectl; then + run_optional "Kubernetes Nodes" kubectl get nodes -o wide + run_optional "Kubernetes Problem Pods" sh -c \ + 'kubectl get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded -o wide' + run_optional "Argo CD Applications" sh -c \ + 'kubectl -n argocd get applications.argoproj.io -o custom-columns=NAME:.metadata.name,SYNC:.status.sync.status,HEALTH:.status.health.status,REVISION:.status.sync.revision,PATH:.spec.source.path --no-headers' + run_optional "Workload Images" sh -c \ + 'kubectl get deploy,statefulset,daemonset -A -o jsonpath="{range .items[*]}{.metadata.namespace}{\"/\"}{.metadata.name}{\"\\t\"}{range .spec.template.spec.containers[*]}{.image}{\" \"}{end}{\"\\n\"}{end}"' +else + section "Kubernetes" + echo "kubectl not installed; skipping Kubernetes snapshot." +fi + +if have helm; then + run_optional "Helm Releases" helm list -A +else + section "Helm Releases" + echo "helm not installed; skipping Helm release snapshot." +fi + +section "Backup Pointers" +state_backup="$(latest_file "${HOMELAB_TOFU_STATE_BACKUP_DIR:-${HOMELAB_STATE_DIR}/tofu-state-backups}" 'tofu-state-*.tgz' || true)" +gitea_backup="$(latest_file "${LAB_GITEA_BACKUP_DIR:-/home/jv/backups/gitea}" 'gitea-*.zip' || true)" +printf 'latest_tofu_state_backup=%s\n' "${state_backup:-missing}" +printf 'latest_gitea_backup=%s\n' "${gitea_backup:-missing}" + +section "Public URLs" +for url in "${LAB_PUBLIC_URL:-https://lab2025.duckdns.org/}" "${LAB_GITEA_URL:-https://lab2025.duckdns.org/git/jv/my-homelab-configs}"; do + if have curl; then + status="$(curl -k -sS -o /dev/null -w '%{http_code}' --max-time 10 "$url" 2>/dev/null || true)" + printf '%-60s http=%s\n' "$url" "${status:-none}" + else + printf '%-60s curl not installed\n' "$url" + fi +done + +section "Next Actions" +cat <