319 lines
13 KiB
PowerShell
319 lines
13 KiB
PowerShell
# ============================================================
|
|
# 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 '<chemin du package server-full>' 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 '<hash64> <chemin-relatif>'.
|
|
# 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.'
|