From bb7dd40436f76aab355e392039214ec49d739c2b Mon Sep 17 00:00:00 2001 From: vitya Date: Sun, 24 May 2026 12:35:25 +0300 Subject: [PATCH] ruvds-backup: add email-notify (Yandex SMTP 587 STARTTLS) + fix size calc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add dual-channel notifications — ntfy (already there) + email via Send-MailMessage / Yandex SMTP, mirroring VDS msmtp pipeline. Email: noreply@snolla.com -> vitya.kuznetsov@gmail.com. Verified delivered (DKIM pass, SPF pass) via run #2 + manual smoke. Settings sourced from C:\sites\snolla\Web.config (Yandex SMTP), cross-checked against pass-store snolla-smtp/full-env. Same creds, same account as VDS backup msmtp uses. SMTP port: 587 STARTTLS, NOT 465 implicit-TLS: .NET SmtpClient / Send-MailMessage support only STARTTLS. Yandex accepts both; we use 587 for native PS tooling. VDS msmtp uses 465 implicit-TLS — both work, different tools. Size in email: switched from `rclone size --json | ConvertFrom-Json` (parses fail when rclone NOTICE stderr leaks into stdout) to local Get-ChildItem on C:\sites\snolla. Instant, no JSON dance. setup.ps1: ned params for SMTP creds + OPS_NOTIFY_EMAIL; config.env template extended. README.md: notifications section split into ntfy + email subsections, new SMTP-port + size-calc decisions in Decisions log. Run #2 (12:30:03) and run #3 (12:33) both produced email delivery receipts in user inbox. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/ruvds-backup-daily-kreknin/README.md | 18 +++++-- scripts/ruvds-backup-daily-kreknin/run.ps1 | 56 ++++++++++++++++---- scripts/ruvds-backup-daily-kreknin/setup.ps1 | 14 ++++- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/scripts/ruvds-backup-daily-kreknin/README.md b/scripts/ruvds-backup-daily-kreknin/README.md index 0f3eafd..a549930 100644 --- a/scripts/ruvds-backup-daily-kreknin/README.md +++ b/scripts/ruvds-backup-daily-kreknin/README.md @@ -35,9 +35,19 @@ Retention: **7 daily snapshots**. Older — pruned by `rclone purge` in step 4. ## Notifications -ntfy topic `vds-backup` (shared with VDS backup) — phone push: -- Success: title `RUVDS backup OK ()`, tag `green_circle`, priority `default` -- Fail: title `RUVDS backup FAILED ()`, tag `red_circle`, priority `high` +**Dual-channel** (oба отправляются после каждого run): + +- **ntfy topic `vds-backup`** (shared с VDS backup) — phone push через ntfy mobile app: + - Success: title `RUVDS backup OK ()`, tag `green_circle`, priority `default` + - Fail: title `RUVDS backup FAILED ()`, tag `red_circle`, priority `high` + +- **Email** via Yandex SMTP (`noreply@snolla.com` → `vitya.kuznetsov@gmail.com`): + - Port **587 STARTTLS** (.NET `SmtpClient` не дружит с implicit-TLS port 465; 587 STARTTLS работает с `Send-MailMessage -UseSsl`) + - Subject: `RUVDS backup -- SUCCESS / FAILED` + - Body: duration, size, certs count, component list, log path + - SMTP creds в `config.env` (re-used из `pass show snolla-smtp/full-env`, тот же account что VDS msmtp) + +Если email или ntfy fail — backup itself **не fail'ит** (notify-функции wrapped в try/catch, warning в лог). ## Decisions log @@ -45,6 +55,8 @@ ntfy topic `vds-backup` (shared with VDS backup) — phone push: - **SFTP без host-key validation** — rclone go-sftp library не parsит SSH known_hosts с keyscan format корректно (key mismatch error даже на fresh keyscan). Path RUVDS↔kreknin через public internet, но key auth достаточно для нашего threat model. Если threat model меняется — populate known_hosts file from `ssh-keyscan` в правильном format. - **`Invoke-Rclone` wrapper** — `$ErrorActionPreference = 'Stop'` + rclone NOTICE на stderr = native command terminating-error в PS. Wrapper temporarily switches к Continue для rclone calls, restores after. Без этого `2>$null` не помогает (PS видит non-empty stderr stream как error). - **`Backup-WebConfiguration -Force`** — параметра нет в этой версии WebAdministration module. Используем `Get-WebConfigurationBackup + Remove-WebConfigurationBackup` если exist + plain `Backup-WebConfiguration`. +- **SMTP port 587 STARTTLS, не 465 implicit-TLS** — .NET `SmtpClient` (и Powershell's `Send-MailMessage`) не поддерживает implicit-TLS на 465 (только STARTTLS). Yandex принимает обоих, выбрали 587 STARTTLS для compat с native PS tooling. VDS msmtp использует 465 implicit — оба валидны, разные tooling. +- **Local Get-ChildItem для size в email, не `rclone size --json`** — `Invoke-Rclone size --json` объединяет stdout JSON со stderr NOTICE, `ConvertFrom-Json` падает на mixed output. `Get-ChildItem C:\sites\snolla -Recurse | Measure -Sum Length` — local-disk-cost, instant. - **Cert PFX export pass = `ruvds-backup-pfx`** — temporary. TODO: pass-equivalent на Windows (gpg4win + pass-bash) или derived от machine-creds. - **SYSTEM principal, не user account** — Task Scheduler SYSTEM имеет full access к C:\sites\, Cert:\LocalMachine\My, %SystemRoot%\System32\inetsrv\, не требует stored-password. Pattern matches VDS root-cron decision. diff --git a/scripts/ruvds-backup-daily-kreknin/run.ps1 b/scripts/ruvds-backup-daily-kreknin/run.ps1 index e61738e..525f36c 100644 --- a/scripts/ruvds-backup-daily-kreknin/run.ps1 +++ b/scripts/ruvds-backup-daily-kreknin/run.ps1 @@ -17,7 +17,6 @@ $remoteToday = "$remoteBase/$today" if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } Start-Transcript -Path $logFile -Append -Force | Out-Null -# Load ntfy creds $cfg = @{} if (Test-Path "$base\config.env") { Get-Content "$base\config.env" | Where-Object { $_ -match '^[A-Z_]+=' } | ForEach-Object { @@ -35,21 +34,32 @@ function Notify-Ntfy($title, $msg, $priority='default', $tags='') { Invoke-RestMethod -Uri "$($cfg.NTFY_URL)/$topic" -Method POST ` -Headers @{ Authorization=$auth; Title=$title; Priority=$priority; Tags=$tags } ` -Body $msg -ContentType 'text/plain' -ErrorAction SilentlyContinue | Out-Null - } catch {} + } catch { Write-Host " ntfy WARNING: $($_.Exception.Message)" -ForegroundColor Yellow } +} + +function Notify-Email($subject, $body) { + try { + if (-not $cfg.SMTP_HOST -or -not $cfg.SMTP_USER -or -not $cfg.SMTP_PASS -or -not $cfg.OPS_NOTIFY_EMAIL) { return } + $secpass = ConvertTo-SecureString $cfg.SMTP_PASS -AsPlainText -Force + $mailCred = New-Object PSCredential($cfg.SMTP_USER, $secpass) + $prevEAP = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + Send-MailMessage -SmtpServer $cfg.SMTP_HOST -Port ([int]$cfg.SMTP_PORT) -UseSsl ` + -Credential $mailCred ` + -From $cfg.SMTP_FROM -To $cfg.OPS_NOTIFY_EMAIL ` + -Subject $subject -Body $body -Encoding UTF8 ` + -WarningAction SilentlyContinue + } finally { $ErrorActionPreference = $prevEAP } + } catch { Write-Host " email WARNING: $($_.Exception.Message)" -ForegroundColor Yellow } } -# Invoke rclone tolerating its NOTICE stderr (PS Stop catches them otherwise). -# Returns array of stdout lines + non-zero $LASTEXITCODE on real failure. function Invoke-Rclone { param([Parameter(ValueFromRemainingArguments=$true)][string[]]$Args) $prevEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' - try { - $out = & $rcloneExe @Args 2>&1 - return $out - } finally { - $ErrorActionPreference = $prevEAP - } + try { return & $rcloneExe @Args 2>&1 } + finally { $ErrorActionPreference = $prevEAP } } try { @@ -125,8 +135,30 @@ try { Remove-Item $certDir -Recurse -Force -ErrorAction SilentlyContinue $duration = [int](New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds - $msg = "RUVDS daily backup $today OK | $duration sec | $exported certs | snolla + IIS configs + ssh state" + $srcBytes = (Get-ChildItem 'C:\sites\snolla' -Recurse -File -EA SilentlyContinue | Measure-Object Length -Sum).Sum + $totalGB = '{0:N2}' -f ($srcBytes / 1GB) + $msg = "RUVDS daily backup $today OK | $duration sec | size $totalGB GB | $exported certs" Notify-Ntfy "RUVDS backup OK ($today)" $msg 'default' 'green_circle' + $emailBody = @" +RUVDS daily backup completed successfully. + +Date: $today +Duration: $duration seconds +Size: $totalGB GB +Certs: $exported PFX +Source: RUVDS ($env:COMPUTERNAME / 80.64.31.36) +Dest: kreknin:/volume1/NetBackup/ruvds-iis/$today/ + +Components synced: +- sites/snolla +- iis-config (applicationHost.config) +- iis-backup-webconfiguration +- certs (LE PFX exports) +- ssh-config + +Log: $logFile +"@ + Notify-Email "RUVDS backup $today -- SUCCESS" $emailBody Write-Host "`n=== DONE in $duration sec ===" } catch { @@ -135,8 +167,10 @@ try { Write-Host $_.ScriptStackTrace $duration = [int](New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds Notify-Ntfy "RUVDS backup FAILED ($today)" "After $duration sec: $err" 'high' 'red_circle' + Notify-Email "RUVDS backup $today -- FAILED" "After $duration sec: $err`n`nLog: $logFile" Stop-Transcript | Out-Null exit 1 } finally { try { Stop-Transcript | Out-Null } catch {} } + diff --git a/scripts/ruvds-backup-daily-kreknin/setup.ps1 b/scripts/ruvds-backup-daily-kreknin/setup.ps1 index f680ed7..f064015 100644 --- a/scripts/ruvds-backup-daily-kreknin/setup.ps1 +++ b/scripts/ruvds-backup-daily-kreknin/setup.ps1 @@ -24,6 +24,12 @@ param( [string]$NtfyPass = 'Pryakhin9', [string]$NtfyUrl = 'https://ntfy.vds.kzntsv.site', [string]$NtfyTopic = 'vds-backup', + [string]$SmtpHost = 'smtp.yandex.ru', + [int]$SmtpPort = 587, + [string]$SmtpFrom = 'noreply@snolla.com', + [string]$SmtpUser = 'noreply@snolla.com', + [string]$SmtpPass = 'JP44ajqZ3K#W', + [string]$OpsNotifyEmail = 'vitya.kuznetsov@gmail.com', [string]$ScheduleAt = '04:30' ) $ErrorActionPreference = 'Stop' @@ -71,12 +77,18 @@ disable_hashcheck = true & cmd.exe /c "icacls `"$base\rclone.conf`" /inheritance:r /grant `"SYSTEM:(F)`" /grant `"BUILTIN\Administrators:(F)`"" | Out-Null Write-Host "[OK] rclone.conf written" -# 4. config.env +# 4. config.env (ntfy + SMTP creds) @" NTFY_URL=$NtfyUrl NTFY_TOPIC=$NtfyTopic NTFY_USER=$NtfyUser NTFY_PASS=$NtfyPass +SMTP_HOST=$SmtpHost +SMTP_PORT=$SmtpPort +SMTP_FROM=$SmtpFrom +SMTP_USER=$SmtpUser +SMTP_PASS=$SmtpPass +OPS_NOTIFY_EMAIL=$OpsNotifyEmail "@ | Set-Content "$base\config.env" -Encoding ASCII -Force & cmd.exe /c "icacls `"$base\config.env`" /inheritance:r /grant `"SYSTEM:(F)`" /grant `"BUILTIN\Administrators:(F)`"" | Out-Null Write-Host "[OK] config.env written"