From dabe5a34e3f4a8b59de7d40b5616ff82a379d4db Mon Sep 17 00:00:00 2001 From: Marius Date: Sat, 8 Aug 2026 17:19:03 +0300 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01BhQBTegE4PiMPPaapLHjkc --- proxmox/vm109-windows-dr/README.md | 45 ++++ proxmox/vm109-windows-dr/scripts/deploy.sh | 281 +++++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 proxmox/vm109-windows-dr/scripts/deploy.sh diff --git a/proxmox/vm109-windows-dr/README.md b/proxmox/vm109-windows-dr/README.md index 0b00bfd..3eeb8b2 100644 --- a/proxmox/vm109-windows-dr/README.md +++ b/proxmox/vm109-windows-dr/README.md @@ -327,6 +327,51 @@ D:\oracle\scripts\cleanup_database.cmd ssh root@10.0.20.202 "qm stop 109" ``` +## 🚀 Deploy + +Repo-ul e sursa; scripturile se copiază pe destinații cu `scripts/deploy.sh`. Până în 2026-08-08 deploy-ul se făcea manual (`cp ... /opt/scripts/`), de unde și fișierele `.bak-*` rămase pe pveelite. + +```bash +cd proxmox/vm109-windows-dr/scripts + +./deploy.sh --check # ce diferă, fără să copieze nimic +./deploy.sh # doar nodurile Proxmox +./deploy.sh --guest # noduri + scripturile PowerShell pe VM 109 +./deploy.sh --guest-only # doar VM 109 +``` + +### Distribuția pe noduri — asimetrică intenționat + +| Script | pvemini (.201) | pveelite (.202) | De ce | +|---|:---:|:---:|---| +| `weekly-dr-test-proxmox.sh` | ✅ | ✅ | cluster-aware, urmărește VM 109 după failover HA | +| `vm109-patch-window.sh` | ✅ | ✅ | idem | +| `vm109-watchdog.sh` | ✅ | ✅ | idem | +| `check_servicing.ps1`, `install_updates.ps1` | ✅ | ✅ | sursă pentru deploy-ul către guest | +| `oracle-backup-monitor-proxmox.sh` | — | ✅ | acolo e datasetul ZFS `oracle-backups` | +| `zfs-replicate-oracle-backups.sh` | — | ✅ | sursa replicării | +| `nightly-backup-mirror.sh` | — | ✅ | idem | +| `pveelite-down-alert.sh` | ✅ | — | reacționează la **căderea** pveelite | +| `failover-dr-to-pvemini.sh` | ✅ | — | rulat de operator **pe pvemini** | +| `failback-dr-to-pveelite.sh` | ✅ | — | idem | + +Nu „repara" asimetria: un script de failover prezent pe nodul greșit e exact scenariul de split-brain pe care scripturile încearcă să-l prevină. + +### Ce verifică deploy.sh înainte să copieze + +- **script în execuție** — refuză suprascrierea; bash citește scriptul incremental de pe disc, iar suprascrierea unui test DR în curs (~18 min) corupe execuția; +- **CRLF** — `bash` pe Linux răspunde `$'\r': command not found`; +- **`bash -n`** local și încă o dată pe nod după copiere (prinde transferurile trunchiate); +- **md5** — sare peste fișierele identice, ca output-ul să arate doar ce s-a schimbat. + +Pentru guest: VM 109 e oprit între teste, deci `--guest` îl pornește, setează `vm109-debug.flag` (altfel watchdog-ul îl oprește la 60 min), copiază prin pveelite ca jump host, verifică funcțional `check_servicing.ps1` și oprește VM-ul la loc. Dacă VM 109 rula deja, avertizează și lasă 5s pentru Ctrl-C. + +**Cron-ul nu e atins de deploy.** După adăugarea unui script nou: + +```bash +ssh root@10.0.20.202 'crontab -l | grep -E "dr-test|patch-window|watchdog"' +``` + ## 🐛 Troubleshooting ### ⚡ Simptome frecvente — unde te uiți întâi diff --git a/proxmox/vm109-windows-dr/scripts/deploy.sh b/proxmox/vm109-windows-dr/scripts/deploy.sh new file mode 100644 index 0000000..03861e1 --- /dev/null +++ b/proxmox/vm109-windows-dr/scripts/deploy.sh @@ -0,0 +1,281 @@ +#!/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