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
This commit is contained in:
@@ -327,6 +327,51 @@ D:\oracle\scripts\cleanup_database.cmd
|
|||||||
ssh root@10.0.20.202 "qm stop 109"
|
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
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
### ⚡ Simptome frecvente — unde te uiți întâi
|
### ⚡ Simptome frecvente — unde te uiți întâi
|
||||||
|
|||||||
281
proxmox/vm109-windows-dr/scripts/deploy.sh
Normal file
281
proxmox/vm109-windows-dr/scripts/deploy.sh
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user