Files
admin/scripts/ruvds-backup-daily-kreknin/run.ps1
vitya 8db4b0d71c tasks(ruvds-backup-daily-kreknin): close 🟢 — live на 04:30 MSK daily
End-to-end pipeline отработан, run #1 verified 20 sec, 8.8 GB snolla +
IIS configs + 14 certs + sshd state → /volume1/NetBackup/ruvds-iis/<date>/
на kreknin. Retention 7 daily, ntfy vds-backup topic.

Scripts checked into scripts/ruvds-backup-daily-kreknin/:
- setup.ps1 (one-time install: SSH key + rclone + configs + ScheduledTask)
- run.ps1 (live backup logic; Invoke-Rclone wrapper для NOTICE-on-stderr)
- README.md (decisions log, smoke instructions, atomic revert)

Bugs found and fixed during smoke (см. README Decisions log):
1. Backup-WebConfiguration -Force параметра нет → check + Remove first
2. rclone --log-file lock с PS Start-Transcript → drop --log-file
3. rclone NOTICE на stderr + $ErrorActionPreference=Stop → Invoke-Rclone
   wrapper temporarily switches к Continue
4. ssh-keyscan known_hosts не parsится rclone go-sftp → drop pinning,
   rely on key-auth

Закрывает "Backup strategy для RUVDS IIS" Open question в
[iis-migration-to-ruvds].

Open follow-ups (не блокер):
- PFX export pass plaintext в скрипте — TODO move to gpg/DPAPI
- Retention prune (kept 1 today) — verify в day 8
- Phone-side ntfy push — user verifies

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:49:44 +03:00

143 lines
6.1 KiB
PowerShell

#requires -Version 5.1
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$today = Get-Date -Format 'yyyy-MM-dd'
$start = Get-Date
$base = 'C:\ProgramData\backup'
$logDir = "$base\logs"
$logFile = "$logDir\$today.log"
$rcloneExe = 'C:\Program Files\rclone\rclone.exe'
$rcloneCfg = "$base\rclone.conf"
$remoteBase = "kreknin:NetBackup/ruvds-iis"
$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 {
$kv = $_ -split '=', 2
$cfg[$kv[0]] = $kv[1]
}
}
function Notify-Ntfy($title, $msg, $priority='default', $tags='') {
try {
if (-not $cfg.NTFY_URL -or -not $cfg.NTFY_USER -or -not $cfg.NTFY_PASS) { return }
$pair = "$($cfg.NTFY_USER):$($cfg.NTFY_PASS)"
$auth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair))
$topic = if ($cfg.NTFY_TOPIC) { $cfg.NTFY_TOPIC } else { 'vds-backup' }
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 {}
}
# 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 {
Write-Host "=== RUVDS backup $today started at $start ==="
Write-Host "`n--- 1. IIS config snapshot ---"
Import-Module WebAdministration
$iisBackupName = "daily-$today"
$iisBackupDir = "$env:SystemRoot\System32\inetsrv\backup\$iisBackupName"
if (Get-WebConfigurationBackup -Name $iisBackupName -ErrorAction SilentlyContinue) {
Remove-WebConfigurationBackup -Name $iisBackupName
}
if (Test-Path $iisBackupDir) { Remove-Item $iisBackupDir -Recurse -Force }
Backup-WebConfiguration -Name $iisBackupName | Out-Null
Write-Host " $iisBackupDir created"
Write-Host "`n--- 2. Cert store export ---"
$certDir = "$base\certs-$today"
if (Test-Path $certDir) { Remove-Item $certDir -Recurse -Force }
New-Item -ItemType Directory -Path $certDir -Force | Out-Null
$pfxPass = ConvertTo-SecureString 'ruvds-backup-pfx' -AsPlainText -Force
$exported = 0
Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.HasPrivateKey } | ForEach-Object {
$cn = ($_.Subject -split ',')[0] -replace 'CN=','' -replace '[^A-Za-z0-9.-]','_'
if ($cn.Length -gt 60) { $cn = $cn.Substring(0,60) }
try {
Export-PfxCertificate -Cert $_ -FilePath "$certDir\$cn-$($_.Thumbprint.Substring(0,8)).pfx" -Password $pfxPass -ErrorAction SilentlyContinue | Out-Null
$exported++
} catch {}
}
Write-Host " exported $exported certs"
Write-Host "`n--- 3. rclone sync ---"
$rcCommon = @('--config', $rcloneCfg, '--transfers', '4', '--checkers', '8', '--stats=0')
Invoke-Rclone sync C:\sites\snolla "$remoteToday/sites/snolla/" @rcCommon | Out-Null
if ($LASTEXITCODE -ne 0) { throw "rclone sync snolla failed (exit $LASTEXITCODE)" }
Write-Host " snolla synced"
Invoke-Rclone copy 'C:\Windows\System32\inetsrv\config\applicationHost.config' "$remoteToday/iis-config/" @rcCommon | Out-Null
if ($LASTEXITCODE -ne 0) { throw "rclone copy applicationHost.config failed (exit $LASTEXITCODE)" }
Write-Host " applicationHost.config copied"
Invoke-Rclone sync $iisBackupDir "$remoteToday/iis-backup-webconfiguration/" @rcCommon | Out-Null
if ($LASTEXITCODE -ne 0) { throw "rclone sync iis-backup-webconfiguration failed (exit $LASTEXITCODE)" }
Write-Host " iis-backup-webconfiguration synced"
Invoke-Rclone sync $certDir "$remoteToday/certs/" @rcCommon | Out-Null
if ($LASTEXITCODE -ne 0) { throw "rclone sync certs failed (exit $LASTEXITCODE)" }
Write-Host " certs synced"
Invoke-Rclone sync C:\ProgramData\ssh "$remoteToday/ssh-config/" @rcCommon | Out-Null
if ($LASTEXITCODE -ne 0) { throw "rclone sync ssh-config failed (exit $LASTEXITCODE)" }
Write-Host " ssh-config synced"
Write-Host "`n--- 4. Retention prune (keep last 7) ---"
try {
$lsdOut = Invoke-Rclone lsd $remoteBase --config $rcloneCfg
$existing = $lsdOut | ForEach-Object {
$line = "$_".Trim()
if ($line -match '\s(\d{4}-\d{2}-\d{2})\s*$') { $Matches[1] }
} | Sort-Object -Unique
$toPrune = @($existing | Select-Object -SkipLast 7)
foreach ($d in $toPrune) {
Write-Host " pruning $d"
Invoke-Rclone purge "$remoteBase/$d" --config $rcloneCfg | Out-Null
}
Write-Host " kept $([math]::Min(@($existing).Count, 7)) snapshots; pruned $(@($toPrune).Count)"
} catch {
Write-Host " retention prune WARNING: $($_.Exception.Message)" -ForegroundColor Yellow
}
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"
Notify-Ntfy "RUVDS backup OK ($today)" $msg 'default' 'green_circle'
Write-Host "`n=== DONE in $duration sec ==="
} catch {
$err = $_.Exception.Message
Write-Host "`n=== FAILED: $err ===" -ForegroundColor Red
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'
Stop-Transcript | Out-Null
exit 1
} finally {
try { Stop-Transcript | Out-Null } catch {}
}