Files
ROMFASTSQL/proxmox/vm109-windows-dr/scripts/deploy.sh
Marius dabe5a34e3 feat(dr): script de deploy pentru scripturile DR + documentare procedura
Pana acum deploy-ul se facea manual (`cp ... /opt/scripts/`), nedocumentat
nicaieri - de unde si fisierele weekly-dr-test-proxmox.sh.bak-* ramase pe
pveelite. deploy.sh face aceiasi pasi, dar refuza situatiile care au costat
deja timp:

- suprascrierea unui script AFLAT IN EXECUTIE: bash citeste scriptul
  incremental de pe disc, iar suprascrierea unui test DR in curs (~18 min)
  corupe executia;
- CRLF: bash pe Linux raspunde "$'\r': command not found";
- transfer trunchiat: bash -n local si inca o data pe nod dupa copiere;
- flag-ul vm109-debug.flag lasat in urma dupa deploy pe guest (dezarmeaza
  permanent watchdog-ul, exact apararea care a prins incidentul 04-20).

Distributia pe noduri e asimetrica intentionat si e codificata explicit in
script: scripturile cluster-aware (test DR, patch window, watchdog) pe ambele
noduri fiindca urmaresc VM 109 dupa failover HA; ZFS/mirror doar pe pveelite
unde traieste datasetul; alertele si failover-ul doar pe pvemini, fiindca
reactioneaza la caderea pveelite - puse pe pveelite ar fi inutile si ar
inlesni exact split-brain-ul pe care incearca sa-l previna.

Verificarea "ruleaza acum" foloseste pgrep -f '[/]opt/scripts/x.sh'.
Parantezele nu sunt cosmetice: comanda trimisa prin ssh apare ea insasi in
lista de procese, deci forma fara paranteze se gaseste pe sine si raporteaza
orice script ca fiind in executie (verificat: exit 0 vs exit 1).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhQBTegE4PiMPPaapLHjkc
2026-08-08 17:19:03 +03:00

282 lines
11 KiB
Bash

