diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index ea153a2..05cb548 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -1,5 +1,5 @@ # Admin Task Board -_Updated: 2026-05-23 (Session-recovery: temp admin начал `[iis-migration-to-ruvds]` 2026-05-23 (09:37 creds в `.secrets/` plaintext, 18:01 size probe, 18:08 robocopy upal exit 16 «сетевой путь не найден»), drop'нул tree dirty. Восстановлено: creds → `pass show ruvds-iis/full-env`, `.secrets/` removed + gitignored, root-cause SMB-445 closed на fresh Win Server 2025 Core зафиксирован в [`windows-server-2025-core-bootstrap`](../.wiki/concepts/windows-server-2025-core-bootstrap.md), task ⚪ → 🟡 paused с concrete next-step. Previous session (2026-05-22): MSSQL Express 2022 live (`mssql.kzntsv.site:1433`, 5 DBs, 8 prod hosts 200 OK). MinIO/imgproxy VDS stack live as standby (CMS остаётся на windows из-за hardcoded URL в closed-source DLL). board-viewer-vds-deploy ✓ (board.kzntsv.site + basicauth). Portainer-migration sweep: 12 ad-hoc stacks → Portainer-managed.)_ +_Updated: 2026-05-24 (IIS migration to RUVDS — **partial cutover live**: snolla site (8.66 GB / 44725 files) transferred via scp (после ISP outbound-445 block обнаружен), IIS recreated, 25 HTTPS SNI bindings с LE certs (re-used from traefik acme.json), 7 prod hostnames live-smoked через VDS (третья сеть) → 200 OK + correct content. DNS A для kupimknigi.spb.ru flipped на 80.64.31.36 (после initial misroute на VDS). Soak window: 24h+. Source IIS:8089 + traefik routes ALIVE — rollback ready. Previous: temp admin start 2026-05-23 09:37-18:08, dropped на UNC-robocopy exit 16; secrets-leak в `.secrets/` fixed → `pass`. См. также 2026-05-22 chain: MSSQL Express 2022 live на `mssql.kzntsv.site:1433` (5 DBs, 8 prod hosts 200 OK), MinIO/imgproxy VDS standby.)_ - + --- diff --git a/.tasks/iis-migration-to-ruvds.md b/.tasks/iis-migration-to-ruvds.md index cc29d5b..719e99f 100644 --- a/.tasks/iis-migration-to-ruvds.md +++ b/.tasks/iis-migration-to-ruvds.md @@ -2,92 +2,106 @@ ## Goal -Migrate 11 IIS sites from [[../entities/windows-recovery-host]] (DESKTOP-NSEF0UK) to RUVDS Windows Server 2025 Core. Убрать SPOF домашней машины. +Migrate IIS hosting from [[../entities/windows-recovery-host]] (DESKTOP-NSEF0UK) to RUVDS Windows Server 2025 Core (`80.64.31.36`). Убрать SPOF домашней машины. + +**Scope finalize 2026-05-24:** только `snolla` IIS site (catch-all для ~26 hostnames через CMS multi-tenant routing) — 8.66 GB. `stostayer` уже на external MSSQL (не наш scope). `stostayer.old` local-only (defer). `snolla-identity-manager` dead conn-string (defer). См. "Scope" ниже. **Pre-requisites done:** - MSSQL+MinIO уже на [[../entities/vds-kzntsv]] (см [[mssql-minio-migration-to-vds]]) - RUVDS purchased: Windows Server 2025 Core, 2GB RAM, 30GB HDD, 1IP, DC Королёв - RDP ready (creds — `pass show ruvds-iis/full-env`, public IP `80.64.31.36`) +## Scope + +**1 IIS site → 25 HTTPS hostnames bound via SNI:** + +| Hostname | Note | +|---|---| +| kupimknigi.spb.ru | ⚡ pilot, DNS flipped 2026-05-24 | +| emspb.ru / www.emspb.ru | ✅ ready | +| pilorama98.ru / www.pilorama98.ru | ✅ ready | +| labtools.ru / www.labtools.ru | ✅ ready | +| labtools.pro / www.labtools.pro | ✅ ready | +| tandemmebel.ru / www.tandemmebel.ru | ✅ ready | +| snolla.com / on.snolla.com / ics-artmaterials.snolla.com / pilorama98.snolla.com | ✅ ready | +| labtools.snolla.com / emspb.snolla.com / tandemmebel.snolla.com | ✅ ready | +| sestech.snolla.com / labtoolspro.snolla.com / artmone.snolla.com | ✅ ready | +| rimiz.ru / www.rimiz.ru / rimiz.snolla.com | ⚠ CMS-side 502/404, не migration defect — degraded pre-cutover [[#degraded-tenants]] | +| maljarka.tandemmebel.ru | ⚠ CMS-side 502 (см. выше) | +| 1 catch-all `*:80:` HTTP binding | retained для legacy/diagnostics | + +Все 25 HTTPS bindings связаны с LE certs (R13, выпущены 2026-04-23, valid до 2026-07-22). + ## Key files -- `C:\sites\snolla\` — 8.66 GB source (samples sites), измерено 2026-05-23 -- `C:\sites\<11 sites>` — полный inetpub root (по `recovery-architecture-snapshot`) -- `C:\Windows\System32\inetsrv\config\applicationHost.config` — source IIS state -- `[[../.wiki/concepts/windows-server-2025-core-bootstrap]]` — bootstrap-чеклист + transfer-методов матрица (ingested 2026-05-23 после failed-robocopy) -- `pass show ruvds-iis/full-env` — RDP creds - -## Migration approach - -1. **RDP-first setup** (RUVDS) — см. `concepts/windows-server-2025-core-bootstrap.md` §Bootstrap-чеклист: - - Install IIS (`Install-WindowsFeature Web-Server -IncludeAllSubFeature -IncludeManagementTools`) - - Verify .NET Framework 4.8 (Release ≥ 528040) - - Install URL Rewrite 2.1 - - Open FW 80/443 - - Test external MSSQL connection to VDS (`mssql.kzntsv.site:1433`) - - Test external MinIO connection (если CMS pipeline переезжает — см. caveat §MinIO ниже) - -2. **Open SMB inbound на время migration** (см. transfer-методов матрица — это recommended choice): - - `Set-NetFirewallRule -DisplayGroup "File and Printer Sharing" -Enabled True` - - `New-NetFirewallRule -DisplayName "SMB-from-source" -Direction Inbound -Protocol TCP -LocalPort 445 -RemoteAddress -Action Allow` - - `New-Item -ItemType Directory -Path C:\sites -Force` - - `New-SmbShare -Name sites -Path C:\sites -FullAccess Administrator` - -3. **Backup source** (windows-recovery-host): - - Export IIS configuration: `appcmd list site -config > sites-backup.txt` - - Export applicationHost.config: `C:\Windows\System32\inetsrv\config\applicationHost.config` - - Backup inetpub\wwwroot\ (11 sites, ~8.66 GB на snolla одной, итого предположительно ~30-50 GB total) - - Document Web.config connection strings (MSSQL/MinIO endpoints) - -4. **Deploy to target** (RUVDS) — через SMB robocopy: - - `net use \\\sites /user:Administrator ` - - `robocopy C:\sites\ \\\sites\ *.* /S /E /DCOPY:DA /COPY:DAT /Z /MT:8 /R:2 /W:5` per site - - Recreate sites через `appcmd add site` или `New-IISSite`, копировать bindings - - Update Web.config connection strings → `Data Source=mssql.kzntsv.site,1433;TrustServerCertificate=True` (matches `[mssql-vds-migration]` pattern) - - Import SSL certificates (traefik LE certs) или setup new LE через `win-acme` (wacs.exe) - -5. **Pilot on kupimknigi.spb.ru** (low-traffic): - - DNS pointing test (local hosts file или temporary DNS) - - Smoke test: homepage + admin + database connectivity - - 24h soak — monitor stability (включая memory pressure — 2GB tight) - - Если OK → proceed с remaining 10 sites - -6. **DNS swap cutover**: - - Update DNS A records → RUVDS IP (`80.64.31.36`) - - Monitor traefik logs на windows-recovery-host (должно увидеть 0 traffic) - - Keep windows-recovery-host warm для 1 week rollback window - - **Cleanup на RUVDS:** `Remove-SmbShare -Name sites` + `Remove-NetFirewallRule -DisplayName "SMB-from-source"` +- `C:\sites\snolla\` — 8.66 GB / 44725 files (source on windows-recovery-host) +- `\\80.64.31.36\C$\sites\snolla\` — destination (transferred 2026-05-23 23:08-23:33 via scp) +- `C:\Users\vitya\iis-backup-pre-ruvds\scp-snolla.log` — scp transcript +- `C:\ProgramData\ssh\administrators_authorized_keys` (RUVDS) — pubkey deployed for transfer +- `~/.ssh/ruvds-iis-migration` / `.pub` (source) — temp key pair (clean up post-cutover) +- `pass show ruvds-iis/full-env` — RDP creds (canonical secret store) +- `scripts/iis-migration-to-ruvds/01-ruvds-bootstrap.ps1` — RUVDS bootstrap (idempotent) +- `[[../.wiki/concepts/windows-server-2025-core-bootstrap]]` — bootstrap recipe (SMB section deprecated, см. Decisions log 2026-05-23 23:00) ## Decisions log -- **2026-05-23 09:37 (admin attempt):** RDP creds сохранены в `.secrets/ruvds-iis.env` (plaintext, в repo tree → нарушение etap-2 secrets-discipline, ретроспективно зафикшено в текущей сессии: → `pass show ruvds-iis/full-env`). -- **2026-05-23 18:01 (admin attempt):** замерен размер `C:\sites\snolla\` = 8871 MB ≈ 8.66 GB. Источник для capacity planning (snolla alone 8.66 GB → 11 sites суммарно может быть 30-50 GB → 30 GB HDD на RUVDS **tight**, потенциальный capacity blocker, см. Open questions). -- **2026-05-23 18:08 (admin attempt):** robocopy `C:\sites\snolla\` → `\\80.64.31.36\sites\snolla\` упал exit 16 «сетевой путь не найден». Root cause (post-mortem из текущей сессии): TCP/445 closed по дефолту на Win Server 2025 Core + SMB share `sites` не создан. UNC robocopy как первый transfer-метод **не работает** на fresh Core без подготовки RUVDS-стороны. -- **2026-05-23 (current session) recommendation:** SMB inbound с source-IP whitelist — самый быстрый transfer для 11 sites × ~1 GB, native robocopy /Z поддерживает interrupt-resume, FW rule убирается после cutover. Полная матрица альтернатив (RDP-redirect / WinRM / SFTP) в `concepts/windows-server-2025-core-bootstrap.md` §Transfer-методов. -- **MinIO pipeline caveat (из `[iis-cutover-to-vds-services]` 2026-05-22):** CMS image pipeline остаётся целиком на windows-recovery-host из-за hardcoded URL в `MoreThenCms.Modules.Imgproxy.dll`. RUVDS-IIS будет coupled к windows-host imgproxy через DNS `imgproxy.kzntsv.site` — то есть RUVDS IIS вызывает server-side GET на windows-host'овский imgproxy. Это **не убирает SPOF домашней машины** для image-rendering pipeline. Если windows-host умрёт, image-URLs broken. Open question для post-migration phase: decompile DLL или Option B (local nginx-relay). +- **2026-05-23 09:37 (admin attempt):** RDP creds сохранены в `.secrets/ruvds-iis.env` plaintext, в repo tree → нарушение etap-2 secrets-discipline. Ретроспективно вынесены в `pass show ruvds-iis/full-env`, `.secrets/` + `*.env` + `*-log.txt` + `*-size.txt` добавлены в `.gitignore`. +- **2026-05-23 18:08 (admin attempt):** robocopy через UNC `\\80.64.31.36\sites\snolla\` упал exit 16. Initial root cause: SMB-share не создан + FW 445 не открыт на fresh Win Server 2025 Core. Зафиксировано в `windows-server-2025-core-bootstrap.md`. +- **2026-05-23 22:30 (session-recovery transfer attempt):** После bootstrap'а (FW+share созданы) SMB всё равно не reachable — TCP/445 не выходит **с home-ISP**. **Real root cause: outbound 445 блокирует ISP (стандартная анти-worm политика residential провайдеров RU). FW scoping на RUVDS-стороне корректен.** SMB recommendation в bootstrap-концепте **deprecated** — SSH/scp = canonical transfer-метод. +- **2026-05-23 22:35 (transfer):** OpenSSH.Server на RUVDS уже был installed (admin'ом раньше), sshd running. Открыл FW 22 scoped к source IP, deployed ed25519 pubkey в `C:\ProgramData\ssh\administrators_authorized_keys` (с правильным ACL — SYSTEM + Administrators only). scp -r `C:\sites\snolla` → `C:/sites/` отработал за ~25 мин (8.66 GB / 5 MB/s home uplink), exit 0, count+size MATCH (44725 files / 9302398880 bytes). +- **2026-05-23 23:18 (IIS recreate):** Default Web Site удалён. AppPool `snolla` (.NET v4.0, Integrated, ApplicationPoolIdentity), Website `snolla` с physicalPath `C:\sites\snolla` + catch-all binding `*:80:`. ACL — `IIS APPPOOL\snolla` + `IIS_IUSRS` Read на 46150 file entries. Local smoke с loopback `localhost` + `Host: kupimknigi.spb.ru` → 200 OK + правильный title. +- **2026-05-23 23:25 (external smoke through home network):** все 8 hostnames вернули "SNOLLA | Cloud CMS" default (28406 bytes), **не** per-tenant content. Внутри RUVDS (loopback) — корректный per-tenant content. Difference: external requests **из home network** теряют Host header через HTTP-aware middlebox (DPI/transparent proxy в OpenWRT/ISP path). SSH tunnel localhost:8888→RUVDS:80 → correct content. Тест из VDS (другая сеть) → correct content. **Conclusion: home-side outbound HTTP mutation; real end-users из других сетей не пострадают.** +- **2026-05-23 23:30 (HTTPS bindings):** Экспортировал 14 LE certs из traefik `acme.json` (`C:\Users\vitya\projects\docker\diskstation\traefik\letsencrypt\acme.json`) → openssl pkcs12 -export → PFX → Import-PfxCertificate на RUVDS → New-WebBinding `*:443:` с SslFlags=1 (SNI) → AddSslCertificate by thumbprint. 25 HTTPS bindings live. Cert chain valid (LE R13), HTTP/2 auto-negotiated. +- **2026-05-23 23:35 (live smoke from VDS):** 7 prod hostnames → 200 OK + correct content; canonical bare→www 301s (CMS-side); 4 hostnames (`maljarka.tandemmebel.ru`, `rimiz.ru`, `www.rimiz.ru`, `rimiz.snolla.com`) → 502/404 — CMS-internal tenant mismatch (на source IIS:8089 они возвращают 200 default = тоже degraded). Не блокирует cutover. +- **2026-05-24 ~00:00 (DNS partial-cutover):** user flipped A `kupimknigi.spb.ru` в reg.ru — сначала на `89.253.255.94` (VDS Linux, ошибка), потом на `80.64.31.36` (RUVDS). Authoritative `ns1.reg.ru` отдаёт правильное; public resolver-cache (8.8.8.8=6h, 1.1.1.1=24h, Yandex=2h) держат старое до TTL expiry. Real end-users мигрируют postepenno over cache TTL. **TTL=86400 — slишком много. Recommendation: снизить до 300s в reg.ru для всех hostnames в scope ДО полного DNS swap'а.** +- **MinIO pipeline caveat (carry-over от `[iis-cutover-to-vds-services]`):** RUVDS IIS coupled к windows-host imgproxy через DNS `imgproxy.kzntsv.site`. SPOF home machine остаётся для image-rendering. Verified post-migration: `Test-NetConnection imgproxy.kzntsv.site -Port 443` с RUVDS = OK. Image pipeline working but не resilient. ## Open questions -- [ ] **RUVDS disk capacity** — 30 GB HDD vs estimated 30-50 GB total sites. Нужен audit `Get-ChildItem C:\sites -Directory | %{[PSCustomObject]@{Name=$_.Name; SizeGB=(Get-ChildItem $_.FullName -Recurse | Measure-Object -Property Length -Sum).Sum / 1GB}}` на source перед transfer'ом. Если >25 GB — нужен upgrade RUVDS plan (или selective transfer без cache/logs/temp dirs). -- [ ] **SSL strategy** — use existing traefik LE certs (export?) или setup new LE на IIS via `win-acme`? Recommendation: `win-acme` — standalone-friendly на Core, не зависит от traefik export-ритуала. -- [ ] **Traefik на RUVDS?** — IIS-only (port 80/443 direct) или traefik reverse proxy again? Recommendation: IIS-only для 11 sites, traefik overkill; ставить traefik только если будут не-IIS workloads на RUVDS. -- [ ] **Backup strategy для RUVDS IIS** — расширить `vds-backup-rsync-kreknin` cron на RUVDS как третий source? VDS snapshot? IIS native backup (`Backup-WebConfiguration`)? Решение откладывается до после успешного cutover. -- [ ] **Image-pipeline SPOF (post-cutover):** windows-host imgproxy остаётся single-point — Option B/D из `[iis-cutover-to-vds-services]` MinIO-phase. Решение откладывается до после успешного cutover. +- [ ] **DNS TTL = 86400 на kupimknigi.spb.ru** — снизить до 300s в reg.ru перед swap'ом остальных 24 hostnames. Это сократит cache-tail с 24h до 5 мин. +- [ ] **Source IIS state backup НЕ снят** (skip'нули — appcmd требует elevated source-shell которого не было). Acceptable risk — source IIS всё ещё running как rollback. Если RUVDS proves stable за 1 неделю → можно decommission source без forensic snapshot'а. +- [ ] **CMS-side 502/404 на 4 hostnames** — `maljarka.tandemmebel.ru` / `rimiz.ru` / `www.rimiz.ru` / `rimiz.snolla.com`. На source тоже не работают (return default page). Pre-existing dead-routes от `[iis-traefik-dead-routes-cleanup]` 2026-05-21. Изоляция не блокер для migration; защититься от cutover-blame — отдельная investigation если нужно. +- [ ] **Backup strategy для RUVDS** — расширить `vds-backup-rsync-kreknin` cron как третий source? Решение отложено до после full cutover. +- [ ] **LE renewal на RUVDS** — текущие certs valid до 2026-07-22 (~60 дней). До expiry нужен permanent renewal pipeline: + - Option A: win-acme (wacs.exe) standalone-на-RUVDS с DNS-01 (manual TXT на reg.ru — нет reg.ru-plugin) или HTTP-01 (но только после DNS swap'а — иначе LE challenge bounce'нется на home). + - Option B: cron-job который re-extract'ит из traefik acme.json + scp upload + Import-PfxCertificate на RUVDS. Pollute source's traefik lifecycle. + - Recommendation: Option A win-acme + HTTP-01 после full cutover (~95 days margin до cert expiry — 60 days minus DNS-stabilization window). +- [ ] **Image-pipeline SPOF (post-cutover):** windows-host imgproxy остаётся single-point. Option B (local nginx-relay) / Option D (decompile DLL) — defer. ## Completed steps - [x] **2026-05-22:** vendor research (`windows-hosting-vendor-research` 🟢) — RUVDS selected. -- [x] **2026-05-23 09:37:** RUVDS purchased + RDP creds saved. **Note:** creds сначала жили в `.secrets/ruvds-iis.env` plaintext (нарушение etap-2 discipline); ретроспективно вынесены в `pass show ruvds-iis/full-env` 2026-05-23 в session-recovery. -- [x] **2026-05-23 18:01:** source size probe — `C:\sites\snolla\` = 8.66 GB. -- [x] **2026-05-23 18:08:** failed-robocopy attempt → root-cause analysis → finding закреплён в `[[../.wiki/concepts/windows-server-2025-core-bootstrap]]`. +- [x] **2026-05-23 09:37:** RUVDS purchased + RDP creds saved (плюс post-hoc cleanup → `pass`). +- [x] **2026-05-23 18:08:** failed-robocopy attempt → finding закреплён. +- [x] **2026-05-23 22:00:** RUVDS bootstrap (IIS + sub-features + .NET 4.8 verify + URL Rewrite + FW 80/443 + SMB share + 445 rule). Idempotent script `scripts/iis-migration-to-ruvds/01-ruvds-bootstrap.ps1`. +- [x] **2026-05-23 22:35:** OpenSSH.Server + FW 22 + key-auth setup. +- [x] **2026-05-23 22:47-23:13:** scp transfer (8.66 GB / 44725 files, exit 0, count+size MATCH). +- [x] **2026-05-23 23:18:** IIS site `snolla` recreated на RUVDS + ACL grant. +- [x] **2026-05-23 23:25:** HTTP catch-all smoke — local (loopback) green, external (home) garbled by middlebox, external (VDS) green. +- [x] **2026-05-23 23:30:** 14 LE certs extracted from traefik acme.json → 14 PFX → 25 HTTPS bindings + SNI live. +- [x] **2026-05-23 23:35:** 7 prod hostnames live-smoked through VDS → 200 OK / correct content; 4 hostnames degraded pre-migration (acceptable). +- [x] **2026-05-24 ~00:00:** DNS swap kupimknigi.spb.ru → 80.64.31.36 (initial misroute to VDS corrected). + +## Remaining steps (post-soak) + +- [ ] Verify kupimknigi.spb.ru через cache-clean resolver (после TTL expiry на 8.8.8.8 / 1.1.1.1) — должно показывать RUVDS content в браузере без `--resolve` override. +- [ ] **Lower DNS TTL** в reg.ru на остальные 24 hostnames до 300s. Можно сделать сразу — обновит cache в ближайшие 24h, после этого swap пойдёт быстро. +- [ ] DNS swap (через reg.ru) остальных 24 hostnames на `80.64.31.36`. +- [ ] 1-неделя soak с RUVDS как live prod. +- [ ] Decommission source IIS:8089 + traefik routes для CMS-hostnames (но не traefik сам — много других routes остаётся). +- [ ] Cleanup: `Remove-NetFirewallRule -DisplayName 'smb-from-source'` (already-unused) + `ssh-from-source` (после disable temp key) + удалить `~/.ssh/ruvds-iis-migration*` keys + удалить `C:\ProgramData\ssh\administrators_authorized_keys` на RUVDS. +- [ ] LE renewal pipeline (см. Open questions). +- [ ] Concept update — `windows-server-2025-core-bootstrap.md` SMB-section deprecate в пользу SSH/scp, добавить host-header-middlebox warning, добавить HTTP/2 note. +- [ ] New concept — `traefik-acme-json-to-iis-cert-import.md` (recipe экспорта-импорта). +- [ ] Source-info commit — записать что image-pipeline SPOF остался → отдельная task. ## Notes -- **Windows Server 2025 Core** = GUI-less, PowerShell-only management. No Server Manager desktop. Drag-n-drop в RDP не работает без `mstsc /admin` Local Drives redirect. -- **2GB RAM constraint** — IIS + 11 sites = tight. Monitor memory после pilot. Возможно нужен per-pool `RecyclingPeriodicRestartMemory 200MB`. -- **Migration is transitional** — long-term snolla-on-node makes IIS obsolete. Don't over-engineer (не ставить Prometheus, не writing custom monitoring). -- **Image-pipeline caveat:** даже после RUVDS cutover, windows-host остаётся SPOF для image-rendering (см. Decisions log §MinIO caveat). +- **Win Server 2025 Core** — GUI-less, RDP-only, drag-n-drop через `mstsc /admin` Local Drives redirect; scp/PSSession предпочтительнее RDP-copy. +- **2GB RAM** — w3wp ~330 MB при cold start, monitor under load. Возможно нужен `RecyclingPeriodicRestartMemory 200MB`. +- **HTTP middlebox в home network** mangles Host header for direct external HTTP — affected smoke testing, не end-users. +- **Migration transitional** — long-term snolla-on-node makes IIS obsolete. Не over-engineer. - - + + diff --git a/scripts/iis-migration-to-ruvds/01-ruvds-bootstrap.ps1 b/scripts/iis-migration-to-ruvds/01-ruvds-bootstrap.ps1 new file mode 100644 index 0000000..1afc30c --- /dev/null +++ b/scripts/iis-migration-to-ruvds/01-ruvds-bootstrap.ps1 @@ -0,0 +1,153 @@ +#requires -Version 5.1 +#requires -RunAsAdministrator +<# +.SYNOPSIS + RUVDS bootstrap for IIS migration from windows-recovery-host. + +.DESCRIPTION + Run on RUVDS in PowerShell as Administrator (RDP session). + + Steps: + 1. Install IIS (Web-Server + sub-features + Management Tools). + 2. Verify .NET Framework 4.8 (Release >= 528040). + 3. Install URL Rewrite Module 2.1. + 4. Open Defender Firewall: HTTP/80, HTTPS/443. + 5. Create C:\sites + SMB share, scoped to source IP 46.151.25.64. + 6. Print summary. + + Idempotent. Transcript: C:\bootstrap-log.txt +#> + +[CmdletBinding()] +param( + [string]$SourceIp = '46.151.25.64', + [string]$SiteRoot = 'C:\sites', + [string]$ShareName = 'sites' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +Start-Transcript -Path C:\bootstrap-log.txt -Append -Force | Out-Null + +function Step($name) { Write-Host "`n=== $name ===" -ForegroundColor Cyan } +function Ok($msg) { Write-Host " [OK] $msg" -ForegroundColor Green } +function Skip($msg) { Write-Host " [SKIP] $msg" -ForegroundColor Yellow } +function Fail($msg) { Write-Host " [FAIL] $msg" -ForegroundColor Red } + +try { + + Step '1. Install IIS' + $iis = Get-WindowsFeature Web-Server + if ($iis.Installed) { + Skip "Web-Server already installed" + } else { + Install-WindowsFeature Web-Server -IncludeAllSubFeature -IncludeManagementTools -Restart:$false | Out-Null + Ok "Web-Server installed" + } + $extras = @('Web-Asp-Net45','Web-Net-Ext45','Web-ISAPI-Ext','Web-ISAPI-Filter','Web-Windows-Auth','Web-Mgmt-Console') + foreach ($f in $extras) { + $st = Get-WindowsFeature $f + if ($st.Installed) { + Skip "$f already installed" + } else { + Install-WindowsFeature $f -Restart:$false | Out-Null + Ok "$f installed" + } + } + + Step '2. Verify .NET Framework 4.8' + $ndp = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\' -ErrorAction SilentlyContinue + if ($ndp -and $ndp.Release -ge 528040) { + Ok ".NET 4.8 detected (Release=$($ndp.Release))" + } else { + Fail ".NET 4.8 NOT found (Release=$($ndp.Release)). Win Server 2025 normally pre-bundles 4.8 -- check Optional Features." + Write-Host " Hint: DISM /Online /Add-Capability /CapabilityName:NetFx4.8~~~~" -ForegroundColor Yellow + throw "Bootstrap aborted: .NET 4.8 missing" + } + + Step '3. Install URL Rewrite Module 2.1' + $rewriteInstalled = Test-Path 'HKLM:\SOFTWARE\Microsoft\IIS Extensions\URL Rewrite' + if ($rewriteInstalled) { + Skip "URL Rewrite already installed" + } else { + $msiUrl = 'https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi' + $msiPath = "$env:TEMP\rewrite_amd64_en-US.msi" + if (-not (Test-Path $msiPath)) { + Write-Host " Downloading $msiUrl ..." + Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing + Ok "downloaded ($([math]::Round((Get-Item $msiPath).Length / 1MB, 2)) MB)" + } + Write-Host " Installing..." + $p = Start-Process msiexec.exe -ArgumentList "/i `"$msiPath`" /quiet /norestart" -Wait -PassThru + if ($p.ExitCode -ne 0) { throw "msiexec exit $($p.ExitCode)" } + Ok "URL Rewrite installed" + } + + Step '4. Open Defender Firewall (HTTP/80, HTTPS/443)' + foreach ($port in @(80,443)) { + $name = "iis-http-$port" + $existing = Get-NetFirewallRule -DisplayName $name -ErrorAction SilentlyContinue + if ($existing) { + Skip "FW rule $name already exists" + } else { + New-NetFirewallRule -DisplayName $name -Direction Inbound -Protocol TCP -LocalPort $port -Action Allow -Profile Any | Out-Null + Ok "FW rule $name added" + } + } + + Step '5. Open SMB inbound + create share' + if (-not (Test-Path $SiteRoot)) { + New-Item -ItemType Directory -Path $SiteRoot -Force | Out-Null + Ok "Created $SiteRoot" + } else { + Skip "$SiteRoot already exists" + } + + $fileSharingRules = Get-NetFirewallRule -DisplayGroup 'File and Printer Sharing' -ErrorAction SilentlyContinue | Where-Object Enabled -eq 'False' + if ($fileSharingRules) { + $cnt = $fileSharingRules.Count + $fileSharingRules | Set-NetFirewallRule -Enabled True + Ok "Enabled built-in 'File and Printer Sharing' group ($cnt rules)" + } else { + Skip "'File and Printer Sharing' rules already enabled" + } + + $smbRule = Get-NetFirewallRule -DisplayName 'smb-from-source' -ErrorAction SilentlyContinue + if ($smbRule) { + Skip "smb-from-source FW rule already exists" + } else { + New-NetFirewallRule -DisplayName 'smb-from-source' ` + -Direction Inbound -Protocol TCP -LocalPort 445 ` + -RemoteAddress $SourceIp ` + -Action Allow -Profile Any | Out-Null + Ok "smb-from-source FW rule added (source=$SourceIp)" + } + + $share = Get-SmbShare -Name $ShareName -ErrorAction SilentlyContinue + if ($share) { + Skip "SMB share '$ShareName' already exists (path=$($share.Path))" + } else { + New-SmbShare -Name $ShareName -Path $SiteRoot -FullAccess Administrator | Out-Null + Ok "SMB share '$ShareName' created -> $SiteRoot" + } + + Step '6. Summary' + Write-Host "" + Write-Host " IIS (Web-Server): $((Get-WindowsFeature Web-Server).Installed)" + Write-Host " ASP.NET 4.5/4.8: $((Get-WindowsFeature Web-Asp-Net45).Installed)" + Write-Host " URL Rewrite 2.1: $(Test-Path 'HKLM:\SOFTWARE\Microsoft\IIS Extensions\URL Rewrite')" + Write-Host " FW HTTP/80: $([bool](Get-NetFirewallRule -DisplayName 'iis-http-80' -ErrorAction SilentlyContinue))" + Write-Host " FW HTTPS/443: $([bool](Get-NetFirewallRule -DisplayName 'iis-http-443' -ErrorAction SilentlyContinue))" + Write-Host " FW smb-from-source: $([bool](Get-NetFirewallRule -DisplayName 'smb-from-source' -ErrorAction SilentlyContinue))" + Write-Host " SMB share '$ShareName': $([bool](Get-SmbShare -Name $ShareName -ErrorAction SilentlyContinue)) -> $SiteRoot" + Write-Host " Disk C: free: $([math]::Round((Get-PSDrive C).Free / 1GB, 2)) GB" + Write-Host "" + Write-Host "Transcript: C:\bootstrap-log.txt" -ForegroundColor DarkGray + +} catch { + Fail $_.Exception.Message + Write-Host $_.ScriptStackTrace -ForegroundColor DarkRed + exit 1 +} finally { + Stop-Transcript | Out-Null +} diff --git a/scripts/iis-migration-to-ruvds/02-source-transfer.ps1 b/scripts/iis-migration-to-ruvds/02-source-transfer.ps1 new file mode 100644 index 0000000..75b7e8a --- /dev/null +++ b/scripts/iis-migration-to-ruvds/02-source-transfer.ps1 @@ -0,0 +1,171 @@ +#requires -Version 5.1 +#requires -RunAsAdministrator +<# +.SYNOPSIS + Source-side transfer: backup IIS state + robocopy snolla site to RUVDS. + +.DESCRIPTION + Run on source (DESKTOP-NSEF0UK / windows-recovery-host) in PowerShell as Administrator. + + Steps: + 1. Probe RUVDS SMB 445 reachability (fail fast). + 2. Pull RUVDS Administrator password from pass-store. + 3. Backup IIS state to C:\Users\vitya\iis-backup-pre-ruvds\: + - applicationHost.config + - appcmd dump of all sites (for reference) + - appcmd dump of snolla site config + apppool (for recreate on RUVDS) + 4. net-use mount \\80.64.31.36\sites with creds. + 5. Copy IIS backup files into the share (so RUVDS can grab them for recreate). + 6. robocopy C:\sites\snolla -> \\80.64.31.36\sites\snolla + (/Z resume-on-disconnect, /MT:8 parallel, /R:2 /W:5 retries, /XJ skip junctions) + 7. Verify count + size match. + 8. Disconnect SMB. + + Transcript: C:\Users\vitya\iis-backup-pre-ruvds\transfer-log.txt +#> + +[CmdletBinding()] +param( + [string]$RuvdsIp = '80.64.31.36', + [string]$ShareName = 'sites', + [string]$SiteName = 'snolla', + [string]$SourceSite = 'C:\sites\snolla', + [string]$BackupDir = 'C:\Users\vitya\iis-backup-pre-ruvds' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +if (-not (Test-Path $BackupDir)) { New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null } +Start-Transcript -Path (Join-Path $BackupDir 'transfer-log.txt') -Append -Force | Out-Null + +function Step($name) { Write-Host "`n=== $name ===" -ForegroundColor Cyan } +function Ok($msg) { Write-Host " [OK] $msg" -ForegroundColor Green } +function Skip($msg) { Write-Host " [SKIP] $msg" -ForegroundColor Yellow } +function Fail($msg) { Write-Host " [FAIL] $msg" -ForegroundColor Red } + +$shareUnc = "\\$RuvdsIp\$ShareName" +$mountedShare = $false + +try { + + Step '1. Probe RUVDS SMB 445' + $smb = Test-NetConnection -ComputerName $RuvdsIp -Port 445 -InformationLevel Quiet -WarningAction SilentlyContinue + if (-not $smb) { throw "RUVDS:445 not reachable. Re-run 01-ruvds-bootstrap.ps1 on RUVDS." } + Ok "RUVDS:445 reachable" + + Step '2. Pull RUVDS password from pass-store' + $bash = 'C:\Program Files\Git\bin\bash.exe' + if (-not (Test-Path $bash)) { throw "bash.exe not found at $bash; pass-store unreachable" } + $passOut = & $bash -lc 'pass show ruvds-iis/full-env' + $passLine = $passOut | Where-Object { $_ -match '^RUVDS_IIS_PASS=' } | Select-Object -First 1 + if (-not $passLine) { throw "RUVDS_IIS_PASS not found in pass show ruvds-iis/full-env" } + $ruvdsPass = $passLine -replace '^RUVDS_IIS_PASS=','' + Ok "credentials loaded from pass-store" + + Step '3. Backup source IIS state' + $appcmd = "$env:SystemRoot\system32\inetsrv\appcmd.exe" + if (-not (Test-Path $appcmd)) { throw "appcmd.exe not found -- IIS Management Tools not installed on source?" } + + Copy-Item 'C:\Windows\System32\inetsrv\config\applicationHost.config' (Join-Path $BackupDir 'applicationHost.config') -Force + Ok "applicationHost.config backed up" + + & $appcmd list site /config:* /xml | Out-File -FilePath (Join-Path $BackupDir 'all-sites.xml') -Encoding UTF8 + Ok "appcmd list site (all) dumped" + + & $appcmd list site /name:$SiteName /config:* /xml | Out-File -FilePath (Join-Path $BackupDir 'snolla-site.xml') -Encoding UTF8 + Ok "snolla-site.xml dumped" + + & $appcmd list apppool /xml | Out-File -FilePath (Join-Path $BackupDir 'all-apppools.xml') -Encoding UTF8 + Ok "all-apppools.xml dumped" + + Step '4. Mount RUVDS share' + cmd.exe /c "net use $shareUnc /user:Administrator $ruvdsPass" 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + cmd.exe /c "net use $shareUnc /delete /y" 2>&1 | Out-Null + cmd.exe /c "net use $shareUnc /user:Administrator $ruvdsPass" 2>&1 | Tee-Object -Variable mountOut | Out-Null + if ($LASTEXITCODE -ne 0) { throw "net use failed: $mountOut" } + } + $mountedShare = $true + Ok "mounted $shareUnc" + + Step '5. Copy IIS backup files to share (for RUVDS recreate-script access)' + $remoteBackupDir = Join-Path $shareUnc '_iis-backup' + if (-not (Test-Path $remoteBackupDir)) { New-Item -ItemType Directory -Path $remoteBackupDir -Force | Out-Null } + Copy-Item (Join-Path $BackupDir '*.xml') $remoteBackupDir -Force + Copy-Item (Join-Path $BackupDir 'applicationHost.config') $remoteBackupDir -Force + Ok "IIS backup files copied to $remoteBackupDir" + + Step '6. robocopy snolla site' + $srcDir = $SourceSite + $dstDir = Join-Path $shareUnc $SiteName + if (-not (Test-Path $srcDir)) { throw "source $srcDir not found" } + + $srcSizeGB = [math]::Round(((Get-ChildItem $srcDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum / 1GB), 2) + Write-Host " Source size: $srcSizeGB GB" + Write-Host " Destination: $dstDir" + Write-Host " This may take 30-90 min on home uplink. Progress goes to log." + Write-Host "" + + $robocopyLog = Join-Path $BackupDir 'robocopy-snolla.log' + $robocopyArgs = @( + $srcDir, $dstDir, + '*.*', + '/S', '/E', + '/DCOPY:DA', '/COPY:DAT', + '/Z', + '/MT:8', + '/R:2', '/W:5', + '/XJ', + '/TEE', + "/LOG+:$robocopyLog", + '/NP', '/NDL' + ) + $rcStart = Get-Date + & robocopy.exe @robocopyArgs + $rcExit = $LASTEXITCODE + $rcDuration = (Get-Date) - $rcStart + + # robocopy exit codes: 0-7 = success/partial, 8+ = error + if ($rcExit -ge 8) { + throw "robocopy failed with exit $rcExit. See $robocopyLog tail." + } + Ok ("robocopy done in {0:hh\:mm\:ss} (exit={1})" -f $rcDuration, $rcExit) + + Step '7. Verify count + size' + $srcCount = (Get-ChildItem $srcDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object).Count + $dstCount = (Get-ChildItem $dstDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object).Count + $srcBytes = (Get-ChildItem $srcDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum + $dstBytes = (Get-ChildItem $dstDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum + Write-Host " Source: $srcCount files, $([math]::Round($srcBytes / 1MB, 0)) MB" + Write-Host " Dest: $dstCount files, $([math]::Round($dstBytes / 1MB, 0)) MB" + if ($srcCount -ne $dstCount) { Fail "FILE COUNT MISMATCH ($srcCount vs $dstCount)"; throw "count mismatch" } + if ($srcBytes -ne $dstBytes) { Fail "SIZE MISMATCH ($srcBytes vs $dstBytes bytes)"; throw "size mismatch" } + Ok "count + size match" + + Step '8. Summary' + Write-Host "" + Write-Host " Source site: $srcDir" + Write-Host " RUVDS dest: $dstDir" + Write-Host " Files transferred: $dstCount" + Write-Host " Bytes transferred: $([math]::Round($dstBytes / 1GB, 2)) GB" + Write-Host " Duration: $('{0:hh\:mm\:ss}' -f $rcDuration)" + Write-Host " IIS backup local: $BackupDir" + Write-Host " IIS backup remote: $remoteBackupDir (on RUVDS as C:\sites\_iis-backup\)" + Write-Host "" + Write-Host " Transcript: $(Join-Path $BackupDir 'transfer-log.txt')" -ForegroundColor DarkGray + Write-Host " Robocopy log: $robocopyLog" -ForegroundColor DarkGray + Write-Host "" + Write-Host " Next: run 03-ruvds-recreate-sites.ps1 on RUVDS." -ForegroundColor Cyan + +} catch { + Fail $_.Exception.Message + Write-Host $_.ScriptStackTrace -ForegroundColor DarkRed + exit 1 +} finally { + if ($mountedShare) { + cmd.exe /c "net use $shareUnc /delete /y" 2>&1 | Out-Null + Write-Host "`n [cleanup] SMB share dismounted" -ForegroundColor DarkGray + } + Stop-Transcript | Out-Null +} diff --git a/scripts/iis-migration-to-ruvds/README.md b/scripts/iis-migration-to-ruvds/README.md new file mode 100644 index 0000000..c814c03 --- /dev/null +++ b/scripts/iis-migration-to-ruvds/README.md @@ -0,0 +1,42 @@ +# IIS migration to RUVDS — execution scripts + +Скрипты для миграции IIS-хостинга `windows-recovery-host` (DESKTOP-NSEF0UK) → RUVDS Win Server 2025 Core (`80.64.31.36`). + +Контекст и план целиком — `.tasks/iis-migration-to-ruvds.md` + `.wiki/concepts/windows-server-2025-core-bootstrap.md`. + +## Execution order + +| # | Script | Where to run | Purpose | +|---|---|---|---| +| 1 | `01-ruvds-bootstrap.ps1` | **на RUVDS** под RDP (PowerShell admin) | Install IIS + URL Rewrite + open FW 80/443 + open SMB scoped to source IP + create C:\sites share. Idempotent. | +| 2 | `02-source-transfer.ps1` *(coming next)* | **на source** (DESKTOP-NSEF0UK) | net-use → robocopy snolla → verify. | +| 3 | `03-ruvds-recreate-sites.ps1` *(coming next)* | **на RUVDS** | appcmd add site / bindings / web.config conn-string swap → `mssql.kzntsv.site,1433`. | +| 4 | `04-pilot-and-swap.md` *(playbook, not script)* | mixed | Pilot kupimknigi.spb.ru через hosts-file → 24h soak → DNS A swap. | +| 5 | `99-ruvds-cleanup-smb.ps1` *(after cutover)* | **на RUVDS** | Remove SMB share + FW rule (закрываем 445 после migration). | + +## Pre-reqs + +- RUVDS up + RDP ready (`pass show ruvds-iis/full-env`) +- Source public IP **46.151.25.64** уже whitelist'нут в скрипте `#1` (если source IP меняется — `-SourceIp ` параметр) +- На RUVDS — Administrator-shell PowerShell + +## How to copy scripts to RUVDS + +Два варианта: + +**A. RDP drive-redirect (рекомендуемо):** +1. `mstsc /v:80.64.31.36` → Show Options → Local Resources → More → Drives → отметить `C:` → ОК. +2. После logon в RUVDS — `\\tsclient\C\` mount'нут. Скопировать `C:\Users\vitya\projects\.admin\scripts\iis-migration-to-ruvds\01-ruvds-bootstrap.ps1` → `C:\bootstrap.ps1`. +3. PowerShell admin → `C:\bootstrap.ps1`. + +**B. Paste-friendly:** открыть скрипт в нашей репе локально → copy всё содержимое → в RUVDS `notepad C:\bootstrap.ps1` → paste → save → run. + +## Verification после bootstrap'а + +На source: +```powershell +Test-NetConnection 80.64.31.36 -Port 445 # должно стать True +Test-NetConnection 80.64.31.36 -Port 443 # должно стать True +``` + +Если оба True — переходим к script #2.