From 0104c255a55f1927b4956667a031b1de477ff7d9 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sun, 13 Sep 2026 10:18:12 +0000 Subject: [PATCH] feat(cluster): cluster-shutdown/startup --allow-on-node pentru rulare pe pvemini Rulare detasata din dashboard/Discord: nssh local fara ssh la sine, CT 171 ramane pornit si moare odata cu pvemini (HA freeze il reporneste), email cu instructiunile de pornire, pauza si pentru testul DR si fereastra de patch. Garda CT 171 nu mai depinde de /proc/1/environ (era sarita fara root). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KddsXCqEbKMhdFJDYbAsx8 --- .../failover/{ => arhiva}/failover-vm201.sh | 0 .../{ => arhiva}/recover-vm201-to-pvemini.sh | 0 proxmox/cluster/scripts/cluster-shutdown.sh | 114 +++++++++++++++--- proxmox/cluster/scripts/cluster-startup.sh | 35 ++++-- 4 files changed, 126 insertions(+), 23 deletions(-) rename proxmox/cluster/failover/{ => arhiva}/failover-vm201.sh (100%) rename proxmox/cluster/failover/{ => arhiva}/recover-vm201-to-pvemini.sh (100%) diff --git a/proxmox/cluster/failover/failover-vm201.sh b/proxmox/cluster/failover/arhiva/failover-vm201.sh similarity index 100% rename from proxmox/cluster/failover/failover-vm201.sh rename to proxmox/cluster/failover/arhiva/failover-vm201.sh diff --git a/proxmox/cluster/failover/recover-vm201-to-pvemini.sh b/proxmox/cluster/failover/arhiva/recover-vm201-to-pvemini.sh similarity index 100% rename from proxmox/cluster/failover/recover-vm201-to-pvemini.sh rename to proxmox/cluster/failover/arhiva/recover-vm201-to-pvemini.sh diff --git a/proxmox/cluster/scripts/cluster-shutdown.sh b/proxmox/cluster/scripts/cluster-shutdown.sh index 7f0b6d4..e5c1810 100755 --- a/proxmox/cluster/scripts/cluster-shutdown.sh +++ b/proxmox/cluster/scripts/cluster-shutdown.sh @@ -9,12 +9,16 @@ # # RULEAZĂ DE PE STAȚIA DE ADMIN, nu de pe un nod Proxmox. Motivul: nodul care se # oprește ultimul nu poate raporta rezultatul propriei opriri. +# Excepție: cu --allow-on-node poate rula chiar pe pvemini (lansat din +# dashboard/Discord, detașat prin systemd-run) — nssh către pvemini devine +# execuție locală, iar pvemini se oprește ultimul, singur, la pasul 8. # # Utilizare: # ./cluster-shutdown.sh # interactiv, cere confirmare # ./cluster-shutdown.sh --yes # fără confirmare # ./cluster-shutdown.sh --dry-run # arată ce ar face, nu execută nimic # ./cluster-shutdown.sh --no-nodes # oprește doar guest-urile, lasă nodurile pornite +# ./cluster-shutdown.sh --allow-on-node # permite rularea chiar pe pvemini # set -euo pipefail @@ -45,12 +49,17 @@ GUEST_SHUTDOWN_ORDER=( 103 # dokploy 102 # docker.romfast.ro 100 # portainer - 171 # claude-agent + 171 # claude-agent — cu --allow-on-node rămâne pornit, vezi KEEP_RUNNING 109 # oracle-dr-windows 301 # docker-portainer-template 108 # central-oracle — ULTIMUL ) +# Cu --allow-on-node, acest guest NU e oprit explicit: ha-manager set --state +# stopped ar marca resursa ca oprită și HA n-ar mai reporni-o la boot. Moare +# oricum odată cu pvemini, iar shutdown_policy=freeze îl repornește după. +KEEP_RUNNING=171 + ORACLE_CT=108 ORACLE_DOCKER=oracle-xe @@ -60,14 +69,19 @@ CRON_BACKUP=/root/crontab.backup-mentenanta.txt # Cron-uri de dezactivat cât timp clusterul e jos (altfel mail storm, iar # vm109-watchdog.sh chiar pornește VM 109). -CRON_PATTERN='oom-alert|pvemini-down-alert|pveelite-down-alert|vm109-watchdog' +CRON_PATTERN='oom-alert|pvemini-down-alert|pveelite-down-alert|vm109-watchdog|weekly-dr-test-proxmox|vm109-patch-window' + +# Destinatarul instrucțiunilor de pornire, trimise prin email după ce starea +# clusterului e salvată. +ALERT_RECIPIENT="${ALERT_RECIPIENT:-mmarius28@gmail.com}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -STATE_FILE="${CLUSTER_STATE_FILE:-$SCRIPT_DIR/.cluster-state.txt}" +LOCAL_NODE=$(hostname) ASSUME_YES=0 DRY_RUN=0 SKIP_NODES=0 +ALLOW_ON_NODE=0 # ------------------------------------------------------------------- utilitare @@ -90,6 +104,10 @@ ping_host() { nssh() { local node=$1; shift + if (( ALLOW_ON_NODE )) && [[ "$node" == "$LOCAL_NODE" ]]; then + bash -c "$*" + return + fi ssh -o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new \ "root@${NODE_IP[$node]}" "$@" } @@ -115,26 +133,40 @@ confirm() { while (( $# )); do case "$1" in - --yes|-y) ASSUME_YES=1 ;; - --dry-run) DRY_RUN=1 ;; - --no-nodes) SKIP_NODES=1 ;; - -h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; - *) die "Argument necunoscut: $1" ;; + --yes|-y) ASSUME_YES=1 ;; + --dry-run) DRY_RUN=1 ;; + --no-nodes) SKIP_NODES=1 ;; + --allow-on-node) ALLOW_ON_NODE=1 ;; + -h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) die "Argument necunoscut: $1" ;; esac shift done +STATE_FILE="${CLUSTER_STATE_FILE:-}" +if [[ -z "$STATE_FILE" ]]; then + if (( ALLOW_ON_NODE )); then + STATE_FILE=/opt/scripts/.cluster-state.txt + else + STATE_FILE="$SCRIPT_DIR/.cluster-state.txt" + fi +fi + # --------------------------------------------------------- garda de rulare # Scriptul NU trebuie rulat de pe un nod Proxmox (s-ar opri singur la mijloc) și # nici din CT 171 claude-agent (e în lista de oprit — s-ar sinucide). -if command -v pvecm >/dev/null 2>&1; then - die "Rulezi pe un nod Proxmox. Scriptul trebuie rulat de pe stația de admin." +# --allow-on-node ridică prima interdicție: rulat din dashboard/Discord, chiar +# pe pvemini, prin systemd-run. +if command -v pvecm >/dev/null 2>&1 && (( ! ALLOW_ON_NODE )); then + die "Rulezi pe un nod Proxmox. Scriptul trebuie rulat de pe stația de admin (sau cu --allow-on-node)." +fi +# Hostname-ul se verifica independent: /proc/1/environ nu e lizibil fara root, +# iar garda era sarita in tacere pentru userul claude. +if [[ "$(hostname)" == *claude-agent* ]]; then + die "Rulezi din CT 171 (claude-agent), care e în lista de oprit. Mută-te pe stația de admin." fi if [[ -r /proc/1/environ ]] && grep -qa 'container=lxc' /proc/1/environ 2>/dev/null; then - if [[ "$(hostname)" == *claude-agent* ]]; then - die "Rulezi din CT 171 (claude-agent), care e în lista de oprit. Mută-te pe stația de admin." - fi warn "Rulezi dintr-un container LXC. Asigură-te că nu e unul găzduit pe acest cluster." fi @@ -218,6 +250,28 @@ if (( ! DRY_RUN )); then fi fi +# Instrucțiuni de pornire, trimise acum cât timp mai există cine să le citească — +# clusterul urmează să se oprească. +if (( DRY_RUN )); then + printf '%s [dry-run] email: instrucțiuni de pornire către %s%s\n' "$c_dim" "$ALERT_RECIPIENT" "$c_off" +else + MAIL_BODY=$(cat </dev/null || true + sleep 15 + systemctl poweroff --no-block + exit 0 + fi + run "$node" "systemctl poweroff --no-block" || true (( DRY_RUN )) && continue diff --git a/proxmox/cluster/scripts/cluster-startup.sh b/proxmox/cluster/scripts/cluster-startup.sh index 28b125a..7a4af48 100755 --- a/proxmox/cluster/scripts/cluster-startup.sh +++ b/proxmox/cluster/scripts/cluster-startup.sh @@ -11,6 +11,9 @@ # implicită de mai jos. # # RULEAZĂ DE PE STAȚIA DE ADMIN, nu de pe un nod. +# Excepție: cu --allow-on-node poate rula chiar pe pvemini (lansat din +# dashboard/Discord, detașat prin systemd-run) — nssh către pvemini devine +# execuție locală. # # NODURILE TREBUIE PORNITE FIZIC ÎNTÂI (sau prin WoL). Scriptul le așteaptă. # Pornește-le pe TOATE TREI aproximativ simultan: un singur nod pornit nu are @@ -21,6 +24,7 @@ # ./cluster-startup.sh --yes # fără confirmări # ./cluster-startup.sh --dry-run # arată ce ar face # ./cluster-startup.sh --all # ignoră fișierul de stare, pornește lista implicită +# ./cluster-startup.sh --allow-on-node # permite rularea chiar pe pvemini # set -euo pipefail @@ -87,11 +91,12 @@ GUEST_WAIT=300 CRON_BACKUP=/root/crontab.backup-mentenanta.txt SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -STATE_FILE="${CLUSTER_STATE_FILE:-$SCRIPT_DIR/.cluster-state.txt}" +LOCAL_NODE=$(hostname) ASSUME_YES=0 DRY_RUN=0 USE_ALL=0 +ALLOW_ON_NODE=0 # ------------------------------------------------------------------- utilitare @@ -113,6 +118,10 @@ ping_host() { nssh() { local node=$1; shift + if (( ALLOW_ON_NODE )) && [[ "$node" == "$LOCAL_NODE" ]]; then + bash -c "$*" + return + fi ssh -o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new \ "root@${NODE_IP[$node]}" "$@" } @@ -136,17 +145,27 @@ confirm() { while (( $# )); do case "$1" in - --yes|-y) ASSUME_YES=1 ;; - --dry-run) DRY_RUN=1 ;; - --all) USE_ALL=1 ;; - -h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; - *) die "Argument necunoscut: $1" ;; + --yes|-y) ASSUME_YES=1 ;; + --dry-run) DRY_RUN=1 ;; + --all) USE_ALL=1 ;; + --allow-on-node) ALLOW_ON_NODE=1 ;; + -h|--help) sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) die "Argument necunoscut: $1" ;; esac shift done -if command -v pvecm >/dev/null 2>&1; then - die "Rulezi pe un nod Proxmox. Scriptul trebuie rulat de pe stația de admin." +STATE_FILE="${CLUSTER_STATE_FILE:-}" +if [[ -z "$STATE_FILE" ]]; then + if (( ALLOW_ON_NODE )); then + STATE_FILE=/opt/scripts/.cluster-state.txt + else + STATE_FILE="$SCRIPT_DIR/.cluster-state.txt" + fi +fi + +if command -v pvecm >/dev/null 2>&1 && (( ! ALLOW_ON_NODE )); then + die "Rulezi pe un nod Proxmox. Scriptul trebuie rulat de pe stația de admin (sau cu --allow-on-node)." fi # ------------------------------------------------- pas 1: așteaptă nodurile