feat(ups): monitorizare UPS pe serverul 36, in locul ViewPower

ViewPower nu putea dialoga cu acest UPS: 5777 de "QPI return(NAK" si zero
citiri reusite in fereastra 6-10 august, pentru ca UPS-ul vorbeste HID Power
Device standard, nu dialectul text Voltronic. Serviciile au fost dezactivate
pe 10.08.2026 la 22:34, lasand serverul fara nicio alertare 17 zile. Nici
inainte nu exista: emailReceivers = 0, iar excuteProgram era gol, deci Oracle
nu era oprit curat oricum.

UPS-ul comunica insa perfect cu Windows pe canalul HID - dovedit de perechile
de evenimente Kernel-Power 105 la panele reale din 2026.

Monitorul nou citeste GetSystemPowerStatus, acelasi semnal pe care il foloseste
Windows pentru propria actiune la baterie critica. Alerteaza pe email la
trecerea pe baterie, la revenirea curentului si cand UPS-ul nu mai comunica -
cazul care a trecut neobservat. Opreste curat listenerul, apoi instanta cu
shutdown immediate, apoi Windows-ul, inaintea pragului brutal de 20% al
Windows-ului.

Ruleaza ca task programat sub SYSTEM, cu AllowStartIfOnBatteries si
DontStopIfGoingOnBatteries - fara ele Windows ar fi oprit taskul exact cand
serverul trece pe baterie.

Validat pe 27.08.2026: citire, email, secventa de oprire in dry-run si
pierderea comunicatiei (alerta la exact 8 cicluri, apoi revenire). Pragurile
de 10 min / 35% sunt conservatoare, nu masurate - testul de autonomie reala
ramane de facut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRyaDj39hPQ89SZS6URRpS
This commit is contained in:
Marius
2026-08-27 15:33:45 +03:00
parent 1e65805835
commit 9ba99eb9d1
8 changed files with 1205 additions and 2 deletions

View File

@@ -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
```

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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.'

View File

@@ -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
}
}
}