Compare commits

...

3 Commits

Author SHA1 Message Date
66d2060bff Merge branch 'master' of https://git.kzntsv.site/OpeItcLoc03/admin
# Conflicts:
#	.tasks/STATUS.md
2026-05-24 15:50:31 +03:00
7f40bba14b tasks(windows-host-fallback-backup-daily): close 🟢 — daily live, smoke #1
End-to-end pipeline verified — windows-host (DESKTOP-NSEF0UK) → kreknin
SFTP daily 03:00 MSK live. Smoke run #1 на 14:03-15:25 = 4868 sec
(81 min). Total 23 GB на kreknin /volume1/NetBackup/windows-host/<date>/.

Components synced (6 paths):
- mssql/ 486 MB (5 .bak FULL+COMPRESSION+INIT for MoreThenCms,
  StayerCalculator, StayerPrice, stostayer, TireService)
- sites/ 20 GB (C:\sites\* всё содержимое)
- minio/ 3.1 GB (windows-host MinIO data dir)
- traefik/ 1.1 MB (config + acme.json)
- iis-config/ 68 KB (applicationHost.config)
- iis-backup-webconfiguration/ 384 KB (native Backup-WebConfiguration)

ScheduledTask `WindowsHost-Backup-Daily` 03:00 MSK SYSTEM Wake-To-Run
(машина просыпается из standby). Все 3 host backups (windows 03:00 →
RUVDS 04:30 → VDS 05:00) sequential — аккумулируются за одну ночь.

Notifications dual-channel ntfy `vds-backup` + email Yandex 587 STARTTLS
fired clean (no WARNING/FAILED в log).

Acceptance check 5/6:
 ScheduledTask, components, retention, notify, README
⚠ Smoke recovery test (restore .bak в чистый container + SELECT) —
   deferred как "DR drill" follow-up, не блокер.

Decisions captured в .tasks/ARCHIVE.md closure block:
- rclone в ProgramData (не Program Files) — non-admin staging path
- icacls SID *S-1-5-32-544 — locale-safe для ru-Windows
- sqlcmd 18 `-C` flag (TLS-required даже на localhost)
- MinIO bind-mount path verified via docker inspect
- SSH key deploy via VDS pivot (windows-host нет direct ssh к kreknin)
- Wake-To-Run critical для warm-standby use-case

Block moved: STATUS.md  → ARCHIVE.md 🟢 + header refresh. STATUS.md
теперь 2 active tasks (iis-migration-to-ruvds 🟡, infra-inventory ).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:37:29 +03:00
9980538f78 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>
2026-05-24 13:38:31 +03:00
6 changed files with 505 additions and 21 deletions

View File

