#!/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. # # 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ă # 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)" STATE_FILE="${CLUSTER_STATE_FILE:-$SCRIPT_DIR/.cluster-state.txt}" ASSUME_YES=0 DRY_RUN=0 USE_ALL=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 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 ;; -h|--help) sed -n '2,22p' "$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." 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@ \"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 <