Files
ROMFASTSQL/proxmox/cluster/scripts/cluster-startup.sh
Claude Agent 0104c255a5 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KddsXCqEbKMhdFJDYbAsx8
2026-09-13 10:18:12 +00:00

494 lines
18 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# cluster-startup.sh — repornirea clusterului Proxmox `romfast` după o oprire
# planificată făcută cu cluster-shutdown.sh.
#
# Așteaptă nodurile, așteaptă quorumul, pornește guest-urile în ordinea corectă
# de dependențe (Oracle primul), restaurează cron-urile de monitorizare.
#
# Pornește DOAR guest-urile care rulau înainte de oprire, citite din fișierul de
# stare scris de cluster-shutdown.sh. Fără fișier de stare, folosește lista
# 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
# quorum, /etc/pve rămâne read-only și nu se poate porni niciun guest.
#
# Utilizare:
# ./cluster-startup.sh # interactiv
# ./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
# ---------------------------------------------------------------- configurare
declare -A NODE_IP=(
[pve1]=10.0.20.200
[pvemini]=10.0.20.201
[pveelite]=10.0.20.202
)
# Ordinea de PORNIRE a guest-urilor: baza de date întâi, consumatorii după.
# Inversul ordinii de oprire din cluster-shutdown.sh.
GUEST_START_ORDER=(
108 # central-oracle — PRIMUL, restul depind de el
201 # roacentral — IIS reverse proxy, consumator Oracle
104 # flowise — consumator Oracle
100 # portainer
102 # docker.romfast.ro
103 # dokploy
106 # gitea
171 # claude-agent
101 # minecraft
110 # moltbot
303 # Win11-Adina
304 # Win11-Marius
302 # oracle-test-302
109 # oracle-dr-windows — DR, în mod normal rămâne oprit
301 # docker-portainer-template
310 # Win11-Template
)
# Guest-uri care NU se pornesc automat când LIPSEȘTE fișierul de stare.
# Dacă fișierul de stare există, el decide — dacă erau pornite, se repornesc.
# 109 — VM DR. Pornirea lui nedorită a declanșat bucla OOM din incidentul
# 2026-04-20; în mod normal stă oprit și e pornit doar de testul DR.
# 301, 310 — template-uri, nu servicii.
NEVER_AUTOSTART=(109 301 310)
# Rețeaua dedicată de cluster („insula"), existentă din 2026-08-29. De ea atârnă
# replicarea ZFS (migration: network=10.10.10.0/24 în datacenter.cfg) și storage-ul
# NFS backup-pvemini. Pe pve1 și pveelite IP-ul stă pe dongle-ul USB, adus la boot
# de un „auto enx..." din /etc/network/interfaces: dacă nucleul enumerează USB-ul
# DUPĂ ce networking.service a terminat, interfața rămâne fără IP, iar nodul pare
# perfect sănătos. Detalii: ../docs/switch-2.5g-dedicat-noduri.md
declare -A ISLAND_IP=(
[pve1]=10.10.10.200
[pvemini]=10.10.10.201
[pveelite]=10.10.10.202
)
ISLAND_NFS_MOUNT=/mnt/pve/backup-pvemini
ISLAND_NFS_SERVER=10.10.10.201
ISLAND_NFS_NODES=(pve1 pveelite)
ORACLE_CT=108
ORACLE_DOCKER=oracle-xe
# Măsurat pe 2026-08-29, la pornire de la rece după oprirea completă a clusterului:
# instanța a avut nevoie de ~10 minute ca să ajungă OPEN. 600s erau prea strâmți.
ORACLE_WAIT=1200 # secunde așteptate până baza răspunde la interogări
NODE_WAIT=1800 # secunde așteptate până apar toate nodurile
QUORUM_WAIT=300
GUEST_WAIT=300
CRON_BACKUP=/root/crontab.backup-mentenanta.txt
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_NODE=$(hostname)
ASSUME_YES=0
DRY_RUN=0
USE_ALL=0
ALLOW_ON_NODE=0
# ------------------------------------------------------------------- utilitare
c_red=$'\033[31m'; c_grn=$'\033[32m'; c_yel=$'\033[33m'; c_dim=$'\033[2m'; c_off=$'\033[0m'
log() { printf '%s[%s]%s %s\n' "$c_dim" "$(date +%H:%M:%S)" "$c_off" "$*"; }
ok() { printf '%s ✓%s %s\n' "$c_grn" "$c_off" "$*"; }
warn() { printf '%s !%s %s\n' "$c_yel" "$c_off" "$*"; }
die() { printf '%s ✗ %s%s\n' "$c_red" "$*" "$c_off" >&2; exit 1; }
step() { printf '\n%s══ %s%s\n' "$c_grn" "$*" "$c_off"; }
ping_host() {
if [[ "${OSTYPE:-}" == msys* || "${OSTYPE:-}" == cygwin* || "${OS:-}" == Windows_NT ]]; then
ping -n 1 -w 2000 "$1" >/dev/null 2>&1
else
ping -c 1 -W 2 "$1" >/dev/null 2>&1
fi
}
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]}" "$@"
}
run() {
local node=$1; shift
if (( DRY_RUN )); then
printf '%s [dry-run] %s: %s%s\n' "$c_dim" "$node" "$*" "$c_off"
return 0
fi
nssh "$node" "$@"
}
confirm() {
(( ASSUME_YES || DRY_RUN )) && return 0
local reply
printf '\n%s%s%s\n' "$c_yel" "$1" "$c_off"
read -r -p "Scrie DA ca să continui: " reply
[[ "$reply" == "DA" ]] || die "Anulat de utilizator."
}
while (( $# )); do
case "$1" in
--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
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
step "Aștept nodurile"
warn "Nodurile trebuie pornite FIZIC. Pornește-le pe toate trei aproximativ simultan."
waited=0
while (( waited < NODE_WAIT )); do
missing=()
for node in "${!NODE_IP[@]}"; do
ping_host "${NODE_IP[$node]}" || missing+=("$node")
done
(( ${#missing[@]} == 0 )) && break
printf '\r%s aștept: %s (%ds) %s' "$c_dim" "${missing[*]}" "$waited" "$c_off"
sleep 10; waited=$(( waited + 10 ))
done
printf '\r%*s\r' 70 ''
(( waited < NODE_WAIT )) || die "Nodurile ${missing[*]} nu au apărut în ${NODE_WAIT}s."
for node in "${!NODE_IP[@]}"; do
w=0
until nssh "$node" true 2>/dev/null; do
(( w += 10 )); (( w > 300 )) && die "$node răspunde la ping dar nu la SSH."
sleep 10
done
ok "$node — up, SSH OK ($(nssh "$node" 'uptime -p'))"
done
# --------------------------------------------------- pas 2: așteaptă quorum
step "Aștept quorumul"
waited=0
until nssh pvemini "pvecm status 2>/dev/null | grep -q 'Quorate: *Yes'"; do
(( waited >= QUORUM_WAIT )) && die "Clusterul nu a atins quorumul în ${QUORUM_WAIT}s."
printf '\r%s aștept quorum (%ds)...%s' "$c_dim" "$waited" "$c_off"
sleep 10; waited=$(( waited + 10 ))
done
printf '\r%*s\r' 40 ''
VOTES=$(nssh pvemini "pvecm status | awk '/Total votes/ {print \$3}'")
[[ "$VOTES" == "3" ]] || warn "Quorum atins, dar doar $VOTES/3 voturi. Un nod lipsește din cluster."
ok "quorum OK, $VOTES/3 voturi"
nssh pvemini "ha-manager status | head -4"
# ----------------------------------- pas 3: rețeaua dedicată de cluster (insula)
# Se verifică ÎNAINTE de pornirea guest-urilor: dacă insula e jos, replicarea și
# backup-ul NFS tac fără nicio alertă, iar simptomul apare abia peste ore.
# Nu blochează pornirea — la revenirea curentului, Oracle sus contează mai mult
# decât replicarea — dar nu te lasă să treci pe lângă problemă fără să o vezi.
step "Verific rețeaua de cluster (10.10.10.0/24)"
if (( DRY_RUN )); then
warn "dry-run — sar peste verificarea insulei"
else
ISLAND_BAD=()
for node in "${!NODE_IP[@]}"; do
want="${ISLAND_IP[$node]}"
have=$(nssh "$node" "ip -4 -o addr show | grep -c ' $want/' || true")
if [[ "$have" == "0" ]]; then
# Cazul așteptat de eșec: dongle-ul USB enumerat târziu la boot.
# ifreload -a aplică doar diferențele față de /etc/network/interfaces,
# deci nu atinge interfețele tap/veth ale guest-urilor.
warn "$node — lipsește $want, încerc 'ifreload -a'"
nssh "$node" "PATH=/usr/sbin:/sbin:/usr/bin:/bin ifreload -a" || true
sleep 8
have=$(nssh "$node" "ip -4 -o addr show | grep -c ' $want/' || true")
fi
if [[ "$have" == "0" ]]; then
ISLAND_BAD+=("$node")
warn "$node — TOT fără $want după ifreload"
else
ok "$node — $want prezent"
fi
done
# Un IP configurat local nu garantează și cablul/switch-ul. pvemini e sursa
# replicării și serverul NFS, deci se verifică din el spre celelalte două.
for node in pve1 pveelite; do
ip="${ISLAND_IP[$node]}"
if nssh pvemini "ping -c1 -W2 $ip >/dev/null 2>&1"; then
ok "pvemini → $node ($ip) — ping OK"
else
printf '%s\n' "${ISLAND_BAD[@]:-}" | grep -qx "$node" || ISLAND_BAD+=("$node")
warn "pvemini → $node ($ip) — NU răspunde (cablu? switch?)"
fi
done
NFS_BAD=()
for node in "${ISLAND_NFS_NODES[@]}"; do
src=$(nssh "$node" "findmnt -no SOURCE $ISLAND_NFS_MOUNT 2>/dev/null || true")
if [[ "$src" == *"$ISLAND_NFS_SERVER"* ]]; then
ok "$node — NFS $ISLAND_NFS_MOUNT montat de pe $ISLAND_NFS_SERVER"
elif [[ -n "${src// /}" ]]; then
# Prins pe 2026-08-29 la primul boot la rece: o linie rămasă în /etc/fstab
# monta storage-ul de pe IP-ul de producție ÎNAINTE ca pvestatd să apuce,
# deci backup-ul mergea tăcut pe rețeaua greșită, cu storage.cfg corect.
NFS_BAD+=("$node")
warn "$node — NFS montat de pe ALT server: $src (așteptat $ISLAND_NFS_SERVER)"
warn " verifică: ssh root@${NODE_IP[$node]} 'grep nfs /etc/fstab'"
else
# pvestatd remontează singur în câteva zeci de secunde dacă rețeaua e sus.
warn "$node — NFS $ISLAND_NFS_MOUNT NEMONTAT (pvestatd îl reia dacă insula urcă)"
fi
done
if (( ${#NFS_BAD[@]} )); then
warn "Backup-ul NFS trece pe altă rețea decât cea proiectată pe: ${NFS_BAD[*]}"
fi
if (( ${#ISLAND_BAD[@]} )); then
warn ""
warn "INSULA E JOS pe: ${ISLAND_BAD[*]}"
warn "Consecință: replicarea ZFS și storage-ul NFS backup-pvemini NU vor funcționa."
warn "Guest-urile pornesc și merg normal — problema e tăcută, se vede abia în pvesr status."
warn "Verifică: ssh root@<nod> \"ip -br a; dmesg | tail -20\" (dongle USB enumerat târziu?)"
confirm "Continui cu pornirea guest-urilor, cu replicarea jos?"
elif (( ${#NFS_BAD[@]} == 0 )); then
ok "insulă completă — replicarea și NFS-ul au calea liberă"
else
warn "insula e sus, dar NFS-ul nu o folosește — vezi mai sus"
fi
fi
# ------------------------------------------------- pas 4: ce trebuie pornit
step "Ce se pornește"
TO_START=""
if (( ! USE_ALL )) && [[ -f "$STATE_FILE" ]]; then
TO_START=$(grep -v '^#' "$STATE_FILE" | grep -v '^[[:space:]]*$' || true)
ok "citit din $STATE_FILE ($(grep -c . <<<"$TO_START") guest-uri)"
else
(( USE_ALL )) || warn "Nu există $STATE_FILE — folosesc lista implicită."
HA_SIDS=$(nssh pvemini "ha-manager config | grep -E '^(ct|vm):' | tr -d ' '" || true)
for node in "${!NODE_IP[@]}"; do
while read -r id _; do
[[ -n "$id" ]] || continue
h=noha; grep -qx "ct:$id" <<<"$HA_SIDS" && h=ha
TO_START+="ct $node $id stopped $h"$'\n'
done < <(nssh "$node" "pct list | awk 'NR>1 {print \$1, \$2}'")
while read -r id _; do
[[ -n "$id" ]] || continue
h=noha; grep -qx "vm:$id" <<<"$HA_SIDS" && h=ha
TO_START+="vm $node $id stopped $h"$'\n'
done < <(nssh "$node" "qm list | awk 'NR>1 {print \$1, \$3}'")
done
# Fără fișier de stare pornim doar ce e explicit în lista de ordine ȘI nu e
# în NEVER_AUTOSTART. Altfel am porni DR-ul și template-urile, care erau
# oprite intenționat.
FILTERED=""
for id in "${GUEST_START_ORDER[@]}"; do
if printf '%s\n' "${NEVER_AUTOSTART[@]}" | grep -qx "$id"; then
warn "$id sărit (NEVER_AUTOSTART) — pornește-l manual dacă chiar îl vrei"
continue
fi
line=$(awk -v id="$id" '$3==id' <<<"$TO_START") || true
[[ -n "$line" ]] && FILTERED+="$line"$'\n'
done
TO_START="$FILTERED"
fi
[[ -n "$TO_START" ]] || die "Nimic de pornit."
while read -r type node id status ha; do
[[ -n "${id:-}" ]] && printf ' %-3s %-9s %-4s %s\n' "$type" "$node" "$id" "$ha"
done <<<"$TO_START"
confirm "Se pornesc guest-urile de mai sus, în ordinea: ${GUEST_START_ORDER[*]}"
# ------------------------------------------------- pas 5: pornire guest-uri
step "Pornire guest-uri"
guest_status() {
local node=$1 type=$2 id=$3
if [[ "$type" == ct ]]; then
nssh "$node" "pct status $id 2>/dev/null | awk '{print \$2}'" || echo unknown
else
nssh "$node" "qm status $id 2>/dev/null | awk '{print \$2}'" || echo unknown
fi
}
start_guest() {
local type=$1 node=$2 id=$3 ha=$4
local sid="$type:$id" cmd
[[ "$type" == ct ]] && cmd=pct || cmd=qm
if [[ "$(guest_status "$node" "$type" "$id")" == "running" ]]; then
ok "$sid deja pornit"; return 0
fi
if [[ "$ha" == ha ]]; then
# Pentru resursele HA, `--state started` repune resursa sub controlul CRM.
run "$node" "ha-manager set $sid --state started"
else
run "$node" "$cmd start $id"
fi
(( DRY_RUN )) && return 0
local waited=0
while (( waited < GUEST_WAIT )); do
[[ "$(guest_status "$node" "$type" "$id")" == "running" ]] && { ok "$sid pornit (${waited}s)"; return 0; }
sleep 5; waited=$(( waited + 5 ))
done
warn "$sid nu a pornit în ${GUEST_WAIT}s — verifică manual"
return 1
}
wait_oracle() {
local node=$1
log "aștept ca instanța Oracle să accepte interogări..."
(( DRY_RUN )) && return 0
local waited=0
while (( waited < ORACLE_WAIT )); do
# `bash -lc`, NU `bash -c`: fără shell de login nu există sqlplus în PATH, iar
# așteptarea ar expira după $ORACLE_WAIT secunde chiar cu baza perfect deschisă.
#
# `timeout 30` e OBLIGATORIU, nu o precauție. Pe 2026-08-29, la pornirea de la
# rece, sqlplus lansat către o instanță încă în startup s-a blocat definitiv:
# docker exec a moștenit blocajul, ssh a așteptat, iar bucla n-a apucat să
# facă nici măcar o iterație. Scriptul a stat agățat 14 minute fără niciun
# mesaj, cu propriul prag de așteptare nefolosit. Sonda trebuie să răspundă
# mereu, fie și cu eșec.
if nssh "$node" "timeout 30 pct exec $ORACLE_CT -- docker exec $ORACLE_DOCKER bash -lc \"printf 'set pagesize 0 feedback off\nselect status from v\\\$instance;\nexit\n' | sqlplus -s / as sysdba\" 2>/dev/null | grep -q OPEN"; then
ok "Oracle deschis și interogabil (${waited}s)"
return 0
fi
printf '\r%s Oracle încă nu răspunde (%ds)...%s' "$c_dim" "$waited" "$c_off"
sleep 15; waited=$(( waited + 15 ))
done
printf '\r%*s\r' 50 ''
warn "Oracle nu a răspuns în ${ORACLE_WAIT}s. Continui, dar VERIFICĂ MANUAL."
warn " ssh root@${NODE_IP[$node]} \"pct exec $ORACLE_CT -- docker logs --tail 50 $ORACLE_DOCKER\""
return 1
}
FAILED=()
for id in "${GUEST_START_ORDER[@]}"; do
line=$(awk -v id="$id" '$3==id' <<<"$TO_START") || true
[[ -n "$line" ]] || continue
read -r type node vmid status ha <<<"$line"
log "pornesc $type:$vmid pe $node ($ha)"
start_guest "$type" "$node" "$vmid" "$ha" || FAILED+=("$type:$vmid")
# Oracle e blocant: nimic care depinde de el nu pornește până nu e deschis.
if [[ "$vmid" == "$ORACLE_CT" ]]; then
wait_oracle "$node" || true
fi
done
# orice a mai rămas în lista de pornit și nu era în ordinea definită
while read -r type node id status ha; do
[[ -n "${id:-}" ]] || continue
printf '%s\n' "${GUEST_START_ORDER[@]}" | grep -qx "$id" && continue
warn "guest neplanificat: $type:$id pe $node — îl pornesc"
start_guest "$type" "$node" "$id" "$ha" || FAILED+=("$type:$id")
done <<<"$TO_START"
# ------------------------------------------------ pas 6: restaurare cron-uri
step "Restaurare cron-uri de monitorizare"
for node in "${!NODE_IP[@]}"; do
if ! nssh "$node" "test -f $CRON_BACKUP"; then
warn "$node — nu există $CRON_BACKUP, sar peste"
continue
fi
run "$node" "crontab $CRON_BACKUP"
if (( ! DRY_RUN )); then
left=$(nssh "$node" "crontab -l | grep -c '^#MENTENANTA' || true")
if [[ "$left" == "0" ]]; then
ok "$node — cron-uri restaurate"
else
warn "$node — au rămas $left linii #MENTENANTA, verifică manual"
fi
fi
done
# ------------------------------------------------------- pas 7: verificare
step "Verificare finală"
if (( DRY_RUN )); then
warn "dry-run — sar peste verificare"
else
nssh pvemini "ha-manager status"
echo
nssh pvemini "pvesr status"
echo
BADREP=$(nssh pvemini "pvesr status | awk 'NR>1 && \$NF!=\"OK\" {print}'" || true)
[[ -z "$BADREP" ]] && ok "replicare: toate job-urile OK" \
|| { warn "job-uri de replicare cu probleme:"; printf '%s\n' "$BADREP"; }
fi
step "Cluster pornit"
if (( ${#FAILED[@]} )); then
warn "Guest-uri care NU au pornit: ${FAILED[*]}"
warn "Verifică-le manual înainte de a considera repornirea terminată."
else
ok "toate guest-urile au pornit"
fi
cat <<EOF
De verificat manual:
- datacenter.cfg are încă AMBELE linii: 'ha: shutdown_policy=freeze'
și 'migration: network=10.10.10.0/24,type=insecure'
- dacă lipsesc date recente, compară cu ultima replicare din pvesr status
- dacă a fost prima repornire după 2026-08-29: notează în
docs/switch-2.5g-dedicat-noduri.md dacă insula a urcat singură la boot
EOF