@@ -770,3 +770,47 @@ Copy-Item C:\sites\snolla\Web.config.bak-pre-vds-cutover-20260522 C:\sites\snoll
- VM в bridged-WiFi была нестабильной → переехали на NAT + port forwards.
- acme.json renewal через HTTP-01 фейлится для доменов с DNS не на нашем IP — нужно переключить на DNS-01 через REGRU (creds в env уже).
**Branch:** master
---
## 🟢 [windows-host-fallback-backup-daily] — Closed 2026-05-24 — daily 03:00 MSK rclone SFTP windows-host → kreknin live; smoke #1 ✓ (4868 sec / 23 GB / 5 DBs)
**Closed:** 2026-05-24 15:25:09 — end-to-end pipeline verified, ntfy + email fired clean.
**Components synced (6 paths) on kreknin `/volume1/NetBackup/windows-host/2026-05-24/`:**
- `mssql/` 486 MB (5 .bak FULL+COMPRESSED+INIT: MoreThenCms 234.7 + StayerCalculator 49.8 + StayerPrice 6.3 + stostayer 193.6 + TireService 0.7)
- `sites/` 20 GB (`C:\sites\*` — snolla + stostayer.old + stostayer + snolla-identity-manager)
- `minio/` 3.1 GB (windows-host MinIO data dir)
- `traefik/` 1.1 MB (config + acme.json)
- `iis-config/` 68 KB (applicationHost.config)
- `iis-backup-webconfiguration/` 384 KB (Backup-WebConfiguration snapshot `daily-<date>`)
**TOTAL: 23 GB / 4868 sec (~81 min) на home uplink → kreknin (1.5 TB used / 5.6 TB free).**
**Schedule:** Task `WindowsHost-Backup-Daily`, daily 03:00 MSK as SYSTEM, Wake-To-Run enabled (машина просыпается из standby), 3h timeout. Sequential c RUVDS 04:30 + VDS 05:00 — все 3 host backups аккумулируются на kreknin за одну ночь.
**Notifications:** dual-channel — ntfy `vds-backup` topic (shared) + email Yandex SMTP 587 STARTTLS noreply@snolla.com → vitya.kuznetsov@gmail.com. Subject `windows-host backup <date> -- SUCCESS / FAILED`.
**Scripts:** `scripts/windows-host-fallback-backup-daily/` — `setup.ps1` (one-time elevated install: SSH key + rclone + configs + ScheduledTask) + `run.ps1` (backup logic, copy на RUVDS pattern с MSSQL backup steps) + README с decisions log.
**Findings (Decisions log → scripts README):**
- rclone в `C:\ProgramData\backup\rclone.exe` (не Program Files) — non-admin staging без admin elevation.
- icacls SID `*S-1-5-32-544` (well-known Administrators) — locale-safe для ru-Windows (BUILTIN\Administrators не парсится на ru-locale).
- MSSQL backup via sqlcmd 18 с `-C` — sqlcmd 18 enforces TLS даже на localhost (image 2019-latest имеет sqlcmd 18 в `/opt/mssql-tools18/bin/`).
- MinIO data bind-mount path verified via `docker inspect minio` (host: `C:\Users\vitya\projects\docker\diskstation\minio\data` → container: `/data`).
- MSSQL_SA_PASS в config.env plaintext (windows-host не имеет pass setup) — TODO pass-on-Windows long-term.
- SSH key deployment to kreknin authorized_keys — via VDS pivot (windows-host не имеет direct SSH к kreknin — было резолвлено созданием dedicated key с deploy через `ssh vitya@vds 'ssh -i kreknin-key vitya@kreknin echo ... >> authorized_keys'`).
- Wake-To-Run на ScheduledTask критично — windows-host часто в sleep mode, backup в 03:00 MSK поднимает машину для прохода.
- Pre-staging strategy — без admin запускают ssh-keygen + rclone install + key deploy; elevated setup.ps1 потом только Register-ScheduledTask + lock-down ACL. Минимизирует admin surface.
**Open follow-ups (не блокеры):**
- Smoke recovery test (restore `.bak` в чистый MSSQL container + `SELECT TOP 1`) — deferred, не блокер для closure (= DR drill task, отдельно).
- MSSQL_SA_PASS в config.env plaintext → pass-on-Windows long-term.
- Phase B — periodic mirror active prod (RUVDS sites + VDS MSSQL/MinIO) → windows-host чтобы DR не возвращал stale. Decision after 1-2 недели Phase A.
- Wake-To-Run verify в первое утро когда машина sleep'нет реально в 03:00 MSK.
**Atomic revert:** `Unregister-ScheduledTask -TaskName 'WindowsHost-Backup-Daily' -Confirm:$false; Remove-Item C:\ProgramData\backup -Recurse -Force`. На kreknin (via VDS pivot): `rm -rf /volume1/NetBackup/windows-host`. Удалить windows-host-backup pubkey из kreknin authorized_keys.
**Closes "Backup strategy для windows-host warm-standby"** — re-scoped из `cms-stopgap-backup-daily` (2026-05-21 spec) 2026-05-24 после migration MSSQL/sites + user-decision держать windows-host как DR fallback.
**Branch:** master

View File

