# ============================================================ # Omika - Déploiement STATIQUE des fichiers serveur (Phase 5C.2A) # ============================================================ # CIBLE : Windows Server 2025 (SRV-OMIKA), PowerShell ADMINISTRATEUR. # NE PAS exécuter depuis le poste de développement. # # Ce script fait UNIQUEMENT du déploiement de FICHIERS (statique) : # 1. Vérifie l'intégrité du package (SHA256SUMS.txt) avant copie. # 2. Crée l'arborescence C:\Omika (Apps / Data / Logs / Config / Backups / Deploy). # 3. Sauvegarde les anciennes publications avant remplacement. # 4. Copie les 5 publications (OmikaApi, OmikaWorker, DiagnosticsWorker, # DiagnosticsWeb, DiagnosticsApi) en Release win-x64 framework-dependent. # 5. Préserve la configuration serveur déjà injectée (jamais écrasée). # 6. Vérifie le nombre de fichiers copiés (source vs cible) et la présence # des exécutables. # # STATIC FILE DEPLOY ONLY - SERVICES NOT ACTIVATED. # Ce script NE FAIT PAS (interdictions Phase 5C.2A) : # - AUCUNE création / arrêt / démarrage de service Windows. # - AUCUNE règle firewall, AUCUNE installation IIS. # - AUCUNE modification de PostgreSQL, du Collector OpenTelemetry, de HFSQL. # - AUCUN mot de passe, AUCUN secret, AUCUNE écriture de config secrète. # - AUCUN choix de compte de service (A_DETERMINER_EN_PHASE_ACTIVATION). # # IDEMPOTENT : une seconde exécution re-sauvegarde, re-copie, ne perd ni logs, # ni checkpoints, ni configuration serveur déjà injectée. # Prend en charge -WhatIf (aucune modification réelle). # ============================================================ [CmdletBinding(SupportsShouldProcess = $true)] param( [string]$PackageRoot, [string]$InstallRoot = 'C:\Omika', [switch]$SkipChecksum ) $ErrorActionPreference = 'Stop' # --- Résolution de PackageRoot APRÈS le bloc param (bug : $PSScriptRoot est # vide dans une valeur par défaut de param()). Valeur par défaut : dossier # PARENT du dossier du script (le package contient Deploy\ + OmikaApi\ + # OmikaWorker\ + DiagnosticsWorker\ + DiagnosticsWeb\). # --- Résolution de PackageRoot APRÈS le bloc param (bug : $PSScriptRoot est # vide dans une valeur par défaut de param()). Valeur par défaut : dossier # PARENT du dossier du script (le package contient Deploy\ + OmikaApi\ + # OmikaWorker\ + DiagnosticsWorker\ + DiagnosticsWeb\ + DiagnosticsApi\). if (-not $PackageRoot) { if ($PSScriptRoot) { $PackageRoot = Split-Path -Parent $PSScriptRoot } } if (-not $PackageRoot -or -not (Test-Path -LiteralPath $PackageRoot)) { Write-Error "PackageRoot introuvable : '$PackageRoot'. Fournissez -PackageRoot '' explicitement." exit 1 } function Write-Step([string]$msg) { Write-Host "== $msg ==" -ForegroundColor Cyan } function Assert-Admin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($id) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Error "Ce script doit être exécuté en PowerShell ADMINISTRATEUR." exit 1 } } # --- Calcul SHA256 read-only (indépendant de ShouldProcess) ----------------- # Get-FileHash est SupportsShouldProcess : sous -WhatIf global, il ne retourne # PAS l'objet attendu (null) et ".Hash.ToLowerInvariant()" lève une exception # "Impossible d'appeler une méthode dans une expression Null". Cette fonction # lit directement le fichier via System.IO.File + System.Security.Cryptography. # SHA256 : opération purement READ-ONLY, insensible à $WhatIfPreference. # - ouvre en lecture (FileAccess.Read) # - partage en lecture (FileShare.Read) # - retourne l'empreinte hexadécimale minuscule sur 64 caractères # - dispose proprement le FileStream et le SHA256 (finally) function Get-Sha256Hex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path ) $stream = $null $sha256 = $null try { $stream = [System.IO.File]::Open( $Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read) $sha256 = [System.Security.Cryptography.SHA256]::Create() $hashBytes = $sha256.ComputeHash($stream) return ([System.BitConverter]::ToString($hashBytes)).Replace('-', '').ToLowerInvariant() } finally { if ($sha256) { $sha256.Dispose() } if ($stream) { $stream.Dispose() } } } # --- Vérification SHA256 du package ----------------------------------------- # SHA256SUMS.txt : une ligne par fichier, format ' '. # Le chemin est relatif à la racine du package. Échoue si un fichier manque ou # si un hash diffère (sauf -SkipChecksum). function Test-PackageChecksums { param([string]$packageRoot) $sumsPath = Join-Path $packageRoot 'SHA256SUMS.txt' if (-not (Test-Path -LiteralPath $sumsPath)) { Write-Error "SHA256SUMS.txt introuvable : $sumsPath" exit 1 } $ok = 0 $fail = 0 foreach ($line in Get-Content -LiteralPath $sumsPath) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $parts = $line -split '\s+' if ($parts.Count -lt 2) { continue } $expected = $parts[0].Trim().ToLowerInvariant() $rel = ($parts[1..($parts.Count - 1)] -join ' ').Trim().TrimStart('*') if ([string]::IsNullOrWhiteSpace($rel)) { continue } $file = Join-Path $packageRoot $rel if (-not (Test-Path -LiteralPath $file)) { Write-Warning "Fichier manquant dans le package : $rel" $fail++ continue } $actual = Get-Sha256Hex -Path $file if ($actual -ne $expected) { Write-Error "Checksum SHA256 KO : $rel (attendu $expected, obtenu $actual)" $fail++ } else { $ok++ } } if ($fail -gt 0) { Write-Error "Vérification SHA256 échouée : $fail fichier(s) en erreur." exit 1 } Write-Host " SHA256 OK : $ok fichier(s) vérifié(s)." } # --- Copie d'une publication avec sauvegarde + préservation config ---------- function Deploy-AppFolder { param( [string]$Name, [string]$Source, [string]$Target, [string]$BackupRoot, [string[]]$Preserve ) if (-not (Test-Path -LiteralPath $Source)) { Write-Error "Publication introuvable : $Source" exit 1 } $backup = $null if (Test-Path -LiteralPath $Target) { $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $backup = Join-Path $BackupRoot ("{0}-{1}" -f $Name, $stamp) if ($PSCmdlet.ShouldProcess($Target, "Sauvegarder l'ancienne version vers $backup")) { New-Item -ItemType Directory -Force -Path $backup | Out-Null Copy-Item -Path (Join-Path $Target '*') -Destination $backup -Recurse -Force -ErrorAction SilentlyContinue Write-Host " Ancienne version sauvegardée : $backup" } } $sourceCount = (Get-ChildItem -LiteralPath $Source -Recurse -File).Count if ($PSCmdlet.ShouldProcess($Target, "Copier $sourceCount fichier(s) depuis $Source")) { New-Item -ItemType Directory -Force -Path $Target | Out-Null Copy-Item -Path (Join-Path $Source '*') -Destination $Target -Recurse -Force # Préserver la configuration serveur déjà injectée (secrets) : restaurer # depuis la sauvegarde si elle existait déjà sur le serveur. if ($backup) { foreach ($file in $Preserve) { $oldFile = Join-Path $backup $file if (Test-Path -LiteralPath $oldFile) { Copy-Item -Path $oldFile -Destination (Join-Path $Target $file) -Force Write-Host " Configuration serveur préservée : $file" } } } $targetCount = (Get-ChildItem -LiteralPath $Target -Recurse -File).Count Write-Host " $Name : $sourceCount fichier(s) source -> $targetCount fichier(s) cible." if ($targetCount -lt $sourceCount) { Write-Warning " Nombre de fichiers cible ($targetCount) inférieur à la source ($sourceCount) : copie incomplète ?" } } } # --- 1. Prérequis ------------------------------------------------------------ Write-Step 'Prérequis' Assert-Admin if (-not $SkipChecksum) { Write-Step 'Vérification SHA256 du package' Test-PackageChecksums -packageRoot $PackageRoot } else { Write-Warning "Vérification SHA256 ignorée (-SkipChecksum)." } # --- 2. Arborescence C:\Omika ------------------------------------------------ Write-Step 'Création de l''arborescence' $appsRoot = Join-Path $InstallRoot 'Apps' $dataRoot = Join-Path $InstallRoot 'Data' $logsRoot = Join-Path $InstallRoot 'Logs' $configRoot = Join-Path $InstallRoot 'Config' $backupsRoot = Join-Path $InstallRoot 'Backups' $deployRoot = Join-Path $InstallRoot 'Deploy' $folders = @( $appsRoot, (Join-Path $appsRoot 'OmikaApi'), (Join-Path $appsRoot 'OmikaWorker'), (Join-Path $appsRoot 'DiagnosticsWorker'), (Join-Path $appsRoot 'DiagnosticsWeb'), (Join-Path $appsRoot 'DiagnosticsApi'), (Join-Path $dataRoot 'Api\Diagnostics\Jsonl'), (Join-Path $dataRoot 'DiagnosticsWorker\Checkpoint'), (Join-Path $dataRoot 'DiagnosticsApi\Central'), (Join-Path $logsRoot 'Api'), (Join-Path $logsRoot 'Worker'), (Join-Path $logsRoot 'DiagnosticsWorker'), (Join-Path $logsRoot 'DiagnosticsWeb'), (Join-Path $logsRoot 'DiagnosticsApi'), $configRoot, $backupsRoot, $deployRoot ) foreach ($folder in $folders) { if ($PSCmdlet.ShouldProcess($folder, 'Créer le dossier')) { New-Item -ItemType Directory -Force -Path $folder | Out-Null } } # --- 3+4. Copie des publications -------------------------------------------- Write-Step 'Copie des publications (avec sauvegarde)' Deploy-AppFolder -Name 'OmikaApi' ` -Source (Join-Path $PackageRoot 'OmikaApi') ` -Target (Join-Path $appsRoot 'OmikaApi') ` -BackupRoot $backupsRoot ` -Preserve @('appsettings.json', 'appsettings.Production.json') Deploy-AppFolder -Name 'OmikaWorker' ` -Source (Join-Path $PackageRoot 'OmikaWorker') ` -Target (Join-Path $appsRoot 'OmikaWorker') ` -BackupRoot $backupsRoot ` -Preserve @('appsettings.json', 'appsettings.Production.json') Deploy-AppFolder -Name 'DiagnosticsWorker' ` -Source (Join-Path $PackageRoot 'DiagnosticsWorker') ` -Target (Join-Path $appsRoot 'DiagnosticsWorker') ` -BackupRoot $backupsRoot ` -Preserve @('appsettings.json', 'appsettings.Production.json') Deploy-AppFolder -Name 'DiagnosticsWeb' ` -Source (Join-Path $PackageRoot 'DiagnosticsWeb') ` -Target (Join-Path $appsRoot 'DiagnosticsWeb') ` -BackupRoot $backupsRoot ` -Preserve @('appsettings.json', 'appsettings.Production.json') Deploy-AppFolder -Name 'DiagnosticsApi' ` -Source (Join-Path $PackageRoot 'DiagnosticsApi') ` -Target (Join-Path $appsRoot 'DiagnosticsApi') ` -BackupRoot $backupsRoot ` -Preserve @('appsettings.json', 'appsettings.Production.json') # --- 5. Vérification des exécutables ---------------------------------------- Write-Step 'Vérification des exécutables' $expectedExe = @( (Join-Path $appsRoot 'OmikaApi\Omika.Api.exe'), (Join-Path $appsRoot 'OmikaWorker\Omika.Worker.exe'), (Join-Path $appsRoot 'DiagnosticsWorker\Omika.Diagnostics.Worker.exe'), (Join-Path $appsRoot 'DiagnosticsWeb\Omika.Diagnostics.Web.exe'), (Join-Path $appsRoot 'DiagnosticsApi\Omika.Diagnostics.Api.exe') ) foreach ($exe in $expectedExe) { if (Test-Path -LiteralPath $exe) { Write-Host " OK : $exe" } else { Write-Warning " EXE absent : $exe (copie incomplète ou nom inattendu)." } } # --- Résumé ------------------------------------------------------------------ Write-Host '' Write-Host 'DEPLOIEMENT STATIQUE TERMINE (fichiers copies uniquement).' -ForegroundColor Green Write-Host " Apps : $appsRoot" Write-Host " Data : $dataRoot" Write-Host " Logs : $logsRoot" Write-Host " Config : $configRoot" Write-Host " Backups : $backupsRoot" Write-Host '' Write-Host 'SERVICES NOT ACTIVATED.' -ForegroundColor Yellow Write-Host 'Aucun service Windows n''a été créé, démarré ni arrêté.' -ForegroundColor Yellow Write-Host 'Aucun firewall / IIS / PostgreSQL / Collector / HFSQL n''a été modifié.' -ForegroundColor Yellow Write-Host '' Write-Host 'RAPPELS (à faire manuellement, jamais automatisé ici) :' -ForegroundColor Yellow Write-Host ' - Injecter les secrets (connection strings, HmacKey, HFSQL, licence)' Write-Host ' uniquement SUR LE SERVEUR, via variables d''environnement ou' Write-Host ' appsettings.Production.json local à ACL restreinte.' Write-Host ' - Créer les comptes de service Windows dédiés et les services dans la' Write-Host ' phase d''activation (comptes A_DETERMINER_EN_PHASE_ACTIVATION).' Write-Host ' - Appliquer les ACL NTFS minimales (voir README-DEPLOYMENT.txt).' Write-Host ' - Aucune règle firewall 5432/4318 n''a été ni ne doit être ajoutée.'