scripts(windows-host-fallback-backup-daily): add setup.ps1 + run.ps1 + README
Daily backup windows-host (DESKTOP-NSEF0UK) → kreknin via rclone SFTP. Pattern parallels ruvds-backup-daily-kreknin 🟢. Components (5): - MSSQL container: docker exec BACKUP DATABASE × 5 DBs (MoreThenCms, StayerCalculator, StayerPrice, stostayer, TireService) с COMPRESSION, INIT, FORMAT → docker cp → rclone sync - Sites: C:\sites\ - MinIO data: C:\Users\vitya\projects\docker\diskstation\minio\data - Traefik: C:\Users\vitya\projects\docker\diskstation\traefik\ - IIS config: applicationHost.config + Backup-WebConfiguration export Schedule: daily 03:00 MSK (sequential с RUVDS 04:30 + VDS 05:00). Wake-To-Run enabled — машина просыпается из standby на backup. SYSTEM principal (full access к C:\sites + Cert store + IIS metadata). Retention 7 daily snapshots. Notifications: dual-channel — ntfy `vds-backup` topic (shared) + email via Yandex SMTP 587 STARTTLS, noreply@snolla.com → ops gmail. Same creds как другие backup pipelines. Decisions log в README: - rclone в `C:\ProgramData\backup\rclone.exe` (не Program Files) — избегаем admin requirement на user-context staging. - icacls SID `*S-1-5-32-544` (well-known Administrators) — locale-safe для RU/EN Windows (BUILTIN\Administrators не парсится на ru-locale). - MSSQL backup via sqlcmd 18 с -C (trust cert) — TLS-required даже на localhost в новых mssql tools. - MSSQL_SA_PASS в config.env (windows-host не имеет pass setup); TODO pass-on-Windows long-term. Pre-staging уже сделано (ssh-key + kreknin authorized_keys via VDS pivot, rclone в ProgramData, SFTP smoke OK). setup.ps1 (elevated) пройдёт idempotent через staged steps, only Register-ScheduledTask fresh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
203
scripts/windows-host-fallback-backup-daily/run.ps1
Normal file
203
scripts/windows-host-fallback-backup-daily/run.ps1
Normal file
@@ -0,0 +1,203 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Windows-host fallback backup → kreknin (rclone SFTP).
|
||||
Run via ScheduledTask 'WindowsHost-Backup-Daily' daily 03:00 MSK as SYSTEM.
|
||||
|
||||
.DESCRIPTION
|
||||
Components (5):
|
||||
1. IIS sites: C:\sites\
|
||||
2. MSSQL container: docker exec BACKUP DATABASE x5 -> docker cp -> sync
|
||||
3. MinIO container data: C:\Users\vitya\projects\docker\diskstation\minio\data\
|
||||
4. Traefik config + acme.json: C:\Users\vitya\projects\docker\diskstation\traefik\
|
||||
5. IIS config: applicationHost.config + Backup-WebConfiguration export
|
||||
|
||||
Retention: keep 7 daily snapshots on kreknin (rclone purge step).
|
||||
Notifications: ntfy (vds-backup topic) + email (Yandex SMTP 587 STARTTLS).
|
||||
#>
|
||||
|
||||
[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:\ProgramData\backup\rclone.exe'
|
||||
$rcloneCfg = "$base\rclone.conf"
|
||||
$remoteBase = "kreknin:NetBackup/windows-host"
|
||||
$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
|
||||
|
||||
$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 { 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 }
|
||||
}
|
||||
|
||||
function Invoke-Rclone {
|
||||
param([Parameter(ValueFromRemainingArguments=$true)][string[]]$Args)
|
||||
$prevEAP = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try { return & $rcloneExe @Args 2>&1 }
|
||||
finally { $ErrorActionPreference = $prevEAP }
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host "=== windows-host backup $today started at $start ==="
|
||||
|
||||
Write-Host "`n--- 1. IIS config snapshot ---"
|
||||
Import-Module WebAdministration
|
||||
$iisBackupName = "daily-$today"
|
||||
if (Get-WebConfigurationBackup -Name $iisBackupName -ErrorAction SilentlyContinue) {
|
||||
Remove-WebConfigurationBackup -Name $iisBackupName
|
||||
}
|
||||
$iisBackupDir = "$env:SystemRoot\System32\inetsrv\backup\$iisBackupName"
|
||||
if (Test-Path $iisBackupDir) { Remove-Item $iisBackupDir -Recurse -Force }
|
||||
Backup-WebConfiguration -Name $iisBackupName | Out-Null
|
||||
Write-Host " $iisBackupDir created"
|
||||
|
||||
Write-Host "`n--- 2. MSSQL container backups ---"
|
||||
$mssqlLocalDir = "$base\mssql-$today"
|
||||
if (Test-Path $mssqlLocalDir) { Remove-Item $mssqlLocalDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $mssqlLocalDir -Force | Out-Null
|
||||
$saPass = $cfg.MSSQL_SA_PASS
|
||||
if (-not $saPass) { throw "MSSQL_SA_PASS not in config.env" }
|
||||
$dbs = @('MoreThenCms','StayerCalculator','StayerPrice','stostayer','TireService')
|
||||
foreach ($db in $dbs) {
|
||||
Write-Host " BACKUP DATABASE [$db]"
|
||||
$bak = "$db-$today.bak"
|
||||
$sql = "BACKUP DATABASE [$db] TO DISK = N'/var/opt/mssql/backup/$bak' WITH COMPRESSION, INIT, FORMAT, NAME = '$db daily $today'"
|
||||
$prevEAP = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$out = & docker exec mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $saPass -C -Q $sql -b 2>&1
|
||||
$rc = $LASTEXITCODE
|
||||
$ErrorActionPreference = $prevEAP
|
||||
if ($rc -ne 0) { throw "BACKUP $db failed (exit $rc): $out" }
|
||||
& docker cp "mssql:/var/opt/mssql/backup/$bak" "$mssqlLocalDir\$bak" 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "docker cp $bak failed" }
|
||||
& docker exec mssql rm -f "/var/opt/mssql/backup/$bak" 2>&1 | Out-Null
|
||||
$sz = [math]::Round((Get-Item "$mssqlLocalDir\$bak").Length / 1MB, 1)
|
||||
Write-Host " -> $bak ($sz MB)"
|
||||
}
|
||||
|
||||
Write-Host "`n--- 3. rclone sync (5 components) ---"
|
||||
$rcCommon = @('--config', $rcloneCfg, '--transfers', '4', '--checkers', '8', '--stats=0')
|
||||
|
||||
Invoke-Rclone sync $mssqlLocalDir "$remoteToday/mssql/" @rcCommon | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "rclone sync mssql failed (exit $LASTEXITCODE)" }
|
||||
Write-Host " mssql backups synced"
|
||||
|
||||
Invoke-Rclone sync 'C:\sites' "$remoteToday/sites/" @rcCommon | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "rclone sync sites failed (exit $LASTEXITCODE)" }
|
||||
Write-Host " sites synced"
|
||||
|
||||
Invoke-Rclone sync 'C:\Users\vitya\projects\docker\diskstation\minio\data' "$remoteToday/minio/" @rcCommon | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "rclone sync minio failed (exit $LASTEXITCODE)" }
|
||||
Write-Host " minio synced"
|
||||
|
||||
Invoke-Rclone sync 'C:\Users\vitya\projects\docker\diskstation\traefik' "$remoteToday/traefik/" @rcCommon | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "rclone sync traefik failed (exit $LASTEXITCODE)" }
|
||||
Write-Host " traefik 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" }
|
||||
Invoke-Rclone sync $iisBackupDir "$remoteToday/iis-backup-webconfiguration/" @rcCommon | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "rclone sync iis-backup-webconfiguration failed" }
|
||||
Write-Host " iis-config + iis-backup-webconfiguration 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 $mssqlLocalDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$duration = [int](New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds
|
||||
$sitesBytes = (Get-ChildItem 'C:\sites' -Recurse -File -EA SilentlyContinue | Measure-Object Length -Sum).Sum
|
||||
$minioBytes = (Get-ChildItem 'C:\Users\vitya\projects\docker\diskstation\minio\data' -Recurse -File -EA SilentlyContinue | Measure-Object Length -Sum).Sum
|
||||
$totalGB = '{0:N2}' -f (($sitesBytes + $minioBytes) / 1GB)
|
||||
|
||||
$msg = "windows-host daily backup $today OK | $duration sec | $totalGB GB (sites+minio) | 5 DBs"
|
||||
Notify-Ntfy "windows-host backup OK ($today)" $msg 'default' 'green_circle'
|
||||
$emailBody = @"
|
||||
windows-host (DESKTOP-NSEF0UK) daily backup completed successfully.
|
||||
|
||||
Date: $today
|
||||
Duration: $duration seconds
|
||||
Size: $totalGB GB (sites+minio data; mssql .bak compressed separately)
|
||||
Source: windows-host (DESKTOP-NSEF0UK / 94.19.247.14)
|
||||
Dest: kreknin:/volume1/NetBackup/windows-host/$today/
|
||||
|
||||
Components synced:
|
||||
- mssql (5 DBs: MoreThenCms, StayerCalculator, StayerPrice, stostayer, TireService)
|
||||
- sites (C:\sites\)
|
||||
- minio (data dir)
|
||||
- traefik (config + acme.json)
|
||||
- iis-config (applicationHost.config) + iis-backup-webconfiguration
|
||||
|
||||
Log: $logFile
|
||||
"@
|
||||
Notify-Email "windows-host backup $today -- SUCCESS" $emailBody
|
||||
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 "windows-host backup FAILED ($today)" "After $duration sec: $err" 'high' 'red_circle'
|
||||
Notify-Email "windows-host backup $today -- FAILED" "After $duration sec: $err`n`nLog: $logFile"
|
||||
Stop-Transcript | Out-Null
|
||||
exit 1
|
||||
} finally {
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
}
|
||||
Reference in New Issue
Block a user