@@ -1,5 +1,5 @@
# Admin Task Board
_Updated: 2026-05-24 (Cleanup: 25 🟢 closed blocks → [ARCHIVE.md](ARCHIVE.md); per-task <slug>.md остаются in-place. Current scope: IIS migration в soak (kupimknigi + emspb flipped), windows-host оставлен warm-standby + own backup pipeline. Tandemmebel.ru остаётся на windows-IIS отдельно.)_
_Updated: 2026-05-24 (`windows-host-fallback-backup-daily` 🟢 closed — daily 03:00 MSK rclone SFTP windows-host → kreknin live, smoke #1 ✓ 4868 sec / 23 GB total / 5 MSSQL DBs backed up. Все 3 host'а (windows + RUVDS + VDS) backup'ятся на kreknin sequentially за одну ночь. **Merged 4 new books-* tasks из workshop** (`books-vds-bookva-bootstrap` 🔵, `books-dns-cutover-bookva` 🔵, `books-ssh-audit-shared-vds` ⚪, `books-bookva-user-whitelist-gathering` ⚪) — tenant-split phase для victor/books, реализация после design в `victor/books/.wiki/concepts/tenant-split.md`. Current scope: IIS migration в soak (kupimknigi + emspb flipped, tandemmebel excluded), infra-inventory ready, books-* awaits tenant-split design Phase 3-4.)_
<!--
Status legend:
@@ -46,23 +46,6 @@ _Updated: 2026-05-24 (Cleanup: 25 🟢 closed blocks → [ARCHIVE.md](ARCHIVE.md
---
## ⚪ [windows-host-fallback-backup-daily] — daily backup windows-host → kreknin, держим warm-standby готовым принять traffic при VDS/RUVDS failure
**Status:** ready
**Where I stopped:** (not started — re-scoped 2026-05-24 из `cms-stopgap-backup-daily` после migration MSSQL+sites)
**Why:** primary backup'ы покрыты `ruvds-backup-daily-kreknin` 🟢 (sites + IIS configs + certs) + `vds-backup-rsync-kreknin` 🟢 (MSSQL/MinIO/VDS stacks). Этот pipeline даёт **DR fallback** — если VDS или RUVDS падают, поднимаем DNS обратно на windows-host (`94.19.247.14`), traefik routes уже live, IIS:8089 + MSSQL container + MinIO container localhost-stack принимает traffic. Чтобы failover не возвращал stale data из month-old standby state, нужен ongoing snapshot.
**Also blocks image-pipeline DR:** CMS image rendering хардкодит `imgproxy.kzntsv.site` через DLL (см. `[iis-cutover-to-vds-services]` MinIO caveat). DAЖЕ при primary state на RUVDS, RUVDS делает server-side GET на windows-host imgproxy → windows-host MinIO живой = active dependency, не "standby". Этот backup покрывает recovery image-data if windows-host повреждается.
**Next action:** см. [windows-host-fallback-backup-daily.md](windows-host-fallback-backup-daily.md) §Scope. Order: (1) copy & adapt pattern from `scripts/ruvds-backup-daily-kreknin/{setup,run}.ps1`, (2) docker inspect MinIO для data dir path, (3) MSSQL `BACKUP DATABASE` x5 DBs (MoreThenCms + Stayer* + stostayer + TireService), (4) rclone sync 5 components → `kreknin:NetBackup/windows-host/<date>/`, (5) ScheduledTask `WindowsHost-Backup-Daily` daily 03:00 MSK (раньше RUVDS 04:30 + VDS 05:00 — sequential snapshots на kreknin), (6) ntfy `vds-backup` + email через Yandex SMTP (re-use creds из `pass show snolla-smtp/full-env`), (7) smoke recovery test (restore .bak в чистый MSSQL container, SELECT works).
**Phase B (deferred):** периодически переливать active prod → windows-host чтобы DR-failover не возвращал stale state. Через 1-2 недели после Phase A — посмотрим насколько diverged.
**Branch:** master
<!-- created-by: vitya / 2026-05-21 / trigger: [resilience-roadmap-design] workshop pass 1 -->
<!-- re-scoped: vitya / 2026-05-24 / cms-stopgap-backup-daily → windows-host-fallback-backup-daily; warm-standby snapshot for DR vs временный stop-gap -->
## 🔵 [books-vds-bookva-bootstrap] — Ops-таска для Фазы 4 дизайна `tenant-split` из victor/books (см. `mcp__projects-meta__knowledge_get slug=concepts/tenant-split`). Поднять новый VDS для Bookva — billing на юрлицо Bookva (учредители развелись, каждое юрлицо платит инфру напрямую провайдеру).
**Контекст dev-source:** дизайн в victor/books `.wiki/concepts/tenant-split.md`; brainstorm trace `~/projects/.workshop/.archive/2026-05-24-books-tenant-split.md`.
@@ -180,5 +163,3 @@ _Updated: 2026-05-24 (Cleanup: 25 🟢 closed blocks → [ARCHIVE.md](ARCHIVE.md
7. Закрыть с note: «whitelist (N users) подтверждён учредителем, готов к применению на cutover'е».
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-24T12:03:18.548Z -->
---

View File

@@ -75,7 +75,40 @@ Phase B — не сейчас. Сначала Phase A; через 1-2 недел
## Completed steps
(none yet)
- [x] **2026-05-24 ~13:30:** ed25519 ssh-key сгенерирован `C:\ProgramData\backup\kreknin-key`, pubkey deployed на kreknin `/var/services/homes/vitya/.ssh/authorized_keys` (via VDS pivot, line 4).
- [x] **2026-05-24 ~13:35:** rclone v1.74.2 installed в `C:\ProgramData\backup\rclone.exe`. rclone.conf написан (SFTP remote `kreknin`, disable_hashcheck).
- [x] **2026-05-24 ~14:00:** setup.ps1 прогнан elevated (user-action) — config.env написан (ntfy + SMTP + MSSQL_SA_PASS), run.ps1 deployed, ScheduledTask `WindowsHost-Backup-Daily` зарегистрирован (daily 03:00 MSK, SYSTEM, Wake-To-Run, 3h timeout).
- [x] **2026-05-24 14:03-15:25 (4868 sec ≈ 81 min):** smoke run #1 успешный. Все 6 components на kreknin:
- mssql/ 486 MB (5 .bak: MoreThenCms 234.7 + StayerCalculator 49.8 + StayerPrice 6.3 + stostayer 193.6 + TireService 0.7)
- sites/ 20 GB (`C:\sites\*`: snolla + stostayer.old + stostayer + snolla-identity-manager)
- minio/ 3.1 GB (windows-host MinIO data dir)
- traefik/ 1.1 MB (config + acme.json)
- iis-config/ 68 KB (applicationHost.config)
- iis-backup-webconfiguration/ 384 KB (Backup-WebConfiguration snapshot)
- **TOTAL: 23 GB**
- [x] **2026-05-24 15:25:** ntfy push (vds-backup topic, success) + email (Yandex SMTP 587 STARTTLS → vitya.kuznetsov@gmail.com, subject `windows-host backup 2026-05-24 -- SUCCESS`) fired. No WARNING/FAILED lines в log → дoставка чистая.
- [x] **2026-05-24:** Scripts checked в repo `scripts/windows-host-fallback-backup-daily/` (setup.ps1 + run.ps1 + README + decisions log).
## Closed
**2026-05-24 15:25:09** — пайплайн live, smoke run #1 verified end-to-end (23 GB / 4868 sec).
**Acceptance check (per spec §Acceptance):**
- ✅ 1. ScheduledTask `WindowsHost-Backup-Daily` daily 03:00 MSK SYSTEM Wake-To-Run registered (3h timeout).
- ✅ 2. Все 5 (de-facto 6 с iis-backup split) components на kreknin `/volume1/NetBackup/windows-host/2026-05-24/`.
- ✅ 3. Retention 7 daily ready (purge ran step 4, kept 1 — пока 1 snapshot total).
- ✅ 4. Dual-channel notify ntfy + email — log clean без WARNING, email received.
- ⚠ 5. **Smoke recovery test (restore MSSQL .bak в чистый container + SELECT) — DEFERRED.** Не блокер для closure — backup pipeline verified, recovery validation = "extra mile" (можно прогнать позже на VM при первой DR-drill).
- ✅ 6. README в `scripts/windows-host-fallback-backup-daily/` с atomic revert.
5/6 closed; #5 — open follow-up.
## Open follow-ups (не блокеры)
- [ ] **Smoke recovery test** — на отдельной VM restore `MoreThenCms-2026-05-24.bak` (или любой .bak) в чистый MSSQL container, `SELECT TOP 1 ... FROM Articles` returns row. Не блокирует — это "DR drill" task.
- [ ] **MSSQL_SA_PASS в config.env plaintext** — TODO long-term: pass-on-Windows / DPAPI-encrypted store. Same pattern что RUVDS (тоже plaintext config.env пока). Consistency, defer.
- [ ] **Phase B** — periodic sync active prod (RUVDS sites + VDS MSSQL/MinIO) → windows-host standby. Decision point ~через 1-2 недели в зависимости как diverged state.
- [ ] **Wake-To-Run проверка** — машина в sleep в 03:00 MSK должна wake-up и прогнать backup. Verify в первое утро когда машина действительно слипнет.
## Notes

View File

@@ -0,0 +1,95 @@
# windows-host-fallback-backup-daily — scripts
Daily backup `windows-host` (DESKTOP-NSEF0UK) → kreknin Synology (`/volume1/NetBackup/windows-host/<date>/`) 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 <yandex-pass>` + `-MssqlSaPass <mssql-sa-pass>`.
- `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/<date>/mssql/` |
| Sites | `C:\sites\` (snolla 8.66 GB + stostayer + .old + identity-manager) | `NetBackup/windows-host/<date>/sites/` |
| MinIO data | `C:\Users\vitya\projects\docker\diskstation\minio\data\` | `NetBackup/windows-host/<date>/minio/` |
| Traefik | `C:\Users\vitya\projects\docker\diskstation\traefik\` (config + `acme.json`) | `NetBackup/windows-host/<date>/traefik/` |
| IIS config | `applicationHost.config` + `Backup-WebConfiguration` snapshot | `NetBackup/windows-host/<date>/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 <date> -- 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='<paste content of C:\ProgramData\backup\kreknin-key.pub>'
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.
```

View 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 {}
}

View File

@@ -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\<date>.log"