diff --git a/scripts/windows-host-fallback-backup-daily/README.md b/scripts/windows-host-fallback-backup-daily/README.md new file mode 100644 index 0000000..e9ce3b9 --- /dev/null +++ b/scripts/windows-host-fallback-backup-daily/README.md @@ -0,0 +1,95 @@ +# windows-host-fallback-backup-daily — scripts + +Daily backup `windows-host` (DESKTOP-NSEF0UK) → kreknin Synology (`/volume1/NetBackup/windows-host//`) via rclone SFTP. + +**Purpose:** держать windows-host warm-standby state свежим для DR failover (см. `.tasks/windows-host-fallback-backup-daily.md` §Goal). + +Schedule: daily **03:00 MSK** (раньше RUVDS 04:30 + VDS 05:00 — sequential snapshots). Task name: `WindowsHost-Backup-Daily`. Wake-To-Run enabled чтобы машина просыпалась на backup даже в sleep state. + +## Files + +- `setup.ps1` — one-time install: SSH key, rclone (в `C:\ProgramData\backup\rclone.exe`), configs, ScheduledTask. **Требует elevated PowerShell** (для ScheduledTask SYSTEM + icacls). Параметры `-SmtpPass ` + `-MssqlSaPass `. +- `run.ps1` — backup logic (deployed by setup.ps1 to `C:\ProgramData\backup\run.ps1`). rclone берёт из `$base\rclone.exe`, ssh key из `$base\kreknin-key`. + +## What's backed up (5 components) + +| Component | Source on windows-host | Remote path on kreknin | +|---|---|---| +| MSSQL container | docker exec `BACKUP DATABASE` × 5 DBs → docker cp → sync | `NetBackup/windows-host//mssql/` | +| Sites | `C:\sites\` (snolla 8.66 GB + stostayer + .old + identity-manager) | `NetBackup/windows-host//sites/` | +| MinIO data | `C:\Users\vitya\projects\docker\diskstation\minio\data\` | `NetBackup/windows-host//minio/` | +| Traefik | `C:\Users\vitya\projects\docker\diskstation\traefik\` (config + `acme.json`) | `NetBackup/windows-host//traefik/` | +| IIS config | `applicationHost.config` + `Backup-WebConfiguration` snapshot | `NetBackup/windows-host//iis-config/` + `iis-backup-webconfiguration/` | + +MSSQL DBs: `MoreThenCms`, `StayerCalculator`, `StayerPrice`, `stostayer`, `TireService`. Backup type: FULL daily, `WITH COMPRESSION, INIT`. Backups создаются внутри контейнера в `/var/opt/mssql/backup/`, потом `docker cp` на host в temp dir, потом rclone sync. + +Retention: **7 daily snapshots**, prune step 4. + +## Notifications + +**Dual-channel** (oба после каждого run): + +- **ntfy** topic `vds-backup` (shared с VDS+RUVDS backups) — phone push через ntfy mobile app +- **Email** via Yandex SMTP 587 STARTTLS — `noreply@snolla.com` → `vitya.kuznetsov@gmail.com` + +Subject formatting: `windows-host backup -- SUCCESS / FAILED`. + +Notify-functions wrapped в try/catch — backup itself не fail'ит при notify сбое. + +## Initial install + +```powershell +# На windows-host, PowerShell as Administrator +# (этот repo уже здесь) +cd C:\Users\vitya\projects\.admin\scripts\windows-host-fallback-backup-daily + +# Pull SMTP + SA creds из pass (или передать вручную) +$smtpPass = bash -lc 'pass show snolla-smtp/full-env' | Select-String 'SMTP_PASS=' | %{ ($_ -split '=', 2)[1] } +$saPass = docker inspect mssql --format '{{range .Config.Env}}{{println .}}{{end}}' | Select-String '^MSSQL_SA_PASSWORD=' | %{ ($_ -split '=', 2)[1] } + +.\setup.ps1 -SmtpPass $smtpPass -MssqlSaPass $saPass +``` + +Между шагом 1 и 6 setup паузится — нужно добавить kreknin-key.pub в `/var/services/homes/vitya/.ssh/authorized_keys` на kreknin (через File Station UI, SSH, или **VDS pivot** if local SSH к kreknin не setup): + +```bash +# VDS pivot (если нет direct SSH с этой машины к kreknin): +PUBKEY='' +ssh vitya@89.253.255.94 "ssh -i ~/.ssh/id_ed25519_kreknin vitya@195.19.90.188 'echo \"$PUBKEY\" >> ~/.ssh/authorized_keys'" +``` + +После этого Enter в setup.ps1 → продолжается. + +## Manual smoke run + +```powershell +Start-ScheduledTask -TaskName 'WindowsHost-Backup-Daily' +Get-Content "C:\ProgramData\backup\logs\$(Get-Date -Format yyyy-MM-dd).log" -Wait +``` + +Verify on kreknin (via VDS pivot): +```bash +ssh vitya@89.253.255.94 'ssh -i ~/.ssh/id_ed25519_kreknin vitya@195.19.90.188 "ls -la /volume1/NetBackup/windows-host/$(date +%Y-%m-%d)/; du -sh /volume1/NetBackup/windows-host/$(date +%Y-%m-%d)/*"' +``` + +## Decisions log + +- **rclone, not rsync** — нет cygwin/WSL зависимости, single .exe. Same pattern что VDS msmtp/rsync, RUVDS rclone — proven. +- **Wake-To-Run** — `-WakeToRun` flag на ScheduledTask. windows-host может быть в sleep когда RUVDS уже принимает live traffic; backup всё равно поднимет машину в 03:00 MSK. Critical для warm-standby use-case. +- **MSSQL backup via docker exec** — `sqlcmd -C` (trust cert; sqlcmd 18 enforces TLS даже на localhost) с `BACKUP DATABASE WITH COMPRESSION, INIT, FORMAT` для consistency. Backup .bak пишется в volume `mssql_mssql_data` (`/var/opt/mssql/backup/`), затем `docker cp` на host temp dir → rclone sync → cleanup container .bak. +- **MinIO data bind-mount path** — `C:\Users\vitya\projects\docker\diskstation\minio\data` (verified via `docker inspect minio`). Backup читает напрямую с file system, без MinIO API calls — proven safe для standby (no live mutations). +- **MSSQL_SA_PASS в config.env**, не в pass — windows-host не имеет pass-store setup. ACL config.env = SYSTEM + Administrators only. TODO: setup pass на windows-host для DPAPI-equivalent (long-term). +- **Schedule 03:00 MSK** — sequential с RUVDS (04:30) + VDS (05:00). Все 3 backups на kreknin аккумулируются за одну ночь. + +## Atomic revert (uninstall) + +```powershell +# На windows-host: +Unregister-ScheduledTask -TaskName 'WindowsHost-Backup-Daily' -Confirm:$false +Remove-Item C:\ProgramData\backup -Recurse -Force +# Optionally: Remove-Item 'C:\Program Files\rclone' -Recurse -Force + +# На kreknin (via VDS pivot): +ssh vitya@89.253.255.94 'ssh -i ~/.ssh/id_ed25519_kreknin vitya@195.19.90.188 "rm -rf /volume1/NetBackup/windows-host"' +# Удалить windows-host pubkey-line (с `windows-host-backup` comment) из ~/.ssh/authorized_keys на kreknin. +``` diff --git a/scripts/windows-host-fallback-backup-daily/run.ps1 b/scripts/windows-host-fallback-backup-daily/run.ps1 new file mode 100644 index 0000000..7d41b8a --- /dev/null +++ b/scripts/windows-host-fallback-backup-daily/run.ps1 @@ -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 {} +} diff --git a/scripts/windows-host-fallback-backup-daily/setup.ps1 b/scripts/windows-host-fallback-backup-daily/setup.ps1 new file mode 100644 index 0000000..0234fa8 --- /dev/null +++ b/scripts/windows-host-fallback-backup-daily/setup.ps1 @@ -0,0 +1,128 @@ +#requires -Version 5.1 +#requires -RunAsAdministrator +<# +.SYNOPSIS + One-time setup для windows-host daily fallback backup → kreknin. + Run on windows-host (DESKTOP-NSEF0UK) as Administrator. Idempotent. + +.DESCRIPTION + 1. Generate ed25519 SSH key (C:\ProgramData\backup\kreknin-key). + 2. Install rclone (single .exe). + 3. Write rclone.conf (SFTP remote `kreknin`, no host-key validation). + 4. Write config.env (ntfy + SMTP + MSSQL SA creds; ACL SYSTEM+Administrators). + 5. Deploy run.ps1. + 6. Register ScheduledTask 'WindowsHost-Backup-Daily' daily 03:00 MSK as SYSTEM, + Wake-To-Run enabled чтобы запускался даже если машина в standby. + + User action required between step 1 and 6: добавить kreknin-key.pub + в /volume1/homes/vitya/.ssh/authorized_keys на kreknin Synology. + (Alt: dispatch via VDS pivot — см. .tasks/windows-host-fallback-backup-daily.md) +#> + +[CmdletBinding()] +param( + [string]$NtfyUser = 'vitya', + [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', + [Parameter(Mandatory=$true)][string]$SmtpPass, + [Parameter(Mandatory=$true)][string]$MssqlSaPass, + [string]$OpsNotifyEmail = 'vitya.kuznetsov@gmail.com', + [string]$ScheduleAt = '03:00' +) +$ErrorActionPreference = 'Stop' +$base = 'C:\ProgramData\backup' +New-Item -ItemType Directory -Path $base -Force | Out-Null +# Locale-safe Administrators SID (RU/EN Windows compatible) +$adminSid = '*S-1-5-32-544' + +# 1. SSH key +$keyPath = "$base\kreknin-key" +if (-not (Test-Path $keyPath)) { + ssh-keygen -t ed25519 -f $keyPath -N '""' -C 'windows-host-backup' -q + & cmd.exe /c "icacls `"$keyPath`" /inheritance:r /grant `"SYSTEM:(F)`" /grant `"$adminSid`:(F)`"" | Out-Null + Write-Host "[OK] SSH key generated: $keyPath" + Write-Host "" + Write-Host "ADD THIS PUBKEY TO KREKNIN /var/services/homes/vitya/.ssh/authorized_keys:" + Get-Content "$keyPath.pub" + Write-Host "" + Read-Host "Press Enter after pubkey deployed to kreknin" +} else { + Write-Host "[SKIP] SSH key exists: $keyPath" +} + +# 2. rclone — installed in ProgramData (не Program Files), не нужен elevated install +$rcloneExe = "$base\rclone.exe" +if (-not (Test-Path $rcloneExe)) { + $ver = (Invoke-RestMethod 'https://downloads.rclone.org/version.txt' -UseBasicParsing).Trim() -replace '^rclone\s+v|^v','' + $url = "https://downloads.rclone.org/v$ver/rclone-v$ver-windows-amd64.zip" + $tmp = "$env:TEMP\rclone.zip" + Invoke-WebRequest $url -OutFile $tmp -UseBasicParsing + if (Test-Path "$env:TEMP\rclone") { Remove-Item "$env:TEMP\rclone" -Recurse -Force } + Expand-Archive $tmp -DestinationPath "$env:TEMP\rclone" -Force + Copy-Item "$env:TEMP\rclone\rclone-*-windows-amd64\rclone.exe" $rcloneExe -Force + Write-Host "[OK] rclone installed: v$ver" +} else { + Write-Host "[SKIP] rclone installed: $(& $rcloneExe --version | Select-Object -First 1)" +} + +# 3. rclone.conf (no known_hosts pin — rclone go-sftp doesn't parse keyscan cleanly; key-auth = sufficient trust) +@" +[kreknin] +type = sftp +host = 195.19.90.188 +user = vitya +key_file = $keyPath +disable_hashcheck = true +"@ | Set-Content "$base\rclone.conf" -Encoding ASCII -Force +& cmd.exe /c "icacls `"$base\rclone.conf`" /inheritance:r /grant `"SYSTEM:(F)`" /grant `"$adminSid`:(F)`"" | Out-Null +Write-Host "[OK] rclone.conf written" + +# 4. config.env +@" +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 +MSSQL_SA_PASS=$MssqlSaPass +"@ | Set-Content "$base\config.env" -Encoding ASCII -Force +& cmd.exe /c "icacls `"$base\config.env`" /inheritance:r /grant `"SYSTEM:(F)`" /grant `"$adminSid`:(F)`"" | Out-Null +Write-Host "[OK] config.env written (ntfy + SMTP + MSSQL_SA_PASS)" + +# 5. Deploy run.ps1 +$srcRun = Join-Path $PSScriptRoot 'run.ps1' +if (-not (Test-Path $srcRun)) { throw "run.ps1 not found alongside setup.ps1 ($srcRun)" } +Copy-Item $srcRun "$base\run.ps1" -Force +& cmd.exe /c "icacls `"$base\run.ps1`" /inheritance:r /grant `"SYSTEM:(F)`" /grant `"$adminSid`:(F)`"" | Out-Null +Write-Host "[OK] run.ps1 deployed" + +# 6. Scheduled Task (Wake-To-Run чтобы запускался даже если машина в standby) +$taskName = 'WindowsHost-Backup-Daily' +if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false +} +$action = New-ScheduledTaskAction -Execute 'powershell.exe' ` + -Argument "-NoProfile -ExecutionPolicy Bypass -File $base\run.ps1" +$trigger = New-ScheduledTaskTrigger -Daily -At $ScheduleAt +$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest +$settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries ` + -StartWhenAvailable -MultipleInstances IgnoreNew ` + -WakeToRun ` + -ExecutionTimeLimit (New-TimeSpan -Hours 3) +Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null +Write-Host "[OK] ScheduledTask '$taskName' registered ($ScheduleAt daily, SYSTEM, Wake-To-Run, 3h max)" + +Write-Host "" +Write-Host "=== Setup complete. Smoke: Start-ScheduledTask -TaskName '$taskName' ===" +Write-Host "Log: $base\logs\.log"