#!/bin/bash
#
# deploy.sh — copiază scripturile DR din repo pe destinații, cu verificări.
#
# Până acum deploy-ul se făcea manual (`cp ... /opt/scripts/`), de unde și
# fișierele .bak-* rămase pe pveelite. Scriptul ăsta face aceiași pași, dar
# refuză situațiile care au costat deja timp:
#
# * suprascrierea unui script care RULEAZĂ — bash citește scriptul
# incremental de pe disc, iar suprascrierea în timpul rulării corupe
# execuția (blocant real: testul DR durează ~18 min);
# * deploy pe un singur nod — cron-ul e pe pveelite ȘI pvemini, iar garda
# cluster-aware face ca doar nodul care găzduiește VM 109 să execute;
# dacă lipsește de pe celălalt, după failover HA testul dispare tăcut;
# * fișiere cu CRLF — bash pe Linux răspunde "$'\r': command not found";
# * flag-ul de debug al watchdog-ului lăsat în urmă după deploy pe guest.
#
# Utilizare:
# ./deploy.sh # doar nodurile Proxmox (implicit)
# ./deploy.sh --guest # noduri + scripturile PowerShell pe VM 109
# ./deploy.sh --guest-only # doar VM 109
# ./deploy.sh --check # nu copiază nimic, doar raportează diferențele
#
# VM 109 e oprit între teste, deci --guest îl pornește, copiază și îl oprește
# la loc. Dacă VM 109 era deja pornit (test în curs), refuză.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PVEMINI="10.0.20.201"
PVEELITE="10.0.20.202"
NODES=("$PVEMINI" "$PVEELITE")
# Distribuția NU e simetrică, iar asimetria e intenționată — nu o "repara".
#
# BOTH scripturi cluster-aware, care urmăresc VM 109 oriunde ar fi;
# garda `[ -f /etc/pve/qemu-server/109.conf ]` face ca doar nodul
# gazdă să execute. Lipsa lor de pe un nod = testul dispare tăcut
# după un failover HA.
# PVEELITE scripturi legate de storage: acolo trăiește datasetul ZFS
# oracle-backups (sursa replicării și a mirror-ului).
# PVEMINI scripturi care reacționează la CĂDEREA pveelite. Puse pe pveelite
# ar fi inutile (nodul e jos) și periculoase: failover-ul rulat de pe
# nodul greșit e exact scenariul de split-brain pe care scripturile
# încearcă să-l evite.
SCRIPTS_BOTH=(
weekly-dr-test-proxmox.sh
vm109-patch-window.sh
vm109-watchdog.sh
)
SCRIPTS_PVEELITE=(
oracle-backup-monitor-proxmox.sh
zfs-replicate-oracle-backups.sh
nightly-backup-mirror.sh
)
SCRIPTS_PVEMINI=(
pveelite-down-alert.sh
failover-dr-to-pvemini.sh
failback-dr-to-pveelite.sh
)
# Pe ambele noduri: sursă pentru deploy-ul către guest, iar VM 109 poate fi
# găzduit de oricare dintre ele.
STAGED_PS_SCRIPTS=(check_servicing.ps1 install_updates.ps1)
# Lista de fișiere pentru un nod anume.
scripts_for_node() {
local node="$1"
printf '%s\n' "${SCRIPTS_BOTH[@]}" "${STAGED_PS_SCRIPTS[@]}"
if [ "$node" = "$PVEELITE" ]; then
printf '%s\n' "${SCRIPTS_PVEELITE[@]}"
elif [ "$node" = "$PVEMINI" ]; then
printf '%s\n' "${SCRIPTS_PVEMINI[@]}"
fi
}
GUEST_SCRIPTS=(check_servicing.ps1 install_updates.ps1 rman_restore_from_zero.ps1 cleanup_database.ps1)
DR_VM_ID=109
DR_VM_HOST="10.0.20.202" # nodul de pe care se face saltul către guest
DR_VM_IP="10.0.20.37"
DR_VM_PORT=22122
DR_VM_USER="romfast"
GUEST_DIR="D:/oracle/scripts"
DEBUG_FLAG="/var/run/vm109-debug.flag"
DO_NODES=true
DO_GUEST=false
CHECK_ONLY=false
case "${1:-}" in
--guest) DO_GUEST=true ;;
--guest-only) DO_GUEST=true; DO_NODES=false ;;
--check) CHECK_ONLY=true ;;
"") ;;
*) echo "Utilizare: $0 [--guest|--guest-only|--check]" >&2; exit 2 ;;
esac
red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
grn() { printf '\033[0;32m%s\033[0m\n' "$*"; }
ylw() { printf '\033[1;33m%s\033[0m\n' "$*"; }
fail() { red "EROARE: $*"; exit 1; }
# ---------------------------------------------------------------- verificări
# Un CR în fișier ajunge pe Linux ca "$'\r': command not found". .gitattributes
# ar trebui să prevină asta, dar un clone vechi sau o editare pe Windows o
# reintroduce — verificăm înainte să trimitem, nu după.
check_line_endings() {
local f="$1"
if grep -qU $'\r' "$f" 2>/dev/null; then
fail "$f are CRLF. Rulează: dos2unix '$f' (sau verifică .gitattributes)"
fi
}
check_syntax() {
local f="$1"
case "$f" in
*.sh) bash -n "$f" || fail "$f nu trece bash -n" ;;
esac
}
# ------------------------------------------------------------- noduri Proxmox
deploy_nodes() {
echo "=== Noduri Proxmox: ${NODES[*]} ==="
local f
for f in "${SCRIPTS_BOTH[@]}" "${SCRIPTS_PVEELITE[@]}" "${SCRIPTS_PVEMINI[@]}" "${STAGED_PS_SCRIPTS[@]}"; do
[ -f "$SCRIPT_DIR/$f" ] || { ylw " (lipsește din repo, sar peste: $f)"; continue; }
check_line_endings "$SCRIPT_DIR/$f"
check_syntax "$SCRIPT_DIR/$f"
done
grn " verificări locale OK (fără CRLF, bash -n trece)"
local node
for node in "${NODES[@]}"; do
echo "--- $node ---"
for f in $(scripts_for_node "$node"); do
[ -f "$SCRIPT_DIR/$f" ] || continue
# Nu atingem un script aflat în execuție.
#
# Parantezele din "[/]opt" nu sunt cosmetice: comanda trimisă prin
# ssh apare ea însăși în lista de procese, deci un `pgrep -f
# /opt/scripts/x.sh` s-ar găsi pe sine și ar raporta orice script ca
# fiind "în execuție". Regexul [/]opt se potrivește cu textul
# /opt/scripts/x.sh, dar NU cu propria linie de comandă, care
# conține literal "[/]opt/scripts/x.sh".
if ssh -o ConnectTimeout=10 "root@$node" "pgrep -f '[/]opt/scripts/$f' >/dev/null 2>&1"; then
red " RULEAZĂ ACUM, nu suprascriu: $f"
red " (reia deploy-ul după ce se termină; verifică: ssh root@$node \"pgrep -af $f\")"
continue
fi
# Diferă de ce e pe nod?
local local_sum remote_sum
local_sum=$(md5sum "$SCRIPT_DIR/$f" | awk '{print $1}')
remote_sum=$(ssh -o ConnectTimeout=10 "root@$node" "md5sum /opt/scripts/$f 2>/dev/null | awk '{print \$1}'" || echo "")
if [ "$local_sum" = "$remote_sum" ]; then
echo " = $f (identic)"
continue
fi
if [ "$CHECK_ONLY" = true ]; then
if [ -z "$remote_sum" ]; then
ylw " ~ $f (LIPSEȘTE pe nod)"
else
ylw " ~ $f (DIFERĂ)"
fi
continue
fi
scp -q "$SCRIPT_DIR/$f" "root@$node:/opt/scripts/$f"
case "$f" in
*.sh) ssh -o ConnectTimeout=10 "root@$node" "chmod +x /opt/scripts/$f && bash -n /opt/scripts/$f" \
|| fail "$f nu trece bash -n PE $node (transfer trunchiat?)" ;;
esac
grn " + $f"
done
done
}
# ------------------------------------------------------------------ guest 109
deploy_guest() {
echo "=== VM $DR_VM_ID (guest Windows) ==="
local vm_was_running=false
local flag_set_by_us=false
if ssh -o ConnectTimeout=10 "root@$DR_VM_HOST" "qm status $DR_VM_ID | grep -q running"; then
vm_was_running=true
ylw " VM $DR_VM_ID rulează deja."
ylw " Dacă e testul DR în curs, oprește deploy-ul (Ctrl-C) — altfel continuă."
sleep 5
fi
if [ "$CHECK_ONLY" = true ]; then
ylw " --check: nu pornesc VM $DR_VM_ID"
return 0
fi
# Curățenia trebuie să ruleze și dacă pică ceva la mijloc.
cleanup_guest() {
if [ "$flag_set_by_us" = true ]; then
ssh -o ConnectTimeout=10 "root@$DR_VM_HOST" "rm -f $DEBUG_FLAG" 2>/dev/null || true
echo " flag debug watchdog șters"
fi
if [ "$vm_was_running" = false ]; then
ssh -o ConnectTimeout=10 "root@$DR_VM_HOST" "qm stop $DR_VM_ID" 2>/dev/null || true
echo " VM $DR_VM_ID oprit la loc"
fi
}
trap cleanup_guest EXIT
if [ "$vm_was_running" = false ]; then
# Watchdog-ul oprește VM 109 dacă rulează în afara ferestrei de test.
ssh -o ConnectTimeout=10 "root@$DR_VM_HOST" "touch $DEBUG_FLAG"
flag_set_by_us=true
echo " pornesc VM $DR_VM_ID..."
ssh -o ConnectTimeout=10 "root@$DR_VM_HOST" "qm start $DR_VM_ID"
fi
echo " aștept SSH pe $DR_VM_IP:$DR_VM_PORT..."
local waited=0
while [ $waited -lt 300 ]; do
if ssh -o ConnectTimeout=15 "root@$DR_VM_HOST" \
"ssh -p $DR_VM_PORT -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o BatchMode=yes $DR_VM_USER@$DR_VM_IP 'echo ok'" >/dev/null 2>&1; then
grn " guest disponibil după ${waited}s"
break
fi
sleep 10
waited=$((waited + 10))
done
[ $waited -lt 300 ] || fail "VM $DR_VM_ID nu a răspuns în 300s"
# Stația nu are rută directă la guest — pveelite e jump host, deci copiem în doi pași.
local f present=()
for f in "${GUEST_SCRIPTS[@]}"; do
if [ -f "$SCRIPT_DIR/$f" ]; then
present+=("$SCRIPT_DIR/$f")
else
ylw " (lipsește din repo, sar peste: $f)"
fi
done
[ ${#present[@]} -gt 0 ] || fail "niciun script de copiat pe guest"
scp -q "${present[@]}" "root@$DR_VM_HOST:/tmp/"
local names=""
for f in "${present[@]}"; do names="$names /tmp/$(basename "$f")"; done
ssh -o ConnectTimeout=20 "root@$DR_VM_HOST" \
"scp -P $DR_VM_PORT -o StrictHostKeyChecking=no $names $DR_VM_USER@$DR_VM_IP:'$GUEST_DIR/'" \
|| fail "copierea pe guest a eșuat"
ssh -o ConnectTimeout=20 "root@$DR_VM_HOST" "rm -f $names" 2>/dev/null || true
for f in "${present[@]}"; do grn " + $(basename "$f")"; done
# Verificare funcțională: guard-ul trebuie să răspundă, altfel testul DR
# îl va sări tăcut (fail-open) și rămânem fără plasa de siguranță.
local state
state=$(ssh -o ConnectTimeout=20 "root@$DR_VM_HOST" \
"ssh -p $DR_VM_PORT -o StrictHostKeyChecking=no $DR_VM_USER@$DR_VM_IP \
'powershell -ExecutionPolicy Bypass -File D:\\oracle\\scripts\\check_servicing.ps1'" 2>/dev/null | tr -d '\r' || echo "")
if echo "$state" | grep -q "^STATE="; then
grn " check_servicing.ps1 răspunde: $state"
else
red " ATENȚIE: check_servicing.ps1 nu răspunde corect (guard-ul va fi sărit)"
fi
}
[ "$DO_NODES" = true ] && deploy_nodes
[ "$DO_GUEST" = true ] && deploy_guest
echo
if [ "$CHECK_ONLY" = true ]; then
grn "Verificare terminată (nu s-a copiat nimic)."
else
grn "Deploy terminat."
echo "Cron-ul NU e modificat de scriptul ăsta. Verifică pe fiecare nod:"
echo " ssh root@10.0.20.202 'crontab -l | grep -E \"dr-test|patch-window|watchdog\"'"
fi