diff --git a/.gitignore b/.gitignore index 940bde1..6cb6f63 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ docs/handoff_*.md # Stare runtime salvata de cluster-shutdown.sh (nu e documentatie) proxmox/cluster/scripts/.cluster-state.txt + +# Configurarea reala a monitorului UPS - contine parola SMTP, traieste doar pe server +scripts/ups-monitor/config.json diff --git a/CLAUDE.md b/CLAUDE.md index d075629..1583288 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,9 +29,10 @@ system_instructions/ # SQL migration guidelines and knowledge base ├── system_prompt.md # SQL migration rules - READ BEFORE WRITING SQL └── knowledge_base/ # Example scripts and PACK_MIGRARE.pck -scripts/ # Python utilities +scripts/ # Utilities ├── generare_proces_verbal.py # Generate assembly minutes (DOCX) -└── verificare_voturi_AG.py # Verify assembly votes +├── verificare_voturi_AG.py # Verify assembly votes +└── ups-monitor/ # UPS monitoring for production Oracle server 10.0.20.36 input/ # Oracle DMP files for import ``` @@ -52,6 +53,7 @@ input/ # Oracle DMP files for import - **Kit de instalare pentru client (DMP-uri șablon + scripturi, gata de dus la client)**: `proxmox/lxc108-oracle/scripts/build-client-kit.ps1` - **VM 302 test environment for ROA setup**: `proxmox/vm302-oracle-test/README.md` - **Diagnostic spațiu Oracle la clienți (job zilnic + emailuri)**: `docs/diagnostic-spatiu-clienti.md` +- **Monitorizare UPS pe serverul Oracle de producție 10.0.20.36 (înlocuiește ViewPower)**: `docs/ups-server36-monitorizare.md` - **Acces SSH la clienți cu chei publice (angajați noi)**: `docs/acces-ssh-chei-angajati.md` - **Acces administrativ la server client prin Tailscale + SSH (fără forward pe router)**: `docs/acces-client-tailscale-ssh.md` - **Cazuri clienți (depanare DB)**: `proxmox/lxc108-oracle/clienti/README.md` diff --git a/docs/ups-server36-monitorizare.md b/docs/ups-server36-monitorizare.md new file mode 100644 index 0000000..731139c --- /dev/null +++ b/docs/ups-server36-monitorizare.md @@ -0,0 +1,266 @@ +# Monitorizare UPS pe serverul Oracle de producție (10.0.20.36) + +**Server:** `ROA-CARAPETRU2` / 10.0.20.36 — Windows 11 Pro, Oracle 19c SE2, instanța `ROA`. +Acces: `ssh -p 22122 Administrator@10.0.20.36`. + +**Pe scurt:** ViewPower nu a putut niciodată dialoga cu acest UPS și a fost dezactivat pe +10 august 2026, lăsând serverul fără nicio alertare timp de 17 zile. UPS-ul, în schimb, +comunică perfect cu Windows pe canalul HID nativ. Monitorizarea nouă folosește exact acel +canal. ViewPower e închis și nu mai pornește. + +Se citește împreună cu [`incident-2026-08-10-server36-standby.md`](incident-2026-08-10-server36-standby.md) +— cele două se leagă în seara de 10 august. + +--- + +## 1. Diagnosticul: ce era stricat, de fapt + +### UPS-ul comunică — și a comunicat tot timpul + +| Verificare | Rezultat (27.08.2026) | +|---|---| +| `USB\VID_0665&PID_5161\HID_UPS` | Status **OK**, enumerat corect | +| `HID\...&MI_01`, driver `HidBatt` | „HID UPS Battery", Status **OK** | +| `Win32_Battery` | `HID UPS`, Status `OK`, BatteryStatus `2` (pe rețea), încărcare **100%** | +| Kernel-Power ID **105** (schimbare sursă) | perechi AC↔baterie reale: 10 iun, 13 mai, 5 mai, 25 feb 2026 | + +Perechile de evenimente 105 sunt dovada care închide discuția: la fiecare pană reală Windows +a *văzut* trecerea pe baterie și revenirea. Ultima pană: **10 iunie 2026**. Din 10 august +(dezactivarea ViewPower) până azi nu a mai fost niciuna, deci nu s-a pierdut niciun eveniment +— noroc, nu proiectare. + +### ViewPower nu vorbește limba acestui UPS + +`C:\ViewPower\log\log4j.log` acoperă **6 aug 15:40:37 → 10 aug 22:34:11** (patru treceri peste +miezul nopții, numărate în fișier). Din 5787 de linii, **5777 sunt `QPI return(NAK`**. Zero +citiri reușite: nicio tensiune, nicio stare de baterie, niciun eveniment electric. + +`QPI` este comanda de identificare a protocolului Voltronic. UPS-ul răspunde `(NAK` — +„comandă nesuportată" — deci ViewPower rămâne blocat în bucla de identificare și nu ajunge +niciodată să interogheze starea. + +Nu e o problemă de port: `GlobalConfig.portName=USB1F0775B7` din `config\ups.properties` +corespunde exact interfeței `HID\VID_0665&PID_5161&MI_00\8&1F0775B7&0&0000`. ViewPower e +legat de dispozitivul corect; **setul de comenzi e greșit**. UPS-ul vorbește HID Power Device +standard — de aceea îl citește Windows fără niciun driver suplimentar — nu dialectul text pe +care îl trimite ViewPower. Nu există setare care să repare asta. + +### Cronologia serii de 10 august + +``` +22:24:12 upsTomcat pornit (o ultimă încercare de repornire) +22:24:31 ups.properties rescris +22:34:11 ultimul QPI return(NAK +22:34:44 System 7040: upsMonitor auto start → disabled +22:34:46 System 7040: upsTomcat demand start → disabled +``` + +Cineva a încercat să repare, nu a reușit și a dezactivat serviciile. Din acel moment: **zero +monitorizare**. Tray-ul care se deschidea și dădea `start ups monitor service error!` încerca, +de fapt, să pornească un serviciu pus pe `Disabled` (registry `Start=4`). + +### Ce nu exista oricum, chiar și cu ViewPower funcțional + +- **Alertare: niciuna.** Logul arată `---------emailReceivers.length()-----0`, iar + `SmsInfo.mobileNums` e gol. Nimeni nu ar fi aflat de o pană nici dacă softul mergea. +- **Oprire curată a Oracle: niciuna.** `Shutdownconfigure.excuteProgram` e gol — nu rula + niciun script înainte de shutdown. +- Regula proprie „30 min pe baterie → oprire" (`batModeShutdown=true`, + `batModeShutdownTime=30`) era inactivă odată cu serviciile. +- Vârsta acumulatorilor neurmărită (`GlobalConfig.batteryLifetime=0`). + +### Plasa care exista, totuși + +Planul de alimentare Windows, pe canalul HID care funcționează: + +| Setare | Valoare | +|---|---| +| Critical battery level | **20 %** | +| Critical battery action | **Shut down** (și pe AC, și pe DC) | +| Low battery level / action | 40 % / *Do nothing* | +| Reserve battery level | 25 % | + +Deci serverul s-ar fi oprit singur la 20 %, dar brutal: fără avertisment, fără `shutdown +immediate` pe Oracle, fără ca cineva să afle. + +--- + +## 2. Ce s-a pus în loc + +Un singur script PowerShell care citește **`GetSystemPowerStatus`** — exact API-ul pe care îl +folosește Windows pentru propria acțiune de „critical battery". Consecința practică: dacă +oprirea automată a Windows-ului ar funcționa, funcționează și scriptul; nu există un al doilea +canal care să se strice separat. + +| Fișier în repo | Rol | +|---|---| +| `scripts/ups-monitor/ups-monitor.ps1` | monitorul propriu-zis (buclă + moduri de test) | +| `scripts/ups-monitor/install-ups-monitor.ps1` | instalare: copiere, `config.json`, ACL, task programat | +| `scripts/ups-monitor/test-comms-loss.ps1` | test: dezactivează temporar bateria HID și verifică alerta de comunicație | +| `scripts/ups-monitor/config.example.json` | șablon; **fișierul real nu se comite** (conține parola SMTP) | + +Pe server: + +``` +C:\ROMFAST\ups-monitor\ups-monitor.ps1 +C:\ROMFAST\ups-monitor\config.json <- parola SMTP; ACL: doar SYSTEM + Administratori +C:\ROMFAST\ups-monitor\state.json <- starea între cicluri +C:\ROMFAST\ups-monitor\log\ups-monitor-YYYY-MM.log +C:\ROMFAST\_deploy\ <- punctul de livrare (de aici rulează instalarea) +``` + +Task programat **`ROMFAST UPS Monitor`**, rulează ca `NT AUTHORITY\SYSTEM` (cont deja membru +`ORA_DBA` pe acest server, deci `sqlplus / as sysdba` merge fără parolă). + +### Ce face + +| Eveniment | Acțiune | +|---|---| +| Trecere pe baterie | email imediat: **„PANĂ DE CURENT"** | +| Revenirea curentului | email cu durata cât s-a stat pe baterie | +| **UPS-ul nu mai e văzut de Windows** | email de alertă, repetat la 24 h cât ține defectul | +| Comunicația revine | email de confirmare | +| Prag de oprire atins | email, apoi oprire curată | + +### Pragurile de oprire + +``` +10 minute pe baterie SAU încărcare ≤ 35 % + (Windows rămâne plasa de siguranță, la 20 %) +``` + +Se declanșează doar după **3 citiri consecutive** care confirmă starea pe baterie — o citire +aberantă nu poate opri serverul. Iar dacă UPS-ul **nu comunică**, scriptul nu ia nicio decizie +de oprire; alertează și atât. + +### Secvența de oprire + +1. email (pleacă **primul** — în pană de curent poate cădea și switch-ul în orice moment) +2. `Stop-Service OracleOraDB19Home1TNSListener` — nu mai intră conexiuni noi +3. `sqlplus / as sysdba` → `shutdown immediate`, cu timeout 180 s +4. `Stop-Service OracleServiceROA` — plasă de siguranță; serviciul e el însuși configurat + `ORA_ROA_SHUTDOWNTYPE=immediate`, `ORA_ROA_SHUTDOWN_TIMEOUT=90`, deci și oprirea lui e curată +5. `shutdown /s /f /t 30` + +> La revenirea curentului serverul **nu pornește singur** decât dacă e activat +> *Restore on AC power loss* în BIOS. De verificat separat, la prima intervenție fizică. + +--- + +## 3. Operare + +```powershell +# starea curentă: UPS, Oracle, task, ultimele linii de log +powershell -ExecutionPolicy Bypass -File C:\ROMFAST\ups-monitor\ups-monitor.ps1 -Mode Status + +# verifică alertarea pe email +powershell -ExecutionPolicy Bypass -File C:\ROMFAST\ups-monitor\ups-monitor.ps1 -Mode TestMail + +# parcurge secvența de oprire în gol — nu oprește nimic +powershell -ExecutionPolicy Bypass -File C:\ROMFAST\ups-monitor\ups-monitor.ps1 -Mode TestShutdown +``` + +### Ce s-a validat la punerea în funcțiune (27.08.2026) + +| Test | Rezultat | +|---|---| +| Citirea stării | `pe rețea`, 100 %, `Win32_Battery: HID UPS, Status=OK` | +| `-Mode TestMail` | email primit — `mail.romfast.ro:587`, STARTTLS, `ups@romfast.ro` | +| `-Mode TestShutdown` (dry-run) | secvența parcursă integral; Oracle a rămas `Running` | +| Pierderea comunicației (`test-comms-loss.ps1`) | dispozitiv oprit 15:24:44 → alertă **15:26:44** (exact 8 cicluri × 15 s) + email; reactivare → „comunicația a revenit" + email; `Win32_Battery` înapoi la 1 instanță | +| Taskul rulează ca SYSTEM | proces `ups-monitor.ps1 -Mode Run`, `state.json` actualizat la fiecare ciclu | + +**Netestat, pentru că cere pană reală:** trecerea pe baterie, pragurile de oprire și oprirea +efectivă a Oracle. Ramura de decizie e comună cu cea testată în dry-run, dar declanșatorul nu +a fost exercitat pe hardware. Vezi punctul 1 din secțiunea 6. + +**Dezarmare de urgență** (monitorul rămâne pornit, dar nu mai ia nicio decizie): + +```powershell +New-Item -ItemType File 'C:\ROMFAST\ups-monitor\DEZACTIVAT' +``` + +Ștergerea fișierului îl rearmează, fără repornire. Există și `"enabled": false` în +`config.json`, dar acela cere repornirea taskului. + +**Reinstalare / schimbare praguri** (de pe stația de administrare): + +```powershell +scp -P 22122 ups-monitor.ps1 install-ups-monitor.ps1 Administrator@10.0.20.36:'C:/ROMFAST/_deploy/' +ssh -p 22122 Administrator@10.0.20.36 "powershell -ExecutionPolicy Bypass -File C:\ROMFAST\_deploy\install-ups-monitor.ps1 -SmtpPassword ... -ShutdownAfterMinutesOnBattery 10 -ShutdownAtBatteryPercent 35" +``` + +**Emailuri:** de la `ups@romfast.ro` către `marius.mutu@romfast.ro`, prin +`mail.romfast.ro:587` (STARTTLS). Același cont SMTP pe care îl folosesc și cele trei noduri +Proxmox — credențialele sunt în `/etc/postfix/sasl_passwd` pe noduri. + +--- + +## 4. Ce s-a întâmplat cu ViewPower + +- procesele `ViewPower.exe`, `upsTray.exe`, `javaw.exe` — **oprite**; +- serviciile `upsMonitor` și `upsTomcat` — rămân **Disabled** (erau deja, din 10 august); +- **nu există autostart** — nici în `Run` (HKLM/HKCU), nici în folderele Startup; procesele + văzute pe 27 august fuseseră pornite manual; +- instalarea din `C:\ViewPower` a fost **lăsată pe disc**, ca referință pentru loguri și + configurație. + +Efect secundar util: nu mai ascultă nimic pe **15178** și **8005** — un Tomcat 8.5.51 cu +OpenJDK 11.0.7 (2020–2021) nu mai stă pornit degeaba pe un server de producție. + +Dezinstalarea completă (`C:\ViewPower\Uninstaller.exe`) e opțională. Înainte de ea merită +salvat `C:\ViewPower\datas` — baza Derby de ~71 MB cu istoricul softului. + +--- + +## 5. Capcane plătite (ca să nu se redescopere) + +**Task-urile programate sunt oprite de Windows exact când serverul trece pe baterie.** E +comportamentul implicit. Fără `-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries` la +`New-ScheduledTaskSettingsSet`, monitorul ar fi fost ucis fix în secunda în care avea treabă. +Asta e greșeala care ar fi trecut neobservată până la prima pană reală. + +**`$PSScriptRoot` este gol în valoarea implicită a unui parametru.** La legarea parametrilor +încă nu e populat, deci `[string]$ConfigPath = (Join-Path $PSScriptRoot 'config.json')` crapă +cu *„Cannot bind argument to parameter 'Path' because it is an empty string"*. Se rezolvă în +corpul scriptului. + +**`[TimeSpan]::MaxValue` pentru `-RepetitionDuration` e respins**, deși e documentat ca „la +nesfârșit": *„The task XML contains a value which is incorrectly formatted or out of range. +Duration: P99999999DT23H59M59S"*. Se omite parametrul — asta chiar înseamnă la nesfârșit. + +**PowerShell 5.1 nu acceptă `if`/`try` ca expresie** într-un literal de hashtable +(`@{ x = if (...) {...} }`) sau la atribuire din `try`. Scriptul e scris în consecință; se +verifică înainte de livrare cu: + +```powershell +$e=$null; [System.Management.Automation.PSParser]::Tokenize((Get-Content x.ps1 -Raw), [ref]$e); $e +``` + +**Ghilimelele simple supraviețuiesc prin `ssh` → `powershell -File`.** `-SmtpPassword +'#Ups2020#'` a ajuns în `config.json` ca `'#Ups2020#'`, cu tot cu apostrofuri, iar serverul de +mail a răspuns *„SMTP AUTH is required for message submission on port 587"* — adică nu s-a +autentificat deloc. Pentru orice valoare cu caractere speciale: se trimite un `.ps1` cu `scp` +și se rulează acolo, nu se construiește comanda din ghilimele. + +**Fișierul `.ps1` e intenționat fără diacritice.** PowerShell 5.1 citește un `.ps1` fără BOM +ca ANSI; diacriticele din literalii de șir ar ajunge corupte în emailuri. Documentația (`.md`) +nu are constrângerea asta. + +--- + +## 6. Rămâne de făcut + +1. **Testul de autonomie reală.** Pragurile de 10 min / 35 % sunt alese conservator, nu + măsurate. Nu se știe cât ține UPS-ul sub sarcina actuală, deci nu se știe dacă cele 10 + minute lasă destul timp pentru `shutdown immediate` + oprirea Windows-ului. Se măsoară + scoțând UPS-ul din priză, cu monitorul **dezarmat** (`DEZACTIVAT`) și cu cineva care + urmărește `-Mode Status`. Până atunci, pragurile sunt o presupunere rezonabilă, nu o + garanție. +2. **Vârsta acumulatorilor.** Necunoscută. Pe cluster există deja criteriul de vârstă în + testul lunar (`proxmox/cluster/ups/`); aici nu există nimic echivalent. +3. **BIOS: *Restore on AC power loss*.** De verificat la prima intervenție fizică — altfel + după o pană lungă serverul rămâne oprit până vine cineva să apese butonul. +4. **Legătura cu incidentul din 10 august:** stările de somn (S3) sunt încă disponibile pe + acest server. O oprire declanșată de UPS ajunge la `shutdown /s`, nu la sleep, deci nu e + afectată — dar problema de fond rămâne deschisă în documentul incidentului. diff --git a/scripts/ups-monitor/README.md b/scripts/ups-monitor/README.md new file mode 100644 index 0000000..7b16df3 --- /dev/null +++ b/scripts/ups-monitor/README.md @@ -0,0 +1,30 @@ +# ups-monitor — monitorizare UPS pentru serverul Oracle de producție (10.0.20.36) + +Înlocuiește ViewPower, care nu putea dialoga cu acest UPS. Citește starea alimentării prin +`GetSystemPowerStatus` (canalul HID nativ Windows), alertează pe email și oprește curat Oracle +înainte de epuizarea bateriei. + +**Documentația completă — diagnostic, praguri, operare, capcane:** +[`docs/ups-server36-monitorizare.md`](../../docs/ups-server36-monitorizare.md) + +| Fișier | Rol | +|---|---| +| `ups-monitor.ps1` | monitorul: buclă + modurile `Status`, `TestMail`, `TestShutdown`, `Once` | +| `install-ups-monitor.ps1` | instalare pe server: copiere, `config.json`, ACL, task programat | +| `config.example.json` | șablon de configurare | + +`config.json` real trăiește **doar pe server** (`C:\ROMFAST\ups-monitor\`), cu ACL restrâns — +conține parola SMTP și nu se comite. + +## Livrare + +```powershell +scp -P 22122 ups-monitor.ps1 install-ups-monitor.ps1 Administrator@10.0.20.36:'C:/ROMFAST/_deploy/' +ssh -p 22122 Administrator@10.0.20.36 "powershell -ExecutionPolicy Bypass -File C:\ROMFAST\_deploy\install-ups-monitor.ps1 -SmtpPassword ..." +``` + +Înainte de livrare, verificarea de sintaxă pentru PowerShell 5.1 (ținta de pe server): + +```powershell +$e=$null; [System.Management.Automation.PSParser]::Tokenize((Get-Content ups-monitor.ps1 -Raw), [ref]$e); $e +``` diff --git a/scripts/ups-monitor/config.example.json b/scripts/ups-monitor/config.example.json new file mode 100644 index 0000000..f3ff7d5 --- /dev/null +++ b/scripts/ups-monitor/config.example.json @@ -0,0 +1,35 @@ +{ + "_comentariu": "Sablon. Fisierul real se genereaza de install-ups-monitor.ps1 direct pe server, in C:\\ROMFAST\\ups-monitor\\config.json, si NU se comiteaza - contine parola SMTP.", + + "enabled": true, + "pollSeconds": 15, + + "shutdownAfterMinutesOnBattery": 10, + "shutdownAtBatteryPercent": 35, + "shutdownAtRuntimeSeconds": 0, + + "confirmPolls": 3, + "commsLostPolls": 8, + "commsAlertRepeatHours": 24, + + "logPath": "C:\\ROMFAST\\ups-monitor\\log", + "stateFile": "C:\\ROMFAST\\ups-monitor\\state.json", + + "mailTo": [ "marius.mutu@romfast.ro" ], + + "smtp": { + "server": "mail.romfast.ro", + "port": 587, + "user": "ups@romfast.ro", + "password": "PAROLA_AICI", + "from": "ups@romfast.ro" + }, + + "oracle": { + "home": "C:\\Users\\Administrator\\Downloads\\WINDOWS.X64_193000_db_home", + "sid": "ROA", + "service": "OracleServiceROA", + "listenerService": "OracleOraDB19Home1TNSListener", + "shutdownTimeoutSeconds": 180 + } +} diff --git a/scripts/ups-monitor/install-ups-monitor.ps1 b/scripts/ups-monitor/install-ups-monitor.ps1 new file mode 100644 index 0000000..dc7dacc --- /dev/null +++ b/scripts/ups-monitor/install-ups-monitor.ps1 @@ -0,0 +1,197 @@ +<# +.SYNOPSIS + Instaleaza monitorizarea UPS pe serverul Oracle de productie: copiaza scriptul, + scrie config.json, restrictioneaza drepturile si inregistreaza taskul programat. + +.DESCRIPTION + Se ruleaza PE SERVER, dintr-un PowerShell ridicat (Administrator). + + Taskul ruleaza ca NT AUTHORITY\SYSTEM - cont care e deja membru ORA_DBA pe acest + server, deci "sqlplus / as sysdba" merge fara parola. + + Doua detalii care fac diferenta si sunt usor de ratat: + - AllowStartIfOnBatteries + DontStopIfGoingOnBatteries: fara ele, Windows + opreste taskul exact cand serverul trece pe baterie, adica exact cand ai + nevoie de el; + - al doilea declansator, care repeta la 10 minute cu MultipleInstances=IgnoreNew: + daca procesul moare, revine singur; daca traieste, pornirea e ignorata. + +.PARAMETER SmtpPassword + Parola contului SMTP. Ajunge in config.json, care primeste ACL doar pentru + SYSTEM si Administratori. NU se comiteaza in git. + +.PARAMETER Uninstall + Sterge taskul programat. Nu sterge directorul, logurile sau configuratia. + +.EXAMPLE + powershell -ExecutionPolicy Bypass -File .\install-ups-monitor.ps1 -SmtpPassword '...' +#> +[CmdletBinding()] +param( + [string]$InstallDir = 'C:\ROMFAST\ups-monitor', + + [string]$SmtpServer = 'mail.romfast.ro', + [int]$SmtpPort = 587, + [string]$SmtpUser = 'ups@romfast.ro', + [string]$SmtpFrom = 'ups@romfast.ro', + [string]$SmtpPassword, + + [string[]]$MailTo = @('marius.mutu@romfast.ro'), + + [int]$PollSeconds = 15, + [int]$ShutdownAfterMinutesOnBattery = 10, + [int]$ShutdownAtBatteryPercent = 35, + + [string]$OracleHome = 'C:\Users\Administrator\Downloads\WINDOWS.X64_193000_db_home', + [string]$OracleSid = 'ROA', + [string]$OracleService = 'OracleServiceROA', + [string]$OracleListenerService = 'OracleOraDB19Home1TNSListener', + [int]$OracleShutdownTimeoutSeconds = 180, + + [switch]$Uninstall +) + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' + +$TaskName = 'ROMFAST UPS Monitor' + +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$principal = New-Object Security.Principal.WindowsPrincipal($identity) +if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw 'Scriptul trebuie rulat dintr-un PowerShell ridicat (Administrator).' +} + +if ($Uninstall) { + if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false + Write-Host "Task '$TaskName' sters. Directorul $InstallDir a ramas pe loc." + } else { + Write-Host "Taskul '$TaskName' nu era inregistrat." + } + return +} + +if (-not $SmtpPassword) { + throw 'Lipseste -SmtpPassword. Fara el nu pleaca nicio alerta.' +} + +# --- fisiere --------------------------------------------------------------- + +$logDir = Join-Path $InstallDir 'log' +foreach ($d in @($InstallDir, $logDir)) { + if (-not (Test-Path -LiteralPath $d)) { + New-Item -ItemType Directory -Path $d -Force | Out-Null + } +} + +$sourceScript = Join-Path $PSScriptRoot 'ups-monitor.ps1' +if (-not (Test-Path -LiteralPath $sourceScript)) { + throw "Nu gasesc ups-monitor.ps1 langa install-ups-monitor.ps1 ($sourceScript)" +} +Copy-Item -LiteralPath $sourceScript -Destination $InstallDir -Force +Write-Host "Copiat ups-monitor.ps1 in $InstallDir" + +$config = [ordered]@{ + enabled = $true + pollSeconds = $PollSeconds + shutdownAfterMinutesOnBattery = $ShutdownAfterMinutesOnBattery + shutdownAtBatteryPercent = $ShutdownAtBatteryPercent + shutdownAtRuntimeSeconds = 0 + confirmPolls = 3 + commsLostPolls = 8 + commsAlertRepeatHours = 24 + logPath = $logDir + stateFile = (Join-Path $InstallDir 'state.json') + mailTo = $MailTo + smtp = [ordered]@{ + server = $SmtpServer + port = $SmtpPort + user = $SmtpUser + password = $SmtpPassword + from = $SmtpFrom + } + oracle = [ordered]@{ + home = $OracleHome + sid = $OracleSid + service = $OracleService + listenerService = $OracleListenerService + shutdownTimeoutSeconds = $OracleShutdownTimeoutSeconds + } +} + +$configPath = Join-Path $InstallDir 'config.json' +$config | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $configPath -Encoding UTF8 +Write-Host "Scris $configPath" + +# --- drepturi: config.json contine parola SMTP ------------------------------ + +$acl = Get-Acl -LiteralPath $InstallDir +$acl.SetAccessRuleProtection($true, $false) # taie mostenirea, fara sa copieze regulile +foreach ($r in @($acl.Access)) { $acl.RemoveAccessRule($r) | Out-Null } +foreach ($who in @('NT AUTHORITY\SYSTEM', 'BUILTIN\Administrators')) { + $acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule( + $who, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))) +} +Set-Acl -LiteralPath $InstallDir -AclObject $acl +Write-Host "Drepturi restranse pe $InstallDir : doar SYSTEM si Administratori" + +# --- verificari inainte de inregistrare ------------------------------------- + +foreach ($svc in @($OracleService, $OracleListenerService)) { + if (-not (Get-Service -Name $svc -ErrorAction SilentlyContinue)) { + Write-Warning "Serviciul '$svc' nu exista pe acest server - verifica parametrii Oracle." + } +} +if (-not (Test-Path -LiteralPath (Join-Path $OracleHome 'bin\sqlplus.exe'))) { + Write-Warning "sqlplus.exe nu e la $OracleHome\bin - oprirea va cadea pe Stop-Service." +} + +# --- taskul programat ------------------------------------------------------- + +$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ( + '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{0}\ups-monitor.ps1" -Mode Run' -f $InstallDir) + +$triggerBoot = New-ScheduledTaskTrigger -AtStartup + +# Al doilea declansator: reporneste procesul daca a murit, fara sa-l dubleze. +# -RepetitionDuration se lasa nespecificat intentionat: inseamna "la nesfarsit". +# [TimeSpan]::MaxValue, desi documentat pentru asta, e respins de Task Scheduler +# ("P99999999DT23H59M59S ... out of range"). +$triggerRepeat = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) ` + -RepetitionInterval (New-TimeSpan -Minutes 10) + +$settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -MultipleInstances IgnoreNew ` + -ExecutionTimeLimit ([TimeSpan]::Zero) ` + -RestartCount 999 ` + -RestartInterval (New-TimeSpan -Minutes 1) + +$taskPrincipal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount -RunLevel Highest + +if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false +} + +Register-ScheduledTask -TaskName $TaskName ` + -Description 'Monitorizeaza UPS-ul prin canalul HID nativ, alerteaza pe email si opreste curat Oracle inainte de epuizarea bateriei. Inlocuieste ViewPower.' ` + -Action $action -Trigger @($triggerBoot, $triggerRepeat) ` + -Settings $settings -Principal $taskPrincipal | Out-Null + +Start-ScheduledTask -TaskName $TaskName +Start-Sleep -Seconds 5 + +Write-Host '' +Write-Host '--- rezultat ---' +Get-ScheduledTask -TaskName $TaskName | Format-Table -AutoSize TaskName, State +Get-ScheduledTaskInfo -TaskName $TaskName | Format-List LastRunTime, LastTaskResult, NumberOfMissedRuns +Write-Host '' +Write-Host 'Verificari utile:' +Write-Host (" powershell -ExecutionPolicy Bypass -File {0}\ups-monitor.ps1 -Mode Status" -f $InstallDir) +Write-Host (" powershell -ExecutionPolicy Bypass -File {0}\ups-monitor.ps1 -Mode TestMail" -f $InstallDir) +Write-Host (" powershell -ExecutionPolicy Bypass -File {0}\ups-monitor.ps1 -Mode TestShutdown" -f $InstallDir) +Write-Host '' +Write-Host ("Oprire de urgenta a automatismului: New-Item -ItemType File '{0}\DEZACTIVAT'" -f $InstallDir) diff --git a/scripts/ups-monitor/test-comms-loss.ps1 b/scripts/ups-monitor/test-comms-loss.ps1 new file mode 100644 index 0000000..7ff7c83 --- /dev/null +++ b/scripts/ups-monitor/test-comms-loss.ps1 @@ -0,0 +1,65 @@ +<# +.SYNOPSIS + Verifica alerta "UPS-ul nu mai comunica", dezactivand temporar dispozitivul de + baterie HID - echivalentul software al cablului USB scos din UPS. + +.DESCRIPTION + Se ruleaza PE SERVER, ridicat (Administrator), cu monitorul deja pornit. + + Reactivarea dispozitivului e in blocul finally, ca sa nu ramana oprit daca pica + sesiunea SSH sau se intrerupe scriptul. Testul e sigur cat timp serverul e pe + retea: fara baterie vizibila, monitorul intra pe ramura de "comunicatie pierduta" + si NU ia nicio decizie de oprire. + + Asteptarea implicita e calculata din config.json: commsLostPolls * pollSeconds, + plus o marja. + +.EXAMPLE + powershell -ExecutionPolicy Bypass -File .\test-comms-loss.ps1 +#> +[CmdletBinding()] +param( + [string]$InstallDir = 'C:\ROMFAST\ups-monitor', + [int]$WaitSeconds = 0 +) + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' + +$dev = Get-PnpDevice -Class Battery -ErrorAction SilentlyContinue | + Where-Object { $_.InstanceId -like 'HID\*' } | Select-Object -First 1 +if (-not $dev) { + throw 'Nu am gasit niciun dispozitiv de baterie HID - UPS-ul nu e vazut de Windows nici acum.' +} +Write-Host "Dispozitiv tinta: $($dev.FriendlyName) [$($dev.InstanceId)]" + +if ($WaitSeconds -le 0) { + $cfg = Get-Content -LiteralPath (Join-Path $InstallDir 'config.json') -Raw -Encoding UTF8 | ConvertFrom-Json + $WaitSeconds = ($cfg.commsLostPolls * $cfg.pollSeconds) + 45 +} +Write-Host "Astept $WaitSeconds secunde cat dispozitivul e oprit." + +try { + Disable-PnpDevice -InstanceId $dev.InstanceId -Confirm:$false -ErrorAction Stop + Start-Sleep -Seconds 3 + $n = @(Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue).Count + Write-Host "dezactivat : $((Get-PnpDevice -InstanceId $dev.InstanceId).Status) | instante Win32_Battery = $n" + Start-Sleep -Seconds $WaitSeconds +} finally { + Enable-PnpDevice -InstanceId $dev.InstanceId -Confirm:$false -ErrorAction SilentlyContinue + Start-Sleep -Seconds 10 + $n = @(Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue).Count + Write-Host "reactivat : $((Get-PnpDevice -InstanceId $dev.InstanceId).Status) | instante Win32_Battery = $n" + if ($n -lt 1) { + Write-Warning 'UPS-ul NU a revenit in Win32_Battery. Verifica Device Manager -> Batteries inainte de a pleca.' + } +} + +Start-Sleep -Seconds 45 +Write-Host '' +Write-Host '--- ultimele linii de log ---' +Get-Content (Join-Path $InstallDir ('log\ups-monitor-{0}.log' -f (Get-Date -Format 'yyyy-MM'))) -Tail 12 + +Write-Host '' +Write-Host 'Asteptat: o linie ALERT "UPS-ul nu mai este vazut de Windows", un email trimis,' +Write-Host 'apoi "Comunicatia cu UPS-ul a revenit" si al doilea email.' diff --git a/scripts/ups-monitor/ups-monitor.ps1 b/scripts/ups-monitor/ups-monitor.ps1 new file mode 100644 index 0000000..cae70cd --- /dev/null +++ b/scripts/ups-monitor/ups-monitor.ps1 @@ -0,0 +1,605 @@ +<# +.SYNOPSIS + Monitorizeaza UPS-ul serverului Oracle de productie (10.0.20.36 / ROA-CARAPETRU2) + prin canalul HID nativ Windows si opreste curat baza inainte de epuizarea bateriei. + +.DESCRIPTION + Inlocuieste ViewPower, care nu reusea handshake-ul de protocol cu acest UPS + (5777 x "QPI return(NAK", zero citiri reusite) si a fost dezactivat pe 10.08.2026. + + Sursa de date este API-ul Windows GetSystemPowerStatus - exact acelasi semnal pe + care il foloseste si Windows pentru actiunea proprie de "critical battery", deci + daca merge oprirea automata a Windows-ului merge si scriptul asta. + + Ce face: + - alerteaza pe email la trecerea pe baterie si la revenirea curentului; + - alerteaza cand UPS-ul nu mai comunica deloc cu calculatorul (cazul care a + trecut neobservat 17 zile in august 2026); + - opreste curat Oracle (listener, apoi "shutdown immediate") si abia apoi + Windows-ul, inainte ca bateria sa ajunga la pragul brutal al Windows-ului. + + Documentatie: docs/ups-server36-monitorizare.md + +.PARAMETER ConfigPath + Calea catre config.json. Implicit, langa script. + +.PARAMETER Mode + Run - bucla continua (modul in care ruleaza taskul programat). + Once - o singura evaluare, apoi iese. Pentru depanare. + Status - afiseaza starea curenta (UPS, Oracle, task, ultimele evenimente). + TestMail - trimite un email de test si iese. + TestShutdown - parcurge secventa de oprire in gol (dry-run), fara sa opreasca nimic. + +.NOTES + Fisierul e intentionat fara diacritice: PowerShell 5.1 citeste .ps1 fara BOM ca + ANSI, iar diacriticele ar ajunge corupte in emailuri. +#> +[CmdletBinding()] +param( + # Implicit, config.json de langa script. Nu se poate folosi $PSScriptRoot direct + # in valoarea implicita: la legarea parametrilor e inca gol. + [string]$ConfigPath, + + [ValidateSet('Run', 'Once', 'Status', 'TestMail', 'TestShutdown')] + [string]$Mode = 'Run' +) + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +if (-not $ConfigPath) { + $scriptDir = $PSScriptRoot + if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path } + $ConfigPath = Join-Path $scriptDir 'config.json' +} + +# --------------------------------------------------------------------------- +# API-ul de alimentare al Windows-ului +# --------------------------------------------------------------------------- + +if (-not ('RomfastPowerStatus' -as [type])) { + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; + +[StructLayout(LayoutKind.Sequential)] +public struct SYSTEM_POWER_STATUS { + public byte ACLineStatus; + public byte BatteryFlag; + public byte BatteryLifePercent; + public byte SystemStatusFlag; + public int BatteryLifeTime; + public int BatteryFullLifeTime; +} + +public static class RomfastPowerStatus { + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetSystemPowerStatus(out SYSTEM_POWER_STATUS status); +} +'@ +} + +# ACLineStatus: 0 = pe baterie, 1 = pe retea, 255 = necunoscut +# BatteryFlag: 1 = high, 2 = low, 4 = critical, 8 = charging, +# 128 = NU EXISTA BATERIE (UPS-ul nu mai comunica), 255 = necunoscut +# BatteryLifePercent: 0..100, 255 = necunoscut +# BatteryLifeTime: secunde ramase, -1 = necunoscut + +function Get-PowerReading { + $st = New-Object SYSTEM_POWER_STATUS + $ok = [RomfastPowerStatus]::GetSystemPowerStatus([ref]$st) + if (-not $ok) { + throw "GetSystemPowerStatus a esuat (Win32 error $([Runtime.InteropServices.Marshal]::GetLastWin32Error()))" + } + + $noBattery = ($st.BatteryFlag -eq 128) -or ($st.BatteryFlag -eq 255) + + $percent = $null + if ($st.BatteryLifePercent -le 100) { $percent = [int]$st.BatteryLifePercent } + + $runtime = $null + if ($st.BatteryLifeTime -ge 0) { $runtime = [int]$st.BatteryLifeTime } + + [pscustomobject]@{ + UpsPresent = (-not $noBattery) + OnBattery = ($st.ACLineStatus -eq 0) + AcKnown = ($st.ACLineStatus -eq 0 -or $st.ACLineStatus -eq 1) + Percent = $percent + RuntimeSec = $runtime + ACLineStatus = [int]$st.ACLineStatus + BatteryFlag = [int]$st.BatteryFlag + } +} + +function Get-UpsDeviceInfo { + # Confirmare a doua, din WMI - folosita doar in alerte si in modul Status, + # ca sa nu interogam WMI la fiecare ciclu. + try { + $b = Get-CimInstance Win32_Battery -ErrorAction Stop | Select-Object -First 1 + if ($null -eq $b) { return 'Win32_Battery: nicio instanta (UPS-ul nu e vazut de Windows)' } + return "Win32_Battery: $($b.Name), Status=$($b.Status), BatteryStatus=$($b.BatteryStatus), Incarcare=$($b.EstimatedChargeRemaining)%" + } catch { + return "Win32_Battery: interogare esuata ($($_.Exception.Message))" + } +} + +# --------------------------------------------------------------------------- +# Configuratie, log, stare +# --------------------------------------------------------------------------- + +function Get-Config { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { + throw "Lipseste fisierul de configurare: $Path" + } + $cfg = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json + + # Valori implicite pentru cheile care pot lipsi. + $defaults = @{ + enabled = $true + pollSeconds = 15 + shutdownAfterMinutesOnBattery = 10 + shutdownAtBatteryPercent = 35 + shutdownAtRuntimeSeconds = 0 # 0 = criteriu dezactivat + confirmPolls = 3 + commsLostPolls = 8 + commsAlertRepeatHours = 24 + } + foreach ($k in $defaults.Keys) { + if (-not ($cfg.PSObject.Properties.Name -contains $k)) { + $cfg | Add-Member -NotePropertyName $k -NotePropertyValue $defaults[$k] + } + } + return $cfg +} + +$script:LogDir = $null + +function Write-Log { + param( + [string]$Message, + [ValidateSet('INFO', 'WARN', 'ALERT', 'ERROR')] + [string]$Level = 'INFO' + ) + $line = '{0} {1,-5} {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message + Write-Host $line + if ($script:LogDir) { + try { + if (-not (Test-Path -LiteralPath $script:LogDir)) { + New-Item -ItemType Directory -Path $script:LogDir -Force | Out-Null + } + $file = Join-Path $script:LogDir ('ups-monitor-{0}.log' -f (Get-Date -Format 'yyyy-MM')) + Add-Content -LiteralPath $file -Value $line -Encoding UTF8 + } catch { + # Logul nu are voie sa opreasca monitorizarea. + } + } +} + +function Get-State { + param([string]$Path) + if (Test-Path -LiteralPath $Path) { + try { + $loaded = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json + return (Initialize-State -State $loaded) + } catch { + Write-Log "Fisierul de stare e corupt, se reia de la zero: $($_.Exception.Message)" -Level WARN + } + } + return New-State +} + +function New-State { + [pscustomobject]@{ + powerState = 'unknown' # online | battery | unknown + onBatterySince = $null + commsState = 'unknown' # ok | lost | unknown + lastCommsAlert = $null + shutdownTriggered = $false + } +} + +function Initialize-State { + # Un state.json scris de o versiune mai veche poate sa nu aiba toate cheile, + # iar Set-StrictMode ar pica pe prima proprietate lipsa. + param([object]$State) + $template = New-State + foreach ($p in $template.PSObject.Properties) { + if (-not ($State.PSObject.Properties.Name -contains $p.Name)) { + $State | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value + } + } + return $State +} + +function Save-State { + param([object]$State, [string]$Path) + try { + $dir = Split-Path -Parent $Path + if ($dir -and -not (Test-Path -LiteralPath $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + $State | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $Path -Encoding UTF8 + } catch { + Write-Log "Nu am putut salva starea in $Path : $($_.Exception.Message)" -Level WARN + } +} + +# --------------------------------------------------------------------------- +# Email +# --------------------------------------------------------------------------- + +function Send-UpsMail { + param( + [object]$Config, + [string]$Subject, + [string]$Body + ) + # Un email esuat nu are voie sa opreasca bucla si nici secventa de oprire: + # in pana de curent e foarte posibil sa fie cazut si switch-ul. + try { + $smtp = New-Object System.Net.Mail.SmtpClient($Config.smtp.server, [int]$Config.smtp.port) + $smtp.EnableSsl = $true + $smtp.Timeout = 20000 + # UseDefaultCredentials trebuie pus inainte de Credentials: setarea lui + # sterge acreditarile deja atribuite. + $smtp.UseDefaultCredentials = $false + $smtp.Credentials = New-Object System.Net.NetworkCredential($Config.smtp.user, $Config.smtp.password) + + $msg = New-Object System.Net.Mail.MailMessage + $msg.From = New-Object System.Net.Mail.MailAddress($Config.smtp.from, "UPS $env:COMPUTERNAME") + foreach ($to in $Config.mailTo) { $msg.To.Add($to) } + $msg.Subject = $Subject + $msg.Body = $Body + $msg.SubjectEncoding = [Text.Encoding]::UTF8 + $msg.BodyEncoding = [Text.Encoding]::UTF8 + + $smtp.Send($msg) + $msg.Dispose() + $smtp.Dispose() + Write-Log "Email trimis: $Subject" + return $true + } catch { + Write-Log "Email NETRIMIS ($Subject): $($_.Exception.Message)" -Level ERROR + return $false + } +} + +function Format-Reading { + param([object]$Reading, [object]$Config) + + $pct = 'necunoscut' + if ($null -ne $Reading.Percent) { $pct = "$($Reading.Percent)%" } + + $rt = 'neraportat de UPS' + if ($null -ne $Reading.RuntimeSec) { $rt = '{0} min' -f [math]::Round($Reading.RuntimeSec / 60, 1) } + + $alim = 'necunoscuta' + if ($Reading.OnBattery) { $alim = 'PE BATERIE' } elseif ($Reading.AcKnown) { $alim = 'pe retea' } + + $vazut = 'NU' + if ($Reading.UpsPresent) { $vazut = 'da' } + + $oracle = 'nedeterminat' + try { + $svc = Get-Service -Name $Config.oracle.service -ErrorAction Stop + $lsn = Get-Service -Name $Config.oracle.listenerService -ErrorAction Stop + $oracle = "$($svc.Name)=$($svc.Status), $($lsn.Name)=$($lsn.Status)" + } catch { + $oracle = "nedeterminat ($($_.Exception.Message))" + } + + @" +Server : $env:COMPUTERNAME (10.0.20.36) +Moment : $(Get-Date -Format 'dd.MM.yyyy HH:mm:ss') +Alimentare : $alim +Incarcare : $pct +Autonomie estim. : $rt +UPS vazut de OS : $vazut +$(Get-UpsDeviceInfo) +Oracle : $oracle + +Praguri configurate: + oprire dupa $($Config.shutdownAfterMinutesOnBattery) min pe baterie + oprire sub $($Config.shutdownAtBatteryPercent)% incarcare + Windows opreste singur, ca ultima plasa, la 20% +"@ +} + +# --------------------------------------------------------------------------- +# Oprirea curata +# --------------------------------------------------------------------------- + +function Stop-OracleCleanly { + param([object]$Config, [switch]$DryRun) + + $oraHome = $Config.oracle.home + $sid = $Config.oracle.sid + $timeoutMs = [int]$Config.oracle.shutdownTimeoutSeconds * 1000 + + Write-Log "Opresc listenerul $($Config.oracle.listenerService)" + if (-not $DryRun) { + try { + Stop-Service -Name $Config.oracle.listenerService -Force -ErrorAction Stop + Write-Log 'Listener oprit' + } catch { + Write-Log "Listenerul nu s-a oprit: $($_.Exception.Message)" -Level WARN + } + } + + $sqlplus = Join-Path $oraHome 'bin\sqlplus.exe' + if (-not (Test-Path -LiteralPath $sqlplus)) { + Write-Log "sqlplus.exe lipseste la $sqlplus - trec direct pe oprirea serviciului" -Level WARN + } else { + Write-Log "Oracle: shutdown immediate pe instanta $sid (timeout $($Config.oracle.shutdownTimeoutSeconds)s)" + if (-not $DryRun) { + $tmpSql = Join-Path $env:TEMP ('ups-shutdown-{0}.sql' -f $PID) + $tmpOut = Join-Path $env:TEMP ('ups-shutdown-{0}.out' -f $PID) + try { + Set-Content -LiteralPath $tmpSql -Encoding ASCII -Value @( + 'whenever sqlerror exit failure' + 'shutdown immediate' + 'exit' + ) + $env:ORACLE_HOME = $oraHome + $env:ORACLE_SID = $sid + + $p = Start-Process -FilePath $sqlplus ` + -ArgumentList @('-S', '-L', '/ as sysdba', "@$tmpSql") ` + -NoNewWindow -PassThru ` + -RedirectStandardOutput $tmpOut -RedirectStandardError "$tmpOut.err" + + if ($p.WaitForExit($timeoutMs)) { + $out = if (Test-Path $tmpOut) { (Get-Content -LiteralPath $tmpOut -Raw).Trim() } else { '' } + Write-Log "sqlplus a iesit cu codul $($p.ExitCode). Iesire: $out" + } else { + Write-Log 'sqlplus nu a terminat in timpul alocat - il opresc si merg pe serviciu' -Level WARN + try { $p.Kill() } catch { } + } + } catch { + Write-Log "shutdown immediate a esuat: $($_.Exception.Message)" -Level WARN + } finally { + Remove-Item -LiteralPath $tmpSql, $tmpOut, "$tmpOut.err" -ErrorAction SilentlyContinue + } + } + } + + # Plasa de siguranta: serviciul e configurat el insusi cu ORA_ROA_SHUTDOWNTYPE=immediate + # si ORA_ROA_SHUTDOWN_TIMEOUT=90, deci oprirea lui e tot curata. + Write-Log "Opresc serviciul $($Config.oracle.service)" + if (-not $DryRun) { + try { + Stop-Service -Name $Config.oracle.service -Force -ErrorAction Stop + Write-Log 'Serviciul Oracle oprit' + } catch { + Write-Log "Serviciul Oracle nu s-a oprit: $($_.Exception.Message)" -Level ERROR + } + } +} + +function Invoke-UpsShutdown { + param( + [object]$Config, + [object]$Reading, + [string]$Reason, + [switch]$DryRun + ) + + $tag = if ($DryRun) { '[DRY-RUN] ' } else { '' } + Write-Log "${tag}DECLANSEZ OPRIREA: $Reason" -Level ALERT + + # Emailul pleaca inainte de orice - in pana de curent reteaua poate cadea oricand. + Send-UpsMail -Config $Config ` + -Subject "$tag[UPS] OPRESC serverul $env:COMPUTERNAME - $Reason" ` + -Body ("Motiv: $Reason`n`n" + (Format-Reading -Reading $Reading -Config $Config) + @" + +Secventa care urmeaza: + 1. oprire listener Oracle + 2. shutdown immediate pe instanta $($Config.oracle.sid) + 3. oprire serviciu Oracle + 4. shutdown Windows + +La revenirea curentului serverul NU porneste singur decat daca e activat +"Restore on AC power loss" in BIOS. +"@) | Out-Null + + Stop-OracleCleanly -Config $Config -DryRun:$DryRun + + Write-Log "${tag}Opresc Windows-ul" + if (-not $DryRun) { + # Out-Null: altfel iesirea lui shutdown.exe s-ar amesteca in valoarea + # returnata de Invoke-Cycle, care e obiectul de stare. + & shutdown.exe /s /f /t 30 /c "UPS: $Reason" | Out-Null + } +} + +# --------------------------------------------------------------------------- +# Un ciclu de evaluare +# --------------------------------------------------------------------------- + +function Invoke-Cycle { + param([object]$Config, [object]$State, [switch]$DryRun) + + $now = Get-Date + $r = Get-PowerReading + + # --- comunicatia cu UPS-ul --------------------------------------------- + if (-not $r.UpsPresent) { + $script:CommsMissCount++ + if ($script:CommsMissCount -ge $Config.commsLostPolls) { + $repeat = $false + if ($State.lastCommsAlert) { + $repeat = ((New-TimeSpan -Start ([datetime]$State.lastCommsAlert) -End $now).TotalHours -ge $Config.commsAlertRepeatHours) + } + if ($State.commsState -ne 'lost' -or $repeat) { + Write-Log 'UPS-ul nu mai este vazut de Windows' -Level ALERT + Send-UpsMail -Config $Config ` + -Subject "[UPS] $env:COMPUTERNAME nu mai comunica cu UPS-ul" ` + -Body ((Format-Reading -Reading $r -Config $Config) + @" + +Windows nu mai vede bateria UPS-ului (BatteryFlag=$($r.BatteryFlag)). +Cat timp e asa, NU exista nicio protectie: nici oprirea automata a scriptului, +nici plasa de 20% a Windows-ului nu se pot declansa. + +De verificat, in ordine: cablul USB, portul, alimentarea UPS-ului, apoi +Device Manager -> Batteries -> "HID UPS Battery". +"@) | Out-Null + $State.commsState = 'lost' + $State.lastCommsAlert = $now.ToString('o') + } + } + return $State # fara comunicatie nu luam nicio decizie de oprire + } + + if ($script:CommsMissCount -gt 0 -or $State.commsState -eq 'lost') { + if ($State.commsState -eq 'lost') { + Write-Log 'Comunicatia cu UPS-ul a revenit' + Send-UpsMail -Config $Config ` + -Subject "[UPS] $env:COMPUTERNAME - comunicatia cu UPS-ul a revenit" ` + -Body (Format-Reading -Reading $r -Config $Config) | Out-Null + } + $script:CommsMissCount = 0 + $State.commsState = 'ok' + $State.lastCommsAlert = $null + } + if ($State.commsState -eq 'unknown') { $State.commsState = 'ok' } + + # --- trecerea pe baterie / revenirea curentului ------------------------- + if ($r.OnBattery) { + $script:OnBatteryCount++ + + if ($State.powerState -ne 'battery') { + $State.powerState = 'battery' + $State.onBatterySince = $now.ToString('o') + $State.shutdownTriggered = $false + Write-Log "PANA DE CURENT - trecut pe baterie ($($r.Percent)%)" -Level ALERT + Send-UpsMail -Config $Config ` + -Subject "[UPS] PANA DE CURENT - $env:COMPUTERNAME a trecut pe baterie" ` + -Body ((Format-Reading -Reading $r -Config $Config) + @" + +Daca curentul nu revine, serverul va fi oprit curat la primul prag atins. +Urmeaza un email si atunci. +"@) | Out-Null + } + + $since = [datetime]$State.onBatterySince + $minutes = (New-TimeSpan -Start $since -End $now).TotalMinutes + + if (-not $State.shutdownTriggered -and $script:OnBatteryCount -ge $Config.confirmPolls) { + $reason = $null + if ($minutes -ge $Config.shutdownAfterMinutesOnBattery) { + $reason = "$([math]::Round($minutes)) minute pe baterie (prag: $($Config.shutdownAfterMinutesOnBattery))" + } elseif ($null -ne $r.Percent -and $r.Percent -le $Config.shutdownAtBatteryPercent) { + $reason = "incarcare $($r.Percent)% (prag: $($Config.shutdownAtBatteryPercent)%)" + } elseif ($Config.shutdownAtRuntimeSeconds -gt 0 -and $null -ne $r.RuntimeSec -and $r.RuntimeSec -le $Config.shutdownAtRuntimeSeconds) { + $reason = "autonomie estimata $([math]::Round($r.RuntimeSec / 60, 1)) min (prag: $([math]::Round($Config.shutdownAtRuntimeSeconds / 60, 1)) min)" + } + + if ($reason) { + $State.shutdownTriggered = $true + Save-State -State $State -Path $Config.stateFile + Invoke-UpsShutdown -Config $Config -Reading $r -Reason $reason -DryRun:$DryRun + } + } + } else { + $script:OnBatteryCount = 0 + + if ($State.powerState -eq 'battery') { + $durata = if ($State.onBatterySince) { + '{0:N1} minute' -f (New-TimeSpan -Start ([datetime]$State.onBatterySince) -End $now).TotalMinutes + } else { 'necunoscuta' } + Write-Log "Curentul a revenit dupa $durata" + Send-UpsMail -Config $Config ` + -Subject "[UPS] Curentul a revenit - $env:COMPUTERNAME" ` + -Body ("Serverul a stat pe baterie $durata.`n`n" + (Format-Reading -Reading $r -Config $Config)) | Out-Null + } + $State.powerState = 'online' + $State.onBatterySince = $null + $State.shutdownTriggered = $false + } + + return $State +} + +# --------------------------------------------------------------------------- +# Punctul de intrare +# --------------------------------------------------------------------------- + +$cfg = Get-Config -Path $ConfigPath +$script:LogDir = $cfg.logPath +$script:OnBatteryCount = 0 +$script:CommsMissCount = 0 + +switch ($Mode) { + + 'Status' { + $r = Get-PowerReading + Write-Host (Format-Reading -Reading $r -Config $cfg) + Write-Host '' + $st = Get-State -Path $cfg.stateFile + Write-Host "Stare salvata : power=$($st.powerState) comms=$($st.commsState) onBatterySince=$($st.onBatterySince)" + try { + $task = Get-ScheduledTask -TaskName 'ROMFAST UPS Monitor' -ErrorAction Stop + $info = Get-ScheduledTaskInfo -TaskName 'ROMFAST UPS Monitor' + Write-Host "Task programat : $($task.State), ultima rulare $($info.LastRunTime), rezultat $($info.LastTaskResult)" + } catch { + Write-Host 'Task programat : NEINREGISTRAT' + } + $log = Join-Path $cfg.logPath ('ups-monitor-{0}.log' -f (Get-Date -Format 'yyyy-MM')) + if (Test-Path -LiteralPath $log) { + Write-Host '' + Write-Host "Ultimele linii din $log :" + Get-Content -LiteralPath $log -Tail 15 | ForEach-Object { " $_" } + } + } + + 'TestMail' { + $r = Get-PowerReading + $ok = Send-UpsMail -Config $cfg ` + -Subject "[UPS] Test - monitorizare $env:COMPUTERNAME" ` + -Body ("Email de test din ups-monitor.ps1. Daca il primesti, alertarea functioneaza.`n`n" + (Format-Reading -Reading $r -Config $cfg)) + if (-not $ok) { exit 1 } + } + + 'TestShutdown' { + $r = Get-PowerReading + Invoke-UpsShutdown -Config $cfg -Reading $r -Reason 'TEST (dry-run, nu se opreste nimic)' -DryRun + } + + 'Once' { + $st = Get-State -Path $cfg.stateFile + $st = Invoke-Cycle -Config $cfg -State $st + Save-State -State $st -Path $cfg.stateFile + Write-Host (Format-Reading -Reading (Get-PowerReading) -Config $cfg) + } + + 'Run' { + Write-Log "Pornit (interval $($cfg.pollSeconds)s, praguri: $($cfg.shutdownAfterMinutesOnBattery) min / $($cfg.shutdownAtBatteryPercent)%)" + $st = Get-State -Path $cfg.stateFile + $disableMarker = Join-Path (Split-Path -Parent $ConfigPath) 'DEZACTIVAT' + $wasDisabled = $false + + while ($true) { + try { + if ((Test-Path -LiteralPath $disableMarker) -or -not $cfg.enabled) { + if (-not $wasDisabled) { + Write-Log 'Monitorizare DEZACTIVATA (marcaj DEZACTIVAT sau enabled=false) - doar astept' -Level WARN + $wasDisabled = $true + } + } else { + if ($wasDisabled) { + Write-Log 'Monitorizare reactivata' + $wasDisabled = $false + } + $st = Invoke-Cycle -Config $cfg -State $st + Save-State -State $st -Path $cfg.stateFile + } + } catch { + Write-Log "Eroare in ciclu: $($_.Exception.Message)" -Level ERROR + } + Start-Sleep -Seconds $cfg.pollSeconds + } + } +}