Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
# ============================================================
# Omika Diagnostic Center - Durcissement PostgreSQL (moindre privilege)
# Applique UNIQUEMENT 003_diagnostics_roles.sql (roles + ownership + grants).
# ============================================================
# A executer SUR LE SERVEUR (Windows Server 2025), PowerShell administrateur,
# avec un compte PostgreSQL ADMINISTRATIF (superutilisateur, ex. postgres).
#
# Ce script est VOLONTAIREMENT SEPARE de Install-DatabaseSchema.ps1 :
# 003 transfere l'ownership et gere les roles -> contexte administrateur.
# 001/002 (schema) sont appliques par Install-DatabaseSchema.ps1.
#
# NOTE (V2) : Install-DatabaseSchema.ps1 applique desormais TOUTES les
# migrations 001 -> 016 dans l'ordre (schema + roles + grants). Ce script reste
# disponible pour n'appliquer QUE 003 (transition/back-compat) ; pour une
# installation complete, preferer Install-DatabaseSchema.ps1.
#
# Garanties :
# - AUCUN mot de passe en dur, AUCUN secret (les mots de passe des roles
# LOGIN sont definis MANUELLEMENT ensuite via psql : \password <role>).
# - AUCUN DROP, AUCUNE suppression de role/objet/donnee (003 est idempotent).
# - Ne touche PAS au role historique omika_diagnostics_app (transition).
#
# Usage :
# $env:PGPASSWORD = '...' # facultatif - sinon saisie masquee demandee
# .\Harden-DatabaseRoles.ps1
# .\Harden-DatabaseRoles.ps1 -DatabaseDir "C:\Omika\Deploy\phase5b2\Database"
# .\Harden-DatabaseRoles.ps1 -PsqlPath "C:\Program Files\PostgreSQL\18\bin\psql.exe"
# .\Harden-DatabaseRoles.ps1 -Username postgres
# ============================================================
[CmdletBinding()]
param(
[string]$DatabaseDir,
[string]$PsqlPath,
[string]$PgHost = '127.0.0.1',
[int]$PgPort = 5432,
[string]$Database = 'omika_diagnostics',
[string]$Username = 'postgres'
)
$ErrorActionPreference = 'Stop'
# --- Resolution de DatabaseDir APRES le bloc param (bug $PSScriptRoot).
# Valeur par defaut : dossier frere "Database" du dossier du script.
if (-not $DatabaseDir) {
if ($PSScriptRoot) {
$DatabaseDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Database'
}
}
if (-not $DatabaseDir -or -not (Test-Path -LiteralPath $DatabaseDir)) {
Write-Error "Dossier des schemas introuvable : $DatabaseDir. Fournissez -DatabaseDir '<chemin>\Database'."
exit 1
}
# --- Detection de psql.exe (ordre : -PsqlPath, PATH, C:\Program Files\PostgreSQL\*).
function Resolve-PsqlPath {
param([string]$ExplicitPath)
if ($ExplicitPath) {
if (-not (Test-Path -LiteralPath $ExplicitPath)) {
Write-Error "PsqlPath fourni introuvable : $ExplicitPath"
exit 1
}
return (Resolve-Path -LiteralPath $ExplicitPath).Path
}
$inPath = Get-Command psql.exe -ErrorAction SilentlyContinue
if ($inPath) {
return $inPath.Source
}
$candidates = @(Get-ChildItem -Path 'C:\Program Files\PostgreSQL\*\bin\psql.exe' -ErrorAction SilentlyContinue)
if ($candidates.Count -gt 0) {
$best = $candidates | ForEach-Object {
$version = 0
$verDir = $_.Directory.Parent.Name
if ($verDir -match '^\d+$') { $version = [int]$verDir }
[PSCustomObject]@{ Path = $_.FullName; Version = $version }
} | Sort-Object -Property Version -Descending | Select-Object -First 1
return $best.Path
}
return $null
}
$psqlExe = Resolve-PsqlPath -ExplicitPath $PsqlPath
if (-not $psqlExe) {
Write-Error "Client 'psql.exe' introuvable. Fournissez -PsqlPath ou installez le client PostgreSQL."
exit 1
}
Write-Host "psql utilise : $psqlExe"
# --- Mot de passe : priorite a PGPASSWORD deja present ; sinon saisie masquee.
$useExistingPgPassword = -not [string]::IsNullOrEmpty($env:PGPASSWORD)
if (-not $useExistingPgPassword) {
$secure = Read-Host "Mot de passe PostgreSQL (role '$Username', base '$Database')" -AsSecureString
$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))
$env:PGPASSWORD = $plain
$plain = $null
}
$sqlPath = Join-Path $DatabaseDir '003_diagnostics_roles.sql'
if (-not (Test-Path -LiteralPath $sqlPath)) {
throw "Migration de securite introuvable : $sqlPath"
}
try {
Write-Host "Application du durcissement : 003_diagnostics_roles.sql (${PgHost}:${PgPort}/${Database}, role $Username) ..."
& $psqlExe `
-h $PgHost `
-p $PgPort `
-U $Username `
-d $Database `
-v ON_ERROR_STOP=1 `
-f $sqlPath
if ($LASTEXITCODE -ne 0) {
throw "psql a echoue (exit $LASTEXITCODE) sur 003_diagnostics_roles.sql."
}
Write-Host ''
Write-Host "Durcissement applique (idempotent)." -ForegroundColor Green
Write-Host "Roles : omika_diagnostics_owner (NOLOGIN), omika_diagnostics_worker, omika_diagnostics_web."
Write-Host "Worker : INSERT diagnostic_event + USAGE sequence. Web : SELECT diagnostic_event."
Write-Host ''
Write-Host "ETAPES MANUELLES SUIVANTES (jamais automatisees ici) :" -ForegroundColor Yellow
Write-Host " 1. Definir les mots de passe (psql connecte comme administrateur PostgreSQL) :"
Write-Host " \password omika_diagnostics_worker"
Write-Host " \password omika_diagnostics_web"
Write-Host " (jamais dans un script, un log, une commande en clair, ni PowerShell history)."
Write-Host " 2. Pointer Worker : Username=omika_diagnostics_worker ; Web : Username=omika_diagnostics_web"
Write-Host " (ConnectionStrings__DiagnosticsDatabase, secret injecte SUR LE SERVEUR uniquement)."
Write-Host " 3. Valider Worker (INSERT) et Web (SELECT)."
Write-Host " 4. SEULEMENT APRES validation : desactiver l'ancien role (action administrateur explicite) :"
Write-Host " ALTER ROLE omika_diagnostics_app NOLOGIN;"
}
finally {
if (-not $useExistingPgPassword) {
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
}
}
+166
View File
@@ -0,0 +1,166 @@
# ============================================================
# Omika Diagnostic Center - Application IDEMPOTENTE du schema PostgreSQL
# Base dediee : omika_diagnostics (outil SUPPORT uniquement)
# ============================================================
# A executer SUR LE SERVEUR (Windows Server 2025), PowerShell administrateur.
# NE JAMAIS executer ce script depuis un poste qui n'heberge pas PostgreSQL.
#
# APPLIQUE TOUTES LES MIGRATIONS Diagnostics DANS L'ORDRE (001 -> 016).
# Inclut le schema (001/002/005/006/007/008), les roles/moindre privilege
# (003/009), les tables administrables (010/011/012) et les GRANT de
# retention/suppression (004/013/014/015/016).
#
# CONTEXTE : a executer SUR LE SERVEUR avec un compte PostgreSQL SUPERUSER
# (ex. -Username postgres), car 003/009 font du CREATE ROLE / ALTER ... OWNER
# et les migrations de GRANT (004/013/014/015/016) requierent le superuser.
#
# Garanties :
# - AUCUN mot de passe en dur (jamais dans ce fichier, jamais affiche).
# - AUCUN DROP, AUCUNE suppression de base/table, AUCUNE migration destructive.
# - AUCUNE donnee ecrasee : applique via CREATE/ADD ... IF NOT EXISTS + GRANT idempotent.
# - Idempotent : une seconde execution est un no-op (schema deja present).
# - Applique les fichiers SQL DANS L'ORDRE (001 -> 016), avec ON_ERROR_STOP=1.
# - Exclut 003_diagnostics_roles_preprod.sql (preproduction uniquement).
#
# Usage :
# $env:PGPASSWORD = '...' # facultatif - sinon saisie masquee demandee
# .\Install-DatabaseSchema.ps1
# .\Install-DatabaseSchema.ps1 -DatabaseDir "C:\Omika\Deploy\server-full\Database"
# .\Install-DatabaseSchema.ps1 -PsqlPath "C:\Program Files\PostgreSQL\18\bin\psql.exe"
#
# Note : le script Harden-DatabaseRoles.ps1 reste disponible pour n'appliquer
# QUE 003 (transition/back-compat) ; le present script couvre l'integralite.
# ============================================================
[CmdletBinding()]
param(
[string[]]$SchemaFiles = @(
'001_diagnostics_schema.sql',
'002_actor_key.sql',
'003_diagnostics_roles.sql',
'004_diagnostics_retention.sql',
'005_diagnostics_client_context.sql',
'006_diagnostics_device_license.sql',
'007_diagnostics_usage.sql',
'008_diagnostics_clients.sql',
'009_diagnostics_clients_grants.sql',
'010_diagnostics_web_users.sql',
'011_diagnostics_reference.sql',
'012_diagnostics_tasks.sql',
'013_diagnostics_tasks_delete.sql',
'014_diagnostics_telemetry_retention.sql',
'015_diagnostics_retention_untreated.sql',
'016_diagnostics_event_delete.sql'
),
[string]$DatabaseDir,
[string]$PsqlPath,
[string]$PgHost = '127.0.0.1',
[int]$PgPort = 5432,
[string]$Database = 'omika_diagnostics',
[string]$Username = 'postgres'
)
$ErrorActionPreference = 'Stop'
# --- Resolution de DatabaseDir APRES le bloc param (bug : $PSScriptRoot est
# vide dans une valeur par defaut de param()). Valeur par defaut : dossier
# frere "Database" du dossier du script (layout package : phase5b2\Database
# + phase5b2\Deploy).
if (-not $DatabaseDir) {
if ($PSScriptRoot) {
$DatabaseDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Database'
}
}
if (-not $DatabaseDir -or -not (Test-Path -LiteralPath $DatabaseDir)) {
Write-Error "Dossier des schemas introuvable : $DatabaseDir. Fournissez -DatabaseDir '<chemin>\Database'."
exit 1
}
# --- Detection de psql.exe (ordre : -PsqlPath explicite, puis PATH, puis
# C:\Program Files\PostgreSQL\*\bin\psql.exe en choisissant la version la
# plus elevee de maniere deterministe).
function Resolve-PsqlPath {
param([string]$ExplicitPath)
if ($ExplicitPath) {
if (-not (Test-Path -LiteralPath $ExplicitPath)) {
Write-Error "PsqlPath fourni introuvable : $ExplicitPath"
exit 1
}
return (Resolve-Path -LiteralPath $ExplicitPath).Path
}
$inPath = Get-Command psql.exe -ErrorAction SilentlyContinue
if ($inPath) {
return $inPath.Source
}
$candidates = @(Get-ChildItem -Path 'C:\Program Files\PostgreSQL\*\bin\psql.exe' -ErrorAction SilentlyContinue)
if ($candidates.Count -gt 0) {
$best = $candidates | ForEach-Object {
$version = 0
$verDir = $_.Directory.Parent.Name
if ($verDir -match '^\d+$') { $version = [int]$verDir }
[PSCustomObject]@{ Path = $_.FullName; Version = $version }
} | Sort-Object -Property Version -Descending | Select-Object -First 1
return $best.Path
}
return $null
}
$psqlExe = Resolve-PsqlPath -ExplicitPath $PsqlPath
if (-not $psqlExe) {
Write-Error "Client 'psql.exe' introuvable. Fournissez -PsqlPath ou placez le client PostgreSQL dans le PATH."
exit 1
}
Write-Host "psql utilise : $psqlExe"
# --- Mot de passe : priorite a PGPASSWORD deja present ; sinon saisie masquee.
# Le mot de passe n'est JAMAIS ecrit dans un fichier ni affiche a l'ecran.
$useExistingPgPassword = -not [string]::IsNullOrEmpty($env:PGPASSWORD)
if (-not $useExistingPgPassword) {
$secure = Read-Host "Mot de passe PostgreSQL (role '$Username', base '$Database')" -AsSecureString
$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))
$env:PGPASSWORD = $plain
$plain = $null
}
try {
foreach ($schemaFile in $SchemaFiles) {
$sqlPath = Join-Path $DatabaseDir $schemaFile
if (-not (Test-Path -LiteralPath $sqlPath)) {
throw "Fichier de schema introuvable : $sqlPath"
}
Write-Host "Application du schema IDEMPOTENT : $schemaFile (${PgHost}:${PgPort}/${Database}, role $Username) ..."
& $psqlExe `
-h $PgHost `
-p $PgPort `
-U $Username `
-d $Database `
-v ON_ERROR_STOP=1 `
-f $sqlPath
if ($LASTEXITCODE -ne 0) {
throw "psql a echoue (exit $LASTEXITCODE) sur $schemaFile. Aucune modification destructive possible (CREATE/ADD IF NOT EXISTS uniquement)."
}
}
Write-Host ''
Write-Host "Migrations appliquees (idempotent, fichiers dans l'ordre 001 -> 016)." -ForegroundColor Green
Write-Host "Tables : diagnostic_event, telemetry_metric_hourly, diagnostics_clients,"
Write-Host " diagnostics_users, diagnostics_reference, diagnostics_tasks."
Write-Host "Roles : omika_diagnostics_owner/worker/web/api. Aucune donnee ecrasee."
Write-Host ''
Write-Host "Rappel : definir les mots de passe des roles LOGIN manuellement (jamais ici) :" -ForegroundColor Yellow
Write-Host " \password omika_diagnostics_worker"
Write-Host " \password omika_diagnostics_web"
Write-Host " \password omika_diagnostics_api"
}
finally {
if (-not $useExistingPgPassword) {
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
}
}
+125
View File
@@ -0,0 +1,125 @@
# ============================================================
# Omika - Installation du service Windows OmikaApi (deploiement 1/6)
# ============================================================
# CIBLE : Windows Server 2025 (SRV-OMIKA), PowerShell ADMINISTRATEUR.
# NE PAS executer depuis le poste de developpement.
#
# CE QUE CE SCRIPT FAIT :
# 1. Cree les dossiers propres a l'API (journalisation + spool JSONL).
# 2. Pose les ACL NTFS minimales (avec CONTROLE du code retour d'icacls).
# 3. Valide le compte de service (existence + droit de connexion).
# 4. Cree le service Windows OmikaApi (idempotent, StartupType Manual).
# 5. Verifie ce qui a ete reellement cree (binaire, compte, etat).
#
# CE QU'IL NE FAIT PAS :
# - AUCUN demarrage de service.
# - AUCUN secret ecrit (variables d'environnement = action manuelle).
# - AUCUNE regle firewall, AUCUN IIS, AUCUNE modification HFSQL/PostgreSQL.
# - AUCUNE copie de binaires (voir Install-OmikaServerFiles.ps1).
#
# PREREQUIS MANUELS :
# - Le compte local dedie existe et possede le droit « Ouvrir une session en
# tant que service » (secpol.msc). Ce script le VERIFIE et avertit.
# - La publication est deja copiee dans <InstallRoot>\Apps\OmikaApi
# (Omika.Api.exe present).
#
# IDEMPOTENT : une seconde execution ne recree rien et signale toute divergence
# entre le service existant et le binaire/compte attendus.
# Prend en charge -WhatIf (aucune modification reelle).
#
# EXEMPLES :
# .\Install-OmikaApi.ps1
# .\Install-OmikaApi.ps1 -WhatIf
# .\Install-OmikaApi.ps1 -Desinstaller # rollback du service
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$InstallRoot = 'C:\Omika',
[string]$ServiceName = 'OmikaApi',
[string]$Account = '.\svc_omika_api',
[ValidateSet('Manual', 'Automatic', 'Disabled')]
[string]$StartupType = 'Manual',
# Rollback : supprime le service (refuse si le service tourne encore).
[switch]$Desinstaller
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'Omika.Deploy.Common.psm1') -Force
# Le module a son propre etat de session : $WhatIfPreference ne s'y propage pas.
$Simulation = [bool]$WhatIfPreference
# --- Definition du service ---------------------------------------------------
$NomAffiche = 'Omika Api'
$DossierApps = 'OmikaApi'
$Executable = 'Omika.Api.exe'
$Description = 'Omika - API metier (Kestrel) : bootstrap mobile, synchronisation, documents, exposition SRV_LANGUES.'
# Dossiers dont CE service est proprietaire (droit Modifier).
$DossiersModifier = @(
'Logs\Api',
'Data\Api\Diagnostics\Jsonl'
)
try {
Assert-OmikaAdmin
$chemins = Get-OmikaPaths -InstallRoot $InstallRoot
$cheminExe = Join-Path (Join-Path $chemins.Apps $DossierApps) $Executable
# --- Rollback ------------------------------------------------------------
if ($Desinstaller) {
Write-OmikaStep "Desinstallation du service $ServiceName"
Remove-OmikaService -Name $ServiceName -WhatIf:$Simulation | Out-Null
Write-OmikaServiceSummary -Name $ServiceName -Supprime
exit 0
}
# --- 1. Dossiers ---------------------------------------------------------
Write-OmikaStep 'Dossiers du service'
foreach ($relatif in $DossiersModifier) {
New-OmikaDirectory -Path (Join-Path $InstallRoot $relatif) -WhatIf:$Simulation | Out-Null
}
# --- 2. ACL --------------------------------------------------------------
Write-OmikaStep 'ACL NTFS minimales'
foreach ($relatif in $DossiersModifier) {
Grant-OmikaAcl -Path (Join-Path $InstallRoot $relatif) -Account $Account -Right 'M' `
-RequireExisting -WhatIf:$Simulation | Out-Null
}
# --- 3. Compte de service ------------------------------------------------
Write-OmikaStep 'Compte de service'
Assert-OmikaServiceAccount -Account $Account | Out-Null
# --- 4. Service Windows --------------------------------------------------
Write-OmikaStep "Creation du service Windows ($StartupType)"
Install-OmikaService -Name $ServiceName -DisplayName $NomAffiche `
-ExePath $cheminExe -Account $Account -Description $Description `
-StartupType $StartupType -WhatIf:$Simulation | Out-Null
# --- 5. Recapitulatif ----------------------------------------------------
Write-OmikaServiceSummary -Name $ServiceName -ExePath $cheminExe -Account $Account -Rappels @(
'Injecter les secrets en variables MACHINE (aucun secret dans ce script) :'
' Hfsql / LicenseConnection / SyncConnection / ServiceConnection / FilePasswords'
' ConnectionStrings__DiagnosticsDatabase (si l''API lit PostgreSQL)'
' DiagnosticsIdentity__HmacKey (ActorKey, min 256 bits Base64)'
' MobileAuth__SigningKey (OBLIGATOIRE : echec au demarrage sinon)'
' DiagnosticDeployment__ClientCode / __ServerInstanceId'
' Observability__Remote__Directory (IDENTIQUE au Worker Diagnostics)'
' ASPNETCORE_ENVIRONMENT=Production'
' ASPNETCORE_URLS=http://127.0.0.1:<PORT_API>'
'Verifier le provider OLE DB PCSOFT.HFSQL (64 bits) enregistre.'
'Installer LibreOffice si la generation PDF des rapports est utilisee.'
'DiagnosticsForwarding__Enabled reste false : ne l''activer que pour un serveur client distant.'
)
exit 0
}
catch {
Write-OmikaFail $_.Exception.Message
exit 1
}
@@ -0,0 +1,133 @@
# ============================================================
# Omika - Installation du service Windows OmikaDiagnosticsApi (deploiement 5/6)
# ============================================================
# CIBLE : Windows Server 2025 (SRV-OMIKA), PowerShell ADMINISTRATEUR.
# NE PAS executer depuis le poste de developpement.
#
# A QUOI SERT CE SERVICE :
# API centrale qui RECOIT les lots de diagnostics envoyes par les serveurs
# clients distants (multi-serveur uniquement). Sur un SRV-OMIKA mono-serveur
# (l'API ecrit son JSONL localement), ce service est INUTILE : ne pas le
# deployer, ou le laisser arrete.
#
# CE QUE CE SCRIPT FAIT :
# 1. Cree le dossier du spool central + journalisation.
# 2. Pose les ACL NTFS minimales (Modifier sur le spool central).
# 3. Valide le compte de service (existence + droit de connexion).
# 4. Cree le service Windows OmikaDiagnosticsApi (idempotent, Manual).
# 5. Verifie ce qui a ete reellement cree.
#
# CE QU'IL NE FAIT PAS :
# - AUCUN demarrage de service, AUCUN secret ecrit, AUCUNE migration SQL.
# - AUCUNE cle de client Diagnostics (se cree dans l'interface Web).
#
# PREREQUIS MANUELS :
# - Compte local dedie + droit « Ouvrir une session en tant que service ».
# - Publication copiee dans <InstallRoot>\Apps\DiagnosticsApi.
# - La table des clients Diagnostics existe (migration 006).
# - Au moins un client enregistre (OmikaDiagnosticsWeb -> Clients Diagnostics).
#
# IDEMPOTENT - prend en charge -WhatIf.
#
# EXEMPLES :
# .\Install-OmikaDiagnosticsApi.ps1
# .\Install-OmikaDiagnosticsApi.ps1 -WhatIf
# .\Install-OmikaDiagnosticsApi.ps1 -Desinstaller
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$InstallRoot = 'C:\Omika',
[string]$ServiceName = 'OmikaDiagnosticsApi',
[string]$Account = '.\svc_omika_diagapi',
# Compte du service OmikaDiagnosticsWorker : lecteur du spool central.
[string]$AccountWorker = '.\svc_omika_diag',
[ValidateSet('Manual', 'Automatic', 'Disabled')]
[string]$StartupType = 'Manual',
[switch]$Desinstaller
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'Omika.Deploy.Common.psm1') -Force
# Le module a son propre etat de session : $WhatIfPreference ne s'y propage pas.
$Simulation = [bool]$WhatIfPreference
# --- Definition du service ---------------------------------------------------
$NomAffiche = 'Omika Diagnostics Api'
$DossierApps = 'DiagnosticsApi'
$Executable = 'Omika.Diagnostics.Api.exe'
$Description = 'Omika - API centrale de reception des lots de diagnostics (multi-serveur, cle client, spool central).'
$DossiersModifier = @(
'Logs\DiagnosticsApi',
'Data\DiagnosticsApi\Central'
)
try {
Assert-OmikaAdmin
$chemins = Get-OmikaPaths -InstallRoot $InstallRoot
$cheminExe = Join-Path (Join-Path $chemins.Apps $DossierApps) $Executable
# --- Rollback ------------------------------------------------------------
if ($Desinstaller) {
Write-OmikaStep "Desinstallation du service $ServiceName"
Remove-OmikaService -Name $ServiceName -WhatIf:$Simulation | Out-Null
Write-OmikaServiceSummary -Name $ServiceName -Supprime
exit 0
}
# --- 1. Dossiers ---------------------------------------------------------
Write-OmikaStep 'Dossiers du service'
foreach ($relatif in $DossiersModifier) {
New-OmikaDirectory -Path (Join-Path $InstallRoot $relatif) -WhatIf:$Simulation | Out-Null
}
# --- 2. ACL --------------------------------------------------------------
Write-OmikaStep 'ACL NTFS minimales'
# 2a. Spool central + logs : ce service ecrit.
foreach ($relatif in $DossiersModifier) {
Grant-OmikaAcl -Path (Join-Path $InstallRoot $relatif) -Account $Account -Right 'M' `
-RequireExisting -WhatIf:$Simulation | Out-Null
}
# 2b. Le Worker Diagnostics doit pouvoir LIRE le spool central.
# Le dossier vient d'etre cree ci-dessus, il existe donc forcement.
Grant-OmikaAcl -Path (Join-Path $InstallRoot 'Data\DiagnosticsApi\Central') `
-Account $AccountWorker -Right 'R' -RequireExisting -WhatIf:$Simulation | Out-Null
# --- 3. Compte de service ------------------------------------------------
Write-OmikaStep 'Compte de service'
Assert-OmikaServiceAccount -Account $Account | Out-Null
# --- 4. Service Windows --------------------------------------------------
Write-OmikaStep "Creation du service Windows ($StartupType)"
Install-OmikaService -Name $ServiceName -DisplayName $NomAffiche `
-ExePath $cheminExe -Account $Account -Description $Description `
-StartupType $StartupType -WhatIf:$Simulation | Out-Null
# --- 5. Recapitulatif ----------------------------------------------------
Write-OmikaServiceSummary -Name $ServiceName -ExePath $cheminExe -Account $Account -Rappels @(
'Injecter les secrets en variables MACHINE (aucun secret dans ce script) :'
' ConnectionStrings__DiagnosticsDatabase=<connexion PostgreSQL centrale>'
' Diagnostics__Central__Directory=<dossier du spool central>'
' ASPNETCORE_ENVIRONMENT=Production'
' ASPNETCORE_URLS=http://127.0.0.1:<PORT_DIAG_API>'
'ENREGISTRER LES CLIENTS : OmikaDiagnosticsWeb -> Clients Diagnostics -> Nouveau client.'
' La cle n''est affichee QU''UNE FOIS : la copier immediatement.'
'Cote serveur client, activer alors DiagnosticsForwarding__Enabled=true + Endpoint + ApiKey.'
'Exposer en HTTPS via le reverse proxy Apache : jamais Kestrel en direct.'
'Sur un serveur MONO-SERVEUR, ce service n''est pas necessaire.'
)
exit 0
}
catch {
Write-OmikaFail $_.Exception.Message
exit 1
}
@@ -0,0 +1,144 @@
# ============================================================
# Omika - Installation du service Windows OmikaDiagnosticsWeb (deploiement 4/6)
# ============================================================
# CIBLE : Windows Server 2025 (SRV-OMIKA), PowerShell ADMINISTRATEUR.
# NE PAS executer depuis le poste de developpement.
#
# CE QUE CE SCRIPT FAIT :
# 1. Cree le dossier de journalisation.
# 2. Pose les ACL NTFS minimales (avec CONTROLE du code retour d'icacls).
# 3. CONTROLE qu'aucun appsettings.Development.json n'est present dans la
# publication (default-deny : un fichier de dev ferait tomber les
# protections Production).
# 4. Valide le compte de service (existence + droit de connexion).
# 5. Cree le service Windows OmikaDiagnosticsWeb (idempotent, Manual).
# 6. Verifie ce qui a ete reellement cree.
#
# CE QU'IL NE FAIT PAS :
# - AUCUN demarrage de service, AUCUN secret ecrit, AUCUNE migration SQL.
# - AUCUN certificat TLS (pose sur le reverse proxy Apache).
#
# PREREQUIS MANUELS :
# - Compte local dedie + droit « Ouvrir une session en tant que service ».
# - Publication copiee dans <InstallRoot>\Apps\DiagnosticsWeb.
# - PostgreSQL joignable + migrations 001 -> 010 appliquees.
# - Premier Admin : voir DEPLOIEMENT_PRODUCTION_DIAGNOSTICS.md §22 (12 etapes).
#
# IDEMPOTENT - prend en charge -WhatIf.
#
# EXEMPLES :
# .\Install-OmikaDiagnosticsWeb.ps1
# .\Install-OmikaDiagnosticsWeb.ps1 -WhatIf
# .\Install-OmikaDiagnosticsWeb.ps1 -Desinstaller
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$InstallRoot = 'C:\Omika',
[string]$ServiceName = 'OmikaDiagnosticsWeb',
[string]$Account = '.\svc_omika_diagweb',
[ValidateSet('Manual', 'Automatic', 'Disabled')]
[string]$StartupType = 'Manual',
[switch]$Desinstaller
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'Omika.Deploy.Common.psm1') -Force
# Le module a son propre etat de session : $WhatIfPreference ne s'y propage pas.
$Simulation = [bool]$WhatIfPreference
# --- Definition du service ---------------------------------------------------
$NomAffiche = 'Omika Diagnostics Web'
$DossierApps = 'DiagnosticsWeb'
$Executable = 'Omika.Diagnostics.Web.exe'
$Description = 'Omika - Interface web de consultation des diagnostics (Kestrel derriere reverse proxy Apache, HTTPS).'
$DossiersModifier = @(
'Logs\DiagnosticsWeb'
)
try {
Assert-OmikaAdmin
$chemins = Get-OmikaPaths -InstallRoot $InstallRoot
$dossierPublie = Join-Path $chemins.Apps $DossierApps
$cheminExe = Join-Path $dossierPublie $Executable
# --- Rollback ------------------------------------------------------------
if ($Desinstaller) {
Write-OmikaStep "Desinstallation du service $ServiceName"
Remove-OmikaService -Name $ServiceName -WhatIf:$Simulation | Out-Null
Write-OmikaServiceSummary -Name $ServiceName -Supprime
exit 0
}
# --- 1. Dossiers ---------------------------------------------------------
Write-OmikaStep 'Dossiers du service'
foreach ($relatif in $DossiersModifier) {
New-OmikaDirectory -Path (Join-Path $InstallRoot $relatif) -WhatIf:$Simulation | Out-Null
}
# --- 2. ACL --------------------------------------------------------------
Write-OmikaStep 'ACL NTFS minimales'
foreach ($relatif in $DossiersModifier) {
Grant-OmikaAcl -Path (Join-Path $InstallRoot $relatif) -Account $Account -Right 'M' `
-RequireExisting -WhatIf:$Simulation | Out-Null
}
# --- 3. Controle default-deny -------------------------------------------
Write-OmikaStep 'Controle de la publication (default-deny)'
if (Test-Path -LiteralPath $dossierPublie) {
$fichiersInterdits = @('appsettings.Development.json', 'appsettings.Test.json')
$trouve = $false
foreach ($nom in $fichiersInterdits) {
$chemin = Join-Path $dossierPublie $nom
if (Test-Path -LiteralPath $chemin) {
Write-OmikaWarn "Fichier de developpement PRESENT dans la publication : $nom"
Write-OmikaWarn ' -> la configuration de developpement peut desactiver les protections Production.'
Write-OmikaWarn ' -> supprimez-le de la publication avant de demarrer le service.'
$trouve = $true
}
}
if (-not $trouve) {
Write-OmikaOk 'Aucun fichier de configuration de developpement dans la publication.'
}
}
else {
Write-OmikaWarn "Publication absente : $dossierPublie (deployez les binaires d'abord)."
}
# --- 4. Compte de service ------------------------------------------------
Write-OmikaStep 'Compte de service'
Assert-OmikaServiceAccount -Account $Account | Out-Null
# --- 5. Service Windows --------------------------------------------------
Write-OmikaStep "Creation du service Windows ($StartupType)"
Install-OmikaService -Name $ServiceName -DisplayName $NomAffiche `
-ExePath $cheminExe -Account $Account -Description $Description `
-StartupType $StartupType -WhatIf:$Simulation | Out-Null
# --- 6. Recapitulatif ----------------------------------------------------
Write-OmikaServiceSummary -Name $ServiceName -ExePath $cheminExe -Account $Account -Rappels @(
'Injecter les secrets en variables MACHINE (aucun secret dans ce script) :'
' ConnectionStrings__DiagnosticsDatabase=Host=127.0.0.1;Port=5432;Database=omika_diagnostics;Username=omika_diagnostics_web;Password=<SECRET>'
' ASPNETCORE_ENVIRONMENT=Production (OBLIGATOIRE : default-deny, PAS Development)'
' ASPNETCORE_URLS=http://127.0.0.1:<PORT_WEB>'
' DiagnosticsWeb__PathBase=<chemin public si servi sous un sous-chemin>'
'Premier Admin (DEPLOIEMENT_PRODUCTION_DIAGNOSTICS.md §22) :'
' 1. configurer TEMPORAIREMENT DiagnosticsAuth__Operators__0__Username / __PasswordHash / __Roles__0=Admin'
' 2. demarrer, se connecter, creer l''Admin DB dans /Users'
' 3. RETIRER l''operateur bootstrap (DiagnosticsAuth:Operators = []) puis redemarrer'
' 4. confirmer que le bootstrap est refuse et que l''Admin DB fonctionne'
'Les mots de passe ne sont JAMAIS dans un appsettings versionne (hash uniquement).'
'Exposer en HTTPS via le reverse proxy Apache, jamais Kestrel en direct.'
)
exit 0
}
catch {
Write-OmikaFail $_.Exception.Message
exit 1
}
@@ -0,0 +1,146 @@
# ============================================================
# Omika - Installation du service Windows OmikaDiagnosticsWorker (deploiement 3/6)
# ============================================================
# CIBLE : Windows Server 2025 (SRV-OMIKA), PowerShell ADMINISTRATEUR.
# NE PAS executer depuis le poste de developpement.
#
# CE QUE CE SCRIPT FAIT :
# 1. Cree le dossier de checkpoint (propre au Worker) + journalisation.
# 2. Pose les ACL :
# - Modifier sur ses propres dossiers (checkpoint, logs) ;
# - LECTURE SEULE sur le spool JSONL de l'API et le spool central.
# 3. Valide le compte de service (existence + droit de connexion).
# 4. Cree le service Windows OmikaDiagnosticsWorker (idempotent, Manual).
# 5. Verifie ce qui a ete reellement cree.
#
# CE QU'IL NE FAIT PAS :
# - AUCUN demarrage de service.
# - AUCUN secret ecrit, AUCUNE migration PostgreSQL appliquee.
# - AUCUN demarrage du Worker pendant une migration (l'ordre est manuel).
#
# PREREQUIS MANUELS :
# - Compte local dedie + droit « Ouvrir une session en tant que service ».
# - Publication copiee dans <InstallRoot>\Apps\DiagnosticsWorker
# (Omika.Diagnostics.Worker.exe).
# - PostgreSQL joignable ET migrations 001 -> 010 appliquees
# (l'ordre sur est : migration d'abord, binaires ensuite).
#
# IDEMPOTENT - prend en charge -WhatIf.
#
# EXEMPLES :
# .\Install-OmikaDiagnosticsWorker.ps1
# .\Install-OmikaDiagnosticsWorker.ps1 -WhatIf
# .\Install-OmikaDiagnosticsWorker.ps1 -Desinstaller
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$InstallRoot = 'C:\Omika',
[string]$ServiceName = 'OmikaDiagnosticsWorker',
[string]$Account = '.\svc_omika_diag',
# Compte du service OmikaApi : proprietaire du spool JSONL.
[string]$AccountApi = '.\svc_omika_api',
# Compte du service OmikaDiagnosticsApi : proprietaire du spool central.
[string]$AccountDiagApi = '.\svc_omika_diagapi',
[ValidateSet('Manual', 'Automatic', 'Disabled')]
[string]$StartupType = 'Manual',
[switch]$Desinstaller
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'Omika.Deploy.Common.psm1') -Force
# Le module a son propre etat de session : $WhatIfPreference ne s'y propage pas.
$Simulation = [bool]$WhatIfPreference
# --- Definition du service ---------------------------------------------------
$NomAffiche = 'Omika Diagnostics Worker'
$DossierApps = 'DiagnosticsWorker'
$Executable = 'Omika.Diagnostics.Worker.exe'
$Description = 'Omika - Importateur asynchrone des diagnostics JSONL vers PostgreSQL (fail-open, checkpoint persistant).'
# Dossiers que CE service possede (crees ici + droit Modifier).
$DossiersModifier = @(
'Logs\DiagnosticsWorker',
'Data\DiagnosticsWorker\Checkpoint'
)
# Dossiers d'AUTRES services : lecture seule, jamais crees ici.
# Absents = normal si le service proprietaire n'est pas encore deploye.
$DossiersLecture = @(
@{ Relatif = 'Data\Api\Diagnostics\Jsonl'; Proprietaire = 'Install-OmikaApi.ps1' },
@{ Relatif = 'Data\DiagnosticsApi\Central'; Proprietaire = 'Install-OmikaDiagnosticsApi.ps1' }
)
try {
Assert-OmikaAdmin
$chemins = Get-OmikaPaths -InstallRoot $InstallRoot
$cheminExe = Join-Path (Join-Path $chemins.Apps $DossierApps) $Executable
# --- Rollback ------------------------------------------------------------
if ($Desinstaller) {
Write-OmikaStep "Desinstallation du service $ServiceName"
Remove-OmikaService -Name $ServiceName -WhatIf:$Simulation | Out-Null
Write-OmikaServiceSummary -Name $ServiceName -Supprime
exit 0
}
# --- 1. Dossiers propres -------------------------------------------------
Write-OmikaStep 'Dossiers du service'
foreach ($relatif in $DossiersModifier) {
New-OmikaDirectory -Path (Join-Path $InstallRoot $relatif) -WhatIf:$Simulation | Out-Null
}
# --- 2. ACL --------------------------------------------------------------
Write-OmikaStep 'ACL NTFS minimales'
# 2a. Ses propres dossiers : droit Modifier.
foreach ($relatif in $DossiersModifier) {
Grant-OmikaAcl -Path (Join-Path $InstallRoot $relatif) -Account $Account -Right 'M' `
-RequireExisting -WhatIf:$Simulation | Out-Null
}
# 2b. Lecture seule sur les spools produits par d'autres services.
foreach ($entree in $DossiersLecture) {
$cible = Join-Path $InstallRoot $entree.Relatif
if (-not (Test-Path -LiteralPath $cible)) {
Write-OmikaWarn "Spool absent : $cible"
Write-OmikaWarn (" -> normal si {0} n'est pas encore deploye ; relancez ce script ensuite." -f $entree.Proprietaire)
continue
}
Grant-OmikaAcl -Path $cible -Account $Account -Right 'R' -WhatIf:$Simulation | Out-Null
}
# --- 3. Compte de service ------------------------------------------------
Write-OmikaStep 'Compte de service'
Assert-OmikaServiceAccount -Account $Account | Out-Null
# --- 4. Service Windows --------------------------------------------------
Write-OmikaStep "Creation du service Windows ($StartupType)"
Install-OmikaService -Name $ServiceName -DisplayName $NomAffiche `
-ExePath $cheminExe -Account $Account -Description $Description `
-StartupType $StartupType -WhatIf:$Simulation | Out-Null
# --- 5. Recapitulatif ----------------------------------------------------
Write-OmikaServiceSummary -Name $ServiceName -ExePath $cheminExe -Account $Account -Rappels @(
'Injecter les secrets en variables MACHINE (aucun secret dans ce script) :'
' ConnectionStrings__DiagnosticsDatabase=Host=127.0.0.1;Port=5432;Database=omika_diagnostics;Username=omika_diagnostics_worker;Password=<SECRET>'
' Observability__Remote__Directory=<IDENTIQUE a celui de Omika.Api>'
' Diagnostics__Import__CheckpointDirectory=<dossier Checkpoint du service>'
' Diagnostics__RetentionDays / Diagnostics__RetentionIntervalHours / Diagnostics__RetentionBatchSize'
'Appliquer les migrations PostgreSQL 001 -> 010 dans l''ordre AVANT de demarrer.'
'Verifier que la migration de retention (004) est appliquee (DELETE/SELECT).'
'Le spool central n''est utile qu''en architecture multi-serveur.'
)
exit 0
}
catch {
Write-OmikaFail $_.Exception.Message
exit 1
}
+166
View File
@@ -0,0 +1,166 @@
# ============================================================
# Omika - Installation du service Windows OmikaLanguesApi (deploiement 6/6)
# ============================================================
# CIBLE : Windows Server 2025 (SRV-OMIKA), PowerShell ADMINISTRATEUR.
# NE PAS executer depuis le poste de developpement.
#
# A QUOI SERT CE SERVICE :
# Service dedie au telechargement des modeles de langue ONNX (traduction hors
# ligne mobile). STATELESS : aucun acces HFSQL, aucune authentification
# (les modeles sont des fichiers generiques publics). Le mobile obtient son
# URL de base via le parametre HFSQL SRV_LANGUES, expose par Omika.Api au
# bootstrap.
#
# CE QUE CE SCRIPT FAIT :
# 1. Cree le dossier des modeles et le dossier de journalisation.
# 2. Pose les ACL NTFS minimales : LECTURE SEULE sur les modeles
# (le service ne fait que les lire et les streamer).
# 3. Valide le compte de service (existence + droit de connexion).
# 4. CONTROLE la disponibilite du port loopback, si -Port est fourni.
# 5. Cree le service Windows OmikaLanguesApi (idempotent, Manual).
# 6. Verifie ce qui a ete reellement cree.
#
# CE QU'IL NE FAIT PAS :
# - AUCUN demarrage de service.
# - AUCUN port choisi en dur : voir -Port et DEPLOIEMENT_API_LANGUES.md §6bis.
# - AUCUN modele telecharge, AUCUN manifest.json ecrit.
# - AUCUNE ligne SRV_LANGUES ecrite dans HFSQL.
#
# PREREQUIS MANUELS :
# - Compte local dedie + droit « Ouvrir une session en tant que service ».
# - Publication copiee dans <InstallRoot>\Apps\LanguesApi.
# - Le service doit contenir UseWindowsService() : sans lui, le demarrage
# echoue avec l'erreur 1053 (« le service n'a pas repondu a temps »).
#
# IDEMPOTENT - prend en charge -WhatIf.
#
# EXEMPLES :
# .\Install-OmikaLanguesApi.ps1 -Port 5099
# .\Install-OmikaLanguesApi.ps1 -Port 5099 -WhatIf
# .\Install-OmikaLanguesApi.ps1 -Desinstaller
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$InstallRoot = 'C:\Omika',
[string]$ServiceName = 'OmikaLanguesApi',
[string]$Account = '.\svc_omika_langues',
# Port loopback a verifier AVANT de le fixer dans ASPNETCORE_URLS.
# Aucune valeur par defaut : le port se decide contre le plan Apache
# (DEPLOIEMENT_API_LANGUES.md §6bis), jamais en recopiant un exemple.
[ValidateRange(1, 65535)]
[int]$Port = 0,
# Dossier racine des modeles (variable Langues__RootPath).
[string]$RootPathLangues,
[ValidateSet('Manual', 'Automatic', 'Disabled')]
[string]$StartupType = 'Manual',
[switch]$Desinstaller
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'Omika.Deploy.Common.psm1') -Force
# Le module a son propre etat de session : $WhatIfPreference ne s'y propage pas.
$Simulation = [bool]$WhatIfPreference
# --- Definition du service ---------------------------------------------------
$NomAffiche = 'Omika Langues Api'
$DossierApps = 'LanguesApi'
$Executable = 'Omika.Langues.Api.exe'
$Description = 'Omika - API de telechargement des modeles de langue (ONNX) pour la traduction hors ligne mobile. Anonyme, sans base de donnees.'
if (-not $RootPathLangues) { $RootPathLangues = Join-Path $InstallRoot 'Data\Langues' }
# Lecture seule : le service streame des fichiers, il n'ecrit jamais dans les modeles.
$DossiersLecture = @(
$RootPathLangues
)
try {
Assert-OmikaAdmin
$chemins = Get-OmikaPaths -InstallRoot $InstallRoot
$cheminExe = Join-Path (Join-Path $chemins.Apps $DossierApps) $Executable
# --- Rollback ------------------------------------------------------------
if ($Desinstaller) {
Write-OmikaStep "Desinstallation du service $ServiceName"
Remove-OmikaService -Name $ServiceName -WhatIf:$Simulation | Out-Null
Write-OmikaServiceSummary -Name $ServiceName -Supprime
exit 0
}
# --- 1. Dossiers ---------------------------------------------------------
Write-OmikaStep 'Dossiers du service'
New-OmikaDirectory -Path (Join-Path $InstallRoot 'Logs\LanguesApi') -WhatIf:$Simulation | Out-Null
New-OmikaDirectory -Path $RootPathLangues -WhatIf:$Simulation | Out-Null
# --- 2. ACL --------------------------------------------------------------
Write-OmikaStep 'ACL NTFS minimales'
Grant-OmikaAcl -Path (Join-Path $InstallRoot 'Logs\LanguesApi') -Account $Account -Right 'M' `
-RequireExisting -WhatIf:$Simulation | Out-Null
# Modeles : LECTURE SEULE. Le service ne doit jamais pouvoir les modifier.
foreach ($dossier in $DossiersLecture) {
Grant-OmikaAcl -Path $dossier -Account $Account -Right 'R' `
-RequireExisting -WhatIf:$Simulation | Out-Null
}
# --- 3. Compte de service ------------------------------------------------
Write-OmikaStep 'Compte de service'
Assert-OmikaServiceAccount -Account $Account | Out-Null
# --- 4. Port loopback ----------------------------------------------------
if ($Port -gt 0) {
Write-OmikaStep "Verification du port loopback $Port"
$verdict = Test-OmikaPortLibre -Port $Port
if ($verdict.Libre) {
Write-OmikaOk "Port $Port disponible : peut etre fixe dans ASPNETCORE_URLS."
}
else {
Write-OmikaWarn "Port $Port NON disponible : $($verdict.Raisons)"
Write-OmikaWarn ' -> choisissez un autre port AVANT de configurer le service,'
Write-OmikaWarn ' et reportez le meme numero dans le ProxyPass du vhost Apache.'
}
}
else {
Write-OmikaStep 'Port loopback (non verifie)'
Write-OmikaInfo 'Aucun -Port fourni : le port se decide contre le plan Apache'
Write-OmikaInfo ' (DEPLOIEMENT_API_LANGUES.md §6bis). Relancez avec -Port <numero> pour le verifier.'
}
# --- 5. Service Windows --------------------------------------------------
Write-OmikaStep "Creation du service Windows ($StartupType)"
Install-OmikaService -Name $ServiceName -DisplayName $NomAffiche `
-ExePath $cheminExe -Account $Account -Description $Description `
-StartupType $StartupType -WhatIf:$Simulation | Out-Null
# --- 6. Recapitulatif ----------------------------------------------------
$rappelPort = 'ASPNETCORE_URLS=http://127.0.0.1:<PORT_LANGUES> (voir §6bis, aucune valeur en dur)'
if ($Port -gt 0) {
$rappelPort = "ASPNETCORE_URLS=http://127.0.0.1:$Port (verifie disponible par ce script)"
}
Write-OmikaServiceSummary -Name $ServiceName -ExePath $cheminExe -Account $Account -Rappels @(
'Injecter la configuration en variables MACHINE (aucun secret : ce service est anonyme) :'
" Langues__RootPath=$RootPathLangues"
" $rappelPort"
' ASPNETCORE_ENVIRONMENT=Production'
'Ce service ne consomme AUCUN secret : ni HFSQL, ni PostgreSQL, ni cle.'
'Reporter le MEME port dans le ProxyPass du vhost Apache (expose en HTTPS).'
'Deployer les modeles + manifest.json dans Langues__RootPath (aucun redemarrage requis ensuite).'
'Ecrire la ligne HFSQL ParametreSysteme SRV_LANGUES = URL publique du service.'
'Le service est ANONYME : ne pas l''exposer sur Internet sans le reverse proxy.'
)
exit 0
}
catch {
Write-OmikaFail $_.Exception.Message
exit 1
}
+318
View File
@@ -0,0 +1,318 @@
# ============================================================
# 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.'
+252
View File
@@ -0,0 +1,252 @@
# ============================================================
# Omika - Installation de TOUS les services Windows (orchestrateur)
# ============================================================
# CIBLE : Windows Server 2025 (SRV-OMIKA), PowerShell ADMINISTRATEUR.
# NE PAS executer depuis le poste de developpement.
#
# Ce script ne fait plus le travail lui-meme : il appelle, DANS L'ORDRE, un
# script dedie par deploiement. Chaque service reste installable seul.
#
# 1. Install-OmikaApi.ps1 (spool JSONL de l'API)
# 2. Install-OmikaWorker.ps1
# 3. Install-OmikaDiagnosticsApi.ps1 (spool central) [optionnel]
# 4. Install-OmikaDiagnosticsWorker.ps1 (lit les 2 spools)
# 5. Install-OmikaDiagnosticsWeb.ps1
# 6. Install-OmikaLanguesApi.ps1 [optionnel]
#
# POURQUOI CET ORDRE : les ACL inter-services (le Worker Diagnostics lit le
# spool produit par l'API et celui produit par l'API centrale) ne peuvent etre
# posees que si le dossier existe deja. L'ancien script monolithique posait les
# ACL en bloc et IGNORAIT SILENCIEUSEMENT les dossiers absents ; ici, le
# proprietaire du dossier est installe AVANT ses lecteurs.
#
# CE QUE CE SCRIPT NE FAIT PAS :
# - AUCUN demarrage de service (doctrine : activation != demarrage).
# - AUCUN secret ecrit, AUCUNE regle firewall, AUCUN IIS, AUCUNE base modifiee.
#
# PREREQUIS MANUELS :
# - Comptes locaux dedies crees + droit « Ouvrir une session en tant que
# service » (chaque script le verifie et avertit).
# - Publications copiees dans <InstallRoot>\Apps (Install-OmikaServerFiles.ps1).
#
# IDEMPOTENT - prend en charge -WhatIf.
#
# EXEMPLES :
# .\Install-OmikaServices.ps1
# .\Install-OmikaServices.ps1 -WhatIf
# .\Install-OmikaServices.ps1 -InclureLangues -PortLangues 5099
# .\Install-OmikaServices.ps1 -SansDiagnosticsApi # serveur mono-serveur
# .\Install-OmikaServices.ps1 -Desinstaller
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$InstallRoot = 'C:\Omika',
# --- Noms de services ----------------------------------------------------
[string]$ApiServiceName = 'OmikaApi',
[string]$WorkerServiceName = 'OmikaWorker',
[string]$DiagServiceName = 'OmikaDiagnosticsWorker',
[string]$DiagWebServiceName = 'OmikaDiagnosticsWeb',
[string]$DiagApiServiceName = 'OmikaDiagnosticsApi',
[string]$LanguesServiceName = 'OmikaLanguesApi',
# --- Comptes de service (jamais LocalSystem, jamais un compte humain) ----
[string]$ApiAccount = '.\svc_omika_api',
[string]$WorkerAccount = '.\svc_omika_worker',
[string]$DiagAccount = '.\svc_omika_diag',
[string]$DiagWebAccount = '.\svc_omika_diagweb',
[string]$DiagApiAccount = '.\svc_omika_diagapi',
[string]$LanguesAccount = '.\svc_omika_langues',
[ValidateSet('Manual', 'Automatic', 'Disabled')]
[string]$StartupType = 'Manual',
# --- Perimetre -----------------------------------------------------------
# Le service Langues n'existe que si la traduction hors ligne mobile est deployee.
[switch]$InclureLangues,
# Sur un serveur MONO-SERVEUR, l'API centrale de diagnostics est inutile
# (l'API metier ecrit son JSONL localement, sans forwarding).
[switch]$SansDiagnosticsApi,
# Port loopback a verifier avant de le fixer dans ASPNETCORE_URLS (§6bis).
[ValidateRange(0, 65535)]
[int]$PortLangues = 0,
# --- Rollback ------------------------------------------------------------
[switch]$Desinstaller
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'Omika.Deploy.Common.psm1') -Force
# Le module a son propre etat de session : $WhatIfPreference ne s'y propage pas.
$Simulation = [bool]$WhatIfPreference
try {
Assert-OmikaAdmin
Write-OmikaStep 'Perimetre du deploiement'
# --- Composition de la sequence -----------------------------------------
$sequence = New-Object System.Collections.Generic.List[object]
$sequence.Add(@{
Script = 'Install-OmikaApi.ps1'
Libelle = 'OmikaApi'
Parametres = @{
InstallRoot = $InstallRoot
ServiceName = $ApiServiceName
Account = $ApiAccount
StartupType = $StartupType
}
})
$sequence.Add(@{
Script = 'Install-OmikaWorker.ps1'
Libelle = 'OmikaWorker'
Parametres = @{
InstallRoot = $InstallRoot
ServiceName = $WorkerServiceName
Account = $WorkerAccount
StartupType = $StartupType
}
})
if (-not $SansDiagnosticsApi) {
$sequence.Add(@{
Script = 'Install-OmikaDiagnosticsApi.ps1'
Libelle = 'OmikaDiagnosticsApi'
Parametres = @{
InstallRoot = $InstallRoot
ServiceName = $DiagApiServiceName
Account = $DiagApiAccount
AccountWorker = $DiagAccount
StartupType = $StartupType
}
})
}
else {
Write-OmikaInfo 'OmikaDiagnosticsApi EXCLU (-SansDiagnosticsApi).'
}
$sequence.Add(@{
Script = 'Install-OmikaDiagnosticsWorker.ps1'
Libelle = 'OmikaDiagnosticsWorker'
Parametres = @{
InstallRoot = $InstallRoot
ServiceName = $DiagServiceName
Account = $DiagAccount
AccountApi = $ApiAccount
AccountDiagApi = $DiagApiAccount
StartupType = $StartupType
}
})
$sequence.Add(@{
Script = 'Install-OmikaDiagnosticsWeb.ps1'
Libelle = 'OmikaDiagnosticsWeb'
Parametres = @{
InstallRoot = $InstallRoot
ServiceName = $DiagWebServiceName
Account = $DiagWebAccount
StartupType = $StartupType
}
})
if ($InclureLangues) {
$parametresLangues = @{
InstallRoot = $InstallRoot
ServiceName = $LanguesServiceName
Account = $LanguesAccount
StartupType = $StartupType
}
if ($PortLangues -gt 0) { $parametresLangues['Port'] = $PortLangues }
$sequence.Add(@{
Script = 'Install-OmikaLanguesApi.ps1'
Libelle = 'OmikaLanguesApi'
Parametres = $parametresLangues
})
}
else {
Write-OmikaInfo 'OmikaLanguesApi EXCLU (ajoutez -InclureLangues pour l''installer).'
}
$mode = 'INSTALLATION'
if ($Desinstaller) { $mode = 'DESINSTALLATION' }
if ($Simulation) { $mode += ' (SIMULATION -WhatIf)' }
Write-OmikaInfo ("{0} - {1} service(s) dans la sequence" -f $mode, $sequence.Count)
# --- Execution -----------------------------------------------------------
$resultats = @()
foreach ($etape in $sequence) {
$nomService = [string]$etape.Parametres['ServiceName']
$cheminScript = Join-Path $PSScriptRoot $etape.Script
if (-not (Test-Path -LiteralPath $cheminScript)) {
Write-OmikaFail ("Script introuvable : {0} (service {1} ignore)" -f $etape.Script, $nomService)
$resultats += [pscustomobject]@{ Service = $nomService; Resultat = 'SCRIPT ABSENT' }
continue
}
$parametres = $etape.Parametres.Clone()
$parametres['WhatIf'] = $Simulation
if ($Desinstaller) { $parametres['Desinstaller'] = $true }
& $cheminScript @parametres
$code = $LASTEXITCODE
if ($code -eq 0) {
$resultats += [pscustomobject]@{ Service = $nomService; Resultat = 'OK' }
}
else {
$resultats += [pscustomobject]@{ Service = $nomService; Resultat = ("ECHEC (code $code)") }
}
}
# --- Recapitulatif -------------------------------------------------------
Write-Host ''
Write-Host '=== RECAPITULATIF ===' -ForegroundColor Cyan
foreach ($ligne in $resultats) {
$couleur = 'Red'
if ($ligne.Resultat -eq 'OK') { $couleur = 'Green' }
Write-Host (" {0,-26} {1}" -f $ligne.Service, $ligne.Resultat) -ForegroundColor $couleur
}
Write-Host ''
Write-Host 'Etat reel des services :' -ForegroundColor Cyan
$noms = @($sequence | ForEach-Object { [string]$_.Parametres['ServiceName'] })
Get-Service | Where-Object { $noms -contains $_.Name } |
Select-Object Name, Status, StartType | Format-Table -AutoSize
$echecs = @($resultats | Where-Object { $_.Resultat -ne 'OK' })
Write-Host ''
if ($echecs.Count -eq 0) {
Write-Host 'ACTIVATION TERMINEE (services MANUAL + STOPPED).' -ForegroundColor Green
Write-Host 'AUCUN service demarre : le demarrage reste une action MANUELLE.' -ForegroundColor Yellow
Write-Host ''
Write-Host 'Ordre de demarrage (consommateurs avant producteurs) :' -ForegroundColor Yellow
Write-Host ' OmikaDiagnosticsWorker -> OmikaDiagnosticsWeb -> OmikaDiagnosticsApi -> OmikaWorker -> OmikaApi' -ForegroundColor Yellow
}
else {
Write-Host ("ACTIVATION INCOMPLETE : {0} etape(s) en echec." -f $echecs.Count) -ForegroundColor Red
Write-Host 'Corrigez chaque echec, puis relancez ce script (idempotent).' -ForegroundColor Red
}
Write-Host ''
Write-Host 'RAPPELS (jamais automatises ici) :' -ForegroundColor Yellow
Write-Host ' - Injecter les secrets par service (voir les rappels de chaque script).'
Write-Host ' - Verifier le provider OLE DB PCSOFT.HFSQL (64 bits) enregistre.'
Write-Host ' - Installer LibreOffice si la generation PDF des rapports est utilisee.'
Write-Host ' - Appliquer les migrations PostgreSQL 001 -> 010 (retention incluse) avant demarrage.'
if ($echecs.Count -gt 0) { exit 1 }
exit 0
}
catch {
Write-OmikaFail $_.Exception.Message
exit 1
}
+124
View File
@@ -0,0 +1,124 @@
# ============================================================
# Omika - Installation du service Windows OmikaWorker (deploiement 2/6)
# ============================================================
# CIBLE : Windows Server 2025 (SRV-OMIKA), PowerShell ADMINISTRATEUR.
# NE PAS executer depuis le poste de developpement.
#
# CE QUE CE SCRIPT FAIT :
# 1. Cree les dossiers propres au Worker (journalisation, spool local).
# 2. Pose les ACL NTFS minimales (avec CONTROLE du code retour d'icacls).
# 3. Valide le compte de service (existence + droit de connexion).
# 4. Cree le service Windows OmikaWorker (idempotent, StartupType Manual).
# 5. Verifie ce qui a ete reellement cree (binaire, compte, etat).
#
# CE QU'IL NE FAIT PAS :
# - AUCUN demarrage de service.
# - AUCUN secret ecrit (variables d'environnement = action manuelle).
# - AUCUNE modification HFSQL / PostgreSQL / firewall.
# - AUCUNE copie de binaires.
#
# PREREQUIS MANUELS :
# - Compte local dedie + droit « Ouvrir une session en tant que service ».
# - Publication copiee dans <InstallRoot>\Apps\OmikaWorker (Omika.Worker.exe).
#
# NOTE SPOOL LOCAL :
# Le dossier Data\Worker\Diagnostics\Jsonl n'est utilise QUE si ce Worker doit
# forwarder ses propres diagnostics (cas serveur SEPARE de l'API, cf.
# docs\T15-RUNBOOK-DEPLOIEMENT-PRODUCTION.md §7.3). Sur un serveur unique,
# seul Omika.Api forwarde : ne pas activer DiagnosticsForwarding ici, sinon
# les evenements partiront DEUX FOIS (un par service).
#
# IDEMPOTENT - prend en charge -WhatIf.
#
# EXEMPLES :
# .\Install-OmikaWorker.ps1
# .\Install-OmikaWorker.ps1 -WhatIf
# .\Install-OmikaWorker.ps1 -Desinstaller
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$InstallRoot = 'C:\Omika',
[string]$ServiceName = 'OmikaWorker',
[string]$Account = '.\svc_omika_worker',
[ValidateSet('Manual', 'Automatic', 'Disabled')]
[string]$StartupType = 'Manual',
[switch]$Desinstaller
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'Omika.Deploy.Common.psm1') -Force
# Le module a son propre etat de session : $WhatIfPreference ne s'y propage pas.
$Simulation = [bool]$WhatIfPreference
# --- Definition du service ---------------------------------------------------
$NomAffiche = 'Omika Worker'
$DossierApps = 'OmikaWorker'
$Executable = 'Omika.Worker.exe'
$Description = 'Omika - Worker de synchronisation metier (HFSQL) : traitement de la file de synchronisation et des reprises.'
$DossiersModifier = @(
'Logs\Worker',
'Data\Worker\Diagnostics\Jsonl'
)
try {
Assert-OmikaAdmin
$chemins = Get-OmikaPaths -InstallRoot $InstallRoot
$cheminExe = Join-Path (Join-Path $chemins.Apps $DossierApps) $Executable
# --- Rollback ------------------------------------------------------------
if ($Desinstaller) {
Write-OmikaStep "Desinstallation du service $ServiceName"
Remove-OmikaService -Name $ServiceName -WhatIf:$Simulation | Out-Null
Write-OmikaServiceSummary -Name $ServiceName -Supprime
exit 0
}
# --- 1. Dossiers ---------------------------------------------------------
Write-OmikaStep 'Dossiers du service'
foreach ($relatif in $DossiersModifier) {
New-OmikaDirectory -Path (Join-Path $InstallRoot $relatif) -WhatIf:$Simulation | Out-Null
}
# --- 2. ACL --------------------------------------------------------------
Write-OmikaStep 'ACL NTFS minimales'
foreach ($relatif in $DossiersModifier) {
Grant-OmikaAcl -Path (Join-Path $InstallRoot $relatif) -Account $Account -Right 'M' `
-RequireExisting -WhatIf:$Simulation | Out-Null
}
# --- 3. Compte de service ------------------------------------------------
Write-OmikaStep 'Compte de service'
Assert-OmikaServiceAccount -Account $Account | Out-Null
# --- 4. Service Windows --------------------------------------------------
Write-OmikaStep "Creation du service Windows ($StartupType)"
Install-OmikaService -Name $ServiceName -DisplayName $NomAffiche `
-ExePath $cheminExe -Account $Account -Description $Description `
-StartupType $StartupType -WhatIf:$Simulation | Out-Null
# --- 5. Recapitulatif ----------------------------------------------------
Write-OmikaServiceSummary -Name $ServiceName -ExePath $cheminExe -Account $Account -Rappels @(
'Injecter les secrets en variables MACHINE (aucun secret dans ce script) :'
' Hfsql / SyncConnection (secrets HFSQL existants)'
' DiagnosticDeployment__ClientCode / __ServerInstanceId'
' ASPNETCORE_ENVIRONMENT=Production'
'UNIQUEMENT si ce Worker est sur un serveur DIFFERENT de l''API (multi-serveur) :'
' DiagnosticsForwarding__Enabled=true'
' DiagnosticsForwarding__Endpoint=https://<central>/api/v1/diagnostics/batches'
' DiagnosticsForwarding__ApiKey=<cle du client Worker>'
' Observability__Remote__Directory=<dossier LOCAL au Worker>'
'Sur un serveur unique : laisser DiagnosticsForwarding desactive (sinon double envoi).'
)
exit 0
}
catch {
Write-OmikaFail $_.Exception.Message
exit 1
}
+525
View File
@@ -0,0 +1,525 @@
# ============================================================
# Omika - Generation / verification du manifeste des packs de langue
# (manifest.json de Omika.Langues.Api)
# ============================================================
# Le manifeste decrit les packs ONNX telechargeables par le mobile. Ce n'est PAS
# un fichier de publication : il decrit les fichiers REELLEMENT presents sous
# Langues__RootPath. Il se genere donc la ou sont les modeles, pas dans le depot.
#
# Arborescence attendue : <racine>\<code>\<sens>\<fichier>, ou <sens> vaut
# versFrancais, depuisFrancais, ou commun (poids partages par les deux sens d'un
# modele familial ; le mobile ne les telecharge qu'une fois).
#
# Deux champs ne peuvent pas etre saisis a la main sans risque :
# - tailleOctets : taille reelle (progression du telechargement mobile) ;
# - sha256 : empreinte d'integrite, reverifiee par le mobile APRES
# telechargement. Une empreinte fausse ou absente ne se voit
# QUE sur le telephone : le pack est refuse et pack.json
# n'est jamais ecrit (aucune erreur ne remonte au serveur).
# Ce script les calcule depuis les fichiers. Aucune valeur n'est inventee.
#
# Usage :
# .\New-OmikaLanguesManifeste.ps1 # genere sous RootPath
# .\New-OmikaLanguesManifeste.ps1 -WhatIf # simulation, affiche
# .\New-OmikaLanguesManifeste.ps1 -RootPathLangues D:\Modeles
# .\New-OmikaLanguesManifeste.ps1 -Verifier # controle d'integrite
# .\New-OmikaLanguesManifeste.ps1 -Libelles @{ es = 'Espagnol (Espagne)' }
#
# -Verifier compare le manifeste EXISTANT aux fichiers du disque et sort en
# erreur (code 1) au moindre ecart. Aucun droit administrateur n'est requis :
# c'est le controle a passer apres chaque ajout ou remplacement de modele.
#
# La racine par defaut est lue dans Langues__RootPath (MACHINE), donc la meme
# source de verite que le service : le manifeste est ecrit la ou l'API le lit.
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param(
# Dossier racine des modeles. Par defaut : variable MACHINE Langues__RootPath,
# puis C:\Omika\Data\Langues (recommandation du DEPLOIEMENT_API_LANGUES.md §6).
[string]$RootPathLangues,
# Fichier manifeste. Par defaut : <RootPathLangues>\manifest.json.
[string]$Sortie,
# Version ecrite dans chaque pack (le mobile la compare a celle installee).
[string]$Version = '1.0.0',
# Libelles affiches, par code de langue. Complete/ecrase la table par defaut.
[hashtable]$Libelles = @{},
# Autorise l'ecriture d'un manifeste SANS aucun pack (deconseille : le mobile
# affiche alors « Aucun modele de langue n'est disponible sur le serveur »).
[switch]$AutoriserVide,
# Controle le manifeste existant au lieu de le regenerer.
[switch]$Verifier
)
$ErrorActionPreference = 'Stop'
# Sens canoniques : le mobile compare ces valeurs de facon SENSIBLE A LA CASSE.
# « commun » porte les poids PARTAGES par les deux sens d'un modele familial
# (opus-mt-tc-big-itc-itc) : le telephone les telecharge une seule fois, et
# chaque sens ne contient que son modele.json (jeton de cible).
$SensCanoniques = @('versFrancais', 'depuisFrancais', 'commun')
# Noms de vocabulaire reconnus par l'application (OnnxTranslationService) :
# vocab.spm = vocabulaire commun, source.spm / target.spm = vocabulaire par
# langue d'un modele Marian/OPUS-MT. Tout autre nom .spm n'est qu'un repli.
$NomsVocabulaire = @('vocab.spm', 'source.spm', 'target.spm')
$LibellesParDefaut = @{
'ar' = 'Arabe'
'bg' = 'Bulgare'
'ca' = 'Catalan'
'cs' = 'Tchèque'
'da' = 'Danois'
'de' = 'Allemand'
'el' = 'Grec'
'en' = 'Anglais'
'es' = 'Espagnol'
'et' = 'Estonien'
'fi' = 'Finnois'
'gl' = 'Galicien'
'hr' = 'Croate'
'hu' = 'Hongrois'
'is' = 'Islandais'
'it' = 'Italien'
'lt' = 'Lituanien'
'lv' = 'Letton'
'mk' = 'Macédonien'
'mt' = 'Maltais'
'nl' = 'Néerlandais'
'no' = 'Norvégien'
'pl' = 'Polonais'
'pt' = 'Portugais'
'ro' = 'Roumain'
'ru' = 'Russe'
'sk' = 'Slovaque'
'sl' = 'Slovène'
'sv' = 'Suédois'
'tr' = 'Turc'
'uk' = 'Ukrainien'
'zh' = 'Chinois'
}
function Write-ManifesteInfo([string]$message) { Write-Host (' ' + $message) }
function Write-ManifesteOk([string]$message) { Write-Host ('[OK] ' + $message) -ForegroundColor Green }
function Write-ManifesteAlerte([string]$message) { Write-Host ('[ALERTE] ' + $message) -ForegroundColor Yellow }
function Write-ManifesteEchec([string]$message) { Write-Host ('[ECHEC] ' + $message) -ForegroundColor Red }
function Format-ManifesteTaille([long]$octets) {
if ($octets -ge 1073741824) { return ('{0:N2} Gio' -f ($octets / 1073741824)) }
if ($octets -ge 1048576) { return ('{0:N1} Mio' -f ($octets / 1048576)) }
if ($octets -ge 1024) { return ('{0:N0} Kio' -f ($octets / 1024)) }
return ('{0} octets' -f $octets)
}
# Code de langue : memes regles que LanguagePaths.EstCodeValide cote application
# (2 a 10 caracteres, minuscules ASCII, chiffres, tiret). Un code invalide fait
# IGNORER le pack SILENCIEUSEMENT par le mobile : on refuse de l'ecrire.
function Test-ManifesteCodeValide([string]$code) {
if ([string]::IsNullOrWhiteSpace($code)) { return $false }
$valeur = $code.Trim()
if ($valeur.Length -lt 2 -or $valeur.Length -gt 10) { return $false }
if ($valeur[0] -eq '-' -or $valeur[-1] -eq '-') { return $false }
foreach ($c in $valeur.ToCharArray()) {
$autorise = (($c -ge 'a' -and $c -le 'z') -or ($c -ge '0' -and $c -le '9') -or $c -eq '-')
if (-not $autorise) { return $false }
}
return $true
}
# Retourne le sens canonique, ou $null si le dossier n'est pas un sens connu.
function Resolve-ManifesteSens([string]$nomDossier) {
foreach ($canonique in $SensCanoniques) {
if ([string]::Equals($nomDossier, $canonique, [System.StringComparison]::OrdinalIgnoreCase)) {
return $canonique
}
}
return $null
}
function Resolve-ManifesteLibelle([string]$code, [hashtable]$surcharge) {
if ($surcharge -and $surcharge.ContainsKey($code)) { return [string]$surcharge[$code] }
if ($LibellesParDefaut.ContainsKey($code)) { return [string]$LibellesParDefaut[$code] }
return $code.ToUpperInvariant()
}
# SHA-256 hexadecimal minuscule : exactement ce que le mobile recalcule.
function Get-ManifesteEmpreinte([string]$chemin) {
return (Get-FileHash -LiteralPath $chemin -Algorithm SHA256).Hash.ToLowerInvariant()
}
# Echappe une chaine pour du JSON (guillemets, antislash, caracteres de controle).
function ConvertTo-ManifesteJsonChaine([string]$valeur) {
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append('"')
foreach ($c in $valeur.ToCharArray()) {
$code = [int]$c
if ($c -eq '"') { [void]$sb.Append('\"') }
elseif ($c -eq '\') { [void]$sb.Append('\\') }
elseif ($code -lt 32) { [void]$sb.Append(('\u{0:x4}' -f $code)) }
else { [void]$sb.Append($c) }
}
[void]$sb.Append('"')
return $sb.ToString()
}
function New-ManifesteBlocFichier([string]$sens, [string]$nomFichier, [long]$taille, [string]$sha256) {
$lignes = @(
' {'
(' "sens": ' + (ConvertTo-ManifesteJsonChaine $sens) + ',')
(' "nomFichier": ' + (ConvertTo-ManifesteJsonChaine $nomFichier) + ',')
(' "tailleOctets": ' + $taille + ',')
(' "sha256": ' + (ConvertTo-ManifesteJsonChaine $sha256))
' }'
)
return ($lignes -join "`r`n")
}
function New-ManifesteBlocPack([string]$code, [string]$libelle, [string]$version, $blocsFichiers) {
$lignes = @(
' {'
(' "code": ' + (ConvertTo-ManifesteJsonChaine $code) + ',')
(' "libelle": ' + (ConvertTo-ManifesteJsonChaine $libelle) + ',')
(' "version": ' + (ConvertTo-ManifesteJsonChaine $version) + ',')
' "fichiers": ['
($blocsFichiers -join (",`r`n"))
' ]'
' }'
)
return ($lignes -join "`r`n")
}
function New-ManifesteJsonText($blocsPacks) {
# Tableau vide ecrit « [] » et non « [\n\n] » : lisible et sans ligne blanche.
$corps = if ($blocsPacks.Count -eq 0) {
@(' "packs": []')
}
else {
@(
' "packs": ['
($blocsPacks -join (",`r`n"))
' ]'
)
}
$lignes = @('{') + $corps + @('}')
return (($lignes -join "`r`n") + "`r`n")
}
# JSON ecrit en UTF-8 SANS BOM : un manifeste est un fichier de donnees lu par
# l'API et par des outils externes, jamais un script.
function Write-ManifesteFichier([string]$chemin, [string]$contenu) {
$encodage = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($chemin, $contenu, $encodage)
}
function Write-ManifesteBilan($echecs, $alertes) {
foreach ($alerte in $alertes) { Write-ManifesteAlerte $alerte }
foreach ($echec in $echecs) { Write-ManifesteEchec $echec }
Write-Host ''
if ($echecs.Count -gt 0) {
Write-Host ("=== RESULTAT : {0} ecart(s), {1} alerte(s) ===" -f $echecs.Count, $alertes.Count) -ForegroundColor Red
return $false
}
Write-Host ("=== RESULTAT : conforme ({0} alerte(s)) ===" -f $alertes.Count) -ForegroundColor Green
return $true
}
# --- Racine effective --------------------------------------------------------
if (-not $RootPathLangues) {
$RootPathLangues = [Environment]::GetEnvironmentVariable('Langues__RootPath', 'Machine')
}
if (-not $RootPathLangues) {
$RootPathLangues = [Environment]::GetEnvironmentVariable('Langues__RootPath', 'Process')
}
if (-not $RootPathLangues) {
$RootPathLangues = 'C:\Omika\Data\Langues'
}
$racine = $RootPathLangues.Trim().TrimEnd([char[]]@('\', '/'))
if (-not $Sortie) { $Sortie = Join-Path $racine 'manifest.json' }
Write-Host '=== Manifeste des packs de langue (Omika Langues API) ==='
Write-ManifesteInfo ("Racine des modeles : {0}" -f $racine)
Write-ManifesteInfo ("Manifeste : {0}" -f $Sortie)
if ($Verifier) { Write-ManifesteInfo 'Mode : VERIFICATION (aucune ecriture)' }
Write-Host ''
if (-not (Test-Path -LiteralPath $racine -PathType Container)) {
Write-ManifesteEchec ("Racine introuvable : {0}" -f $racine)
Write-ManifesteInfo 'Verifier Langues__RootPath (MACHINE), ou passer -RootPathLangues.'
exit 1
}
$echecs = New-Object 'System.Collections.Generic.List[string]'
$alertes = New-Object 'System.Collections.Generic.List[string]'
# ============================================================================
# MODE VERIFICATION
# ============================================================================
if ($Verifier) {
if (-not (Test-Path -LiteralPath $Sortie -PathType Leaf)) {
Write-ManifesteEchec ("Manifeste ABSENT : {0}" -f $Sortie)
Write-ManifesteInfo 'Sans ce fichier, GET /api/langues repond 404 (et le mobile n''a aucune langue).'
Write-ManifesteInfo 'Generer avec : .\New-OmikaLanguesManifeste.ps1'
exit 1
}
$manifeste = $null
try {
$manifeste = (Get-Content -LiteralPath $Sortie -Raw -Encoding UTF8 | ConvertFrom-Json)
}
catch {
Write-ManifesteEchec ("Manifeste illisible (JSON invalide) : {0}" -f $_.Exception.Message)
Write-ManifesteInfo 'Cote service : « Manifeste des langues illisible » puis 404.'
exit 1
}
$packs = @($manifeste.packs)
if ($packs.Count -eq 0) {
$echecs.Add('Le manifeste ne declare aucun pack : le mobile affichera « Aucun modele de langue n''est disponible sur le serveur ».')
}
$declare = New-Object 'System.Collections.Generic.List[string]'
$nbFichiers = 0
$octetsTotal = [long]0
foreach ($pack in $packs) {
$code = ([string]$pack.code).Trim().ToLowerInvariant()
if (-not (Test-ManifesteCodeValide $code)) {
$echecs.Add(("Pack « {0} » : code de langue invalide -> IGNORE SILENCIEUSEMENT par le mobile." -f $pack.code))
continue
}
if ([string]::IsNullOrWhiteSpace([string]$pack.libelle)) {
$alertes.Add(("Pack « {0} » : libelle vide (le code s'affichera a la place)." -f $code))
}
if ([string]::IsNullOrWhiteSpace([string]$pack.version)) {
$alertes.Add(("Pack « {0} » : version vide (comparaison d'installation impossible)." -f $code))
}
$fichiers = @($pack.fichiers)
if ($fichiers.Count -eq 0) {
$echecs.Add(("Pack « {0} » : aucun fichier declare." -f $code))
continue
}
foreach ($fichier in $fichiers) {
$sensManifeste = [string]$fichier.sens
$sens = Resolve-ManifesteSens $sensManifeste
if (-not $sens) {
$echecs.Add(("Pack « {0} » : sens inconnu « {1} » (attendu versFrancais, depuisFrancais ou commun)." -f $code, $sensManifeste))
continue
}
if ($sensManifeste -cne $sens) {
$echecs.Add(("Pack « {0} » : sens « {1} » a la mauvaise casse -> REFUSE par le mobile (comparaison sensible a la casse)." -f $code, $sensManifeste))
continue
}
$nomFichier = [string]$fichier.nomFichier
if ([string]::IsNullOrWhiteSpace($nomFichier)) {
$echecs.Add(("Pack « {0} » : nomFichier vide." -f $code))
continue
}
if ($nomFichier -ne [System.IO.Path]::GetFileName($nomFichier)) {
$echecs.Add(("Pack « {0} » : nomFichier « {1} » contient un chemin (interdit)." -f $code, $nomFichier))
continue
}
$chemin = Join-Path (Join-Path (Join-Path $racine $code) $sens) $nomFichier
$declare.Add($chemin.ToLowerInvariant())
$relatif = ('{0}/{1}/{2}' -f $code, $sens, $nomFichier)
# Un fichier declare est un fichier TELECHARGE par chaque telephone :
# signaler ceux qui ne servent a rien.
if ($nomFichier.EndsWith('.onnx', [System.StringComparison]::OrdinalIgnoreCase) `
-and $nomFichier -match 'past') {
$alertes.Add(("{0} : decodeur a cache (past_key_values) IGNORE par l'application, mais declare donc TELECHARGE pour rien." -f $relatif))
}
elseif ($nomFichier.EndsWith('.spm', [System.StringComparison]::OrdinalIgnoreCase) `
-and $NomsVocabulaire -notcontains $nomFichier) {
$alertes.Add(("{0} : nom de vocabulaire non reconnu (attendu vocab.spm, source.spm ou target.spm) : pris comme repli « premier .spm »." -f $relatif))
}
if (-not (Test-Path -LiteralPath $chemin -PathType Leaf)) {
$echecs.Add(("Fichier ABSENT sur le disque : {0} -> le mobile recevra 404." -f $relatif))
continue
}
$info = Get-Item -LiteralPath $chemin
$taille = [long]$fichier.tailleOctets
if ($taille -le 0) {
$alertes.Add(("{0} : tailleOctets non renseignee (0) -> pas de controle de taille ni de progression." -f $relatif))
}
elseif ($taille -ne $info.Length) {
$echecs.Add(("{0} : taille differente -> manifeste {1} octet(s), disque {2}." -f $relatif, $taille, $info.Length))
}
$sha = ([string]$fichier.sha256).Trim()
if ([string]::IsNullOrWhiteSpace($sha)) {
$echecs.Add(("{0} : empreinte SHA-256 absente -> le pack sera REFUSE par le mobile." -f $relatif))
continue
}
$reel = Get-ManifesteEmpreinte $chemin
if (-not [string]::Equals($sha, $reel, [System.StringComparison]::OrdinalIgnoreCase)) {
$echecs.Add(("{0} : empreinte differente ->`r`n manifeste {1}`r`n disque {2}" -f $relatif, $sha, $reel))
}
$nbFichiers++
$octetsTotal += $info.Length
}
}
# Fichiers presents mais non declares : jamais telecharges, poids mort.
foreach ($fichierDisque in @(Get-ChildItem -LiteralPath $racine -Recurse -File)) {
if ($fichierDisque.FullName.ToLowerInvariant() -eq $Sortie.ToLowerInvariant()) { continue }
# La sauvegarde du manifeste ecrite par ce script n'est evidemment pas un
# fichier de modele : la signaler serait un faux positif a chaque controle.
if ($fichierDisque.FullName.ToLowerInvariant() -eq ($Sortie + '.bak').ToLowerInvariant()) { continue }
$relatifDisque = $fichierDisque.FullName.Substring($racine.Length).TrimStart('\', '/')
# Dossiers techniques (ex. .hf-cache du script d'export, .tmp-*) : ce ne
# sont pas des modeles, et un cache peut contenir des milliers de fichiers.
if ($relatifDisque -match '(^|\\)\.[^\\]') { continue }
if (-not $declare.Contains($fichierDisque.FullName.ToLowerInvariant())) {
$alertes.Add(("Fichier non declare dans le manifeste (jamais telecharge) : {0}" -f $relatifDisque))
}
}
Write-ManifesteInfo ("{0} pack(s), {1} fichier(s) verifie(s), {2}" -f $packs.Count, $nbFichiers, (Format-ManifesteTaille $octetsTotal))
Write-Host ''
if (Write-ManifesteBilan $echecs $alertes) { exit 0 } else { exit 1 }
}
# ============================================================================
# MODE GENERATION
# ============================================================================
$dossiersPacks = @(Get-ChildItem -LiteralPath $racine -Directory |
Where-Object { -not $_.Name.StartsWith('.') } |
Sort-Object -Property Name)
if ($dossiersPacks.Count -eq 0) {
$message = ("Aucun sous-dossier de langue sous {0} (attendu : {0}\es\versFrancais\encoder.onnx)." -f $racine)
# -AutoriserVide couvre les DEUX causes de manifeste vide : racine sans
# dossier de langue, et dossiers sans aucun fichier exploitable.
if ($AutoriserVide) { $alertes.Add($message) } else { $echecs.Add($message) }
}
$blocsPacks = New-Object 'System.Collections.Generic.List[string]'
$recap = New-Object 'System.Collections.Generic.List[string]'
foreach ($dossierPack in $dossiersPacks) {
$nomDossier = $dossierPack.Name
$code = $nomDossier.Trim().ToLowerInvariant()
if (-not (Test-ManifesteCodeValide $code)) {
$alertes.Add(("Dossier ignore « {0} » : code de langue invalide (2 a 10 caracteres, minuscules a-z, chiffres, tiret)." -f $nomDossier))
continue
}
# -cne : comparaison SENSIBLE A LA CASSE. « -ne » considererait « ru » et
# « RU » comme egaux et ne signalerait rien.
if ($code -cne $nomDossier) {
$alertes.Add(("Dossier « {0} » : le manifeste ecrit « {1} ». Renommer le dossier si le volume peut etre sensible a la casse." -f $nomDossier, $code))
}
$libelle = Resolve-ManifesteLibelle $code $Libelles
$blocsFichiers = New-Object 'System.Collections.Generic.List[string]'
$nbFichiersPack = 0
$octetsPack = [long]0
foreach ($dossierSens in @(Get-ChildItem -LiteralPath $dossierPack.FullName -Directory | Sort-Object -Property Name)) {
$sens = Resolve-ManifesteSens $dossierSens.Name
if (-not $sens) {
$alertes.Add(("Dossier ignore {0}\{1} : sens inconnu (attendu versFrancais, depuisFrancais ou commun)." -f $nomDossier, $dossierSens.Name))
continue
}
if ($sens -cne $dossierSens.Name) {
$alertes.Add(("Dossier {0}\{1} : le manifeste ecrit « {2} » (casse canonique exigee par le mobile)." -f $nomDossier, $dossierSens.Name, $sens))
}
# L'API ne sert que {code}/{sens}/{fichier} : un sous-dossier n'est jamais atteignable.
foreach ($sousDossier in @(Get-ChildItem -LiteralPath $dossierSens.FullName -Directory)) {
$alertes.Add(("Dossier ignore {0}\{1}\{2} : l'API ne sert que {{code}}/{{sens}}/{{fichier}}." -f $nomDossier, $dossierSens.Name, $sousDossier.Name))
}
foreach ($fichier in @(Get-ChildItem -LiteralPath $dossierSens.FullName -File | Sort-Object -Property Name)) {
# Deux fichiers inutiles qu'il ne faut PAS livrer : declares ici, ils
# seraient telecharges par chaque telephone pour rien.
if ($fichier.Name.EndsWith('.onnx', [System.StringComparison]::OrdinalIgnoreCase) `
-and $fichier.Name -match 'past') {
$alertes.Add(("{0}\{1}\{2} (~{3}) : decodeur a cache (past_key_values) IGNORE par l'application, mais il serait TELECHARGE. Ne pas livrer ce fichier." -f $nomDossier, $dossierSens.Name, $fichier.Name, (Format-ManifesteTaille $fichier.Length)))
}
elseif ($fichier.Name.EndsWith('.spm', [System.StringComparison]::OrdinalIgnoreCase) `
-and $NomsVocabulaire -notcontains $fichier.Name) {
$alertes.Add(("{0}\{1}\{2} : nom de vocabulaire non reconnu (attendu vocab.spm, source.spm ou target.spm). L'application le prendrait comme repli « premier .spm » : verifier qu'il est bien le bon." -f $nomDossier, $dossierSens.Name, $fichier.Name))
}
$sha = Get-ManifesteEmpreinte $fichier.FullName
$blocsFichiers.Add((New-ManifesteBlocFichier -sens $sens -nomFichier $fichier.Name -taille $fichier.Length -sha256 $sha))
$nbFichiersPack++
$octetsPack += $fichier.Length
}
}
if ($nbFichiersPack -eq 0) {
$alertes.Add(("Pack « {0} » ignore : aucun fichier utilisable." -f $code))
continue
}
$blocsPacks.Add((New-ManifesteBlocPack -code $code -libelle $libelle -version $Version -blocsFichiers $blocsFichiers))
$recap.Add((' {0,-6} {1,-16} {2,3} fichier(s) {3,12}' -f $code, $libelle, $nbFichiersPack, (Format-ManifesteTaille $octetsPack)))
}
if ($blocsPacks.Count -eq 0 -and -not $AutoriserVide) {
$echecs.Add('Aucun pack exploitable : manifeste NON ecrit (un manifeste vide ferait afficher au mobile « Aucun modele de langue n''est disponible sur le serveur »).')
$echecs.Add('Corriger l''arborescence, ou forcer avec -AutoriserVide si c''est volontaire.')
}
if ($echecs.Count -gt 0) {
Write-ManifesteBilan $echecs $alertes | Out-Null
exit 1
}
$json = New-ManifesteJsonText $blocsPacks
foreach ($ligne in $recap) { Write-ManifesteInfo $ligne }
Write-Host ''
if (-not $PSCmdlet.ShouldProcess($Sortie, 'Ecriture du manifeste des packs de langue')) {
Write-ManifesteInfo 'Simulation (-WhatIf) : aucun fichier ecrit. Contenu qui aurait ete ecrit :'
Write-Host ''
Write-Host $json
Write-Host ''
Write-ManifesteBilan $echecs $alertes | Out-Null
exit 0
}
if (Test-Path -LiteralPath $Sortie -PathType Leaf) {
$sauvegarde = $Sortie + '.bak'
Copy-Item -LiteralPath $Sortie -Destination $sauvegarde -Force
Write-ManifesteInfo ("Manifeste precedent conserve : {0}" -f $sauvegarde)
}
Write-ManifesteFichier -chemin $Sortie -contenu $json
Write-ManifesteOk ("Manifeste ecrit : {0}" -f $Sortie)
Write-Host ''
foreach ($alerte in $alertes) { Write-ManifesteAlerte $alerte }
Write-Host ''
Write-ManifesteInfo 'Aucun demarrage/redemarrage de service n''est necessaire : l''API relit le'
Write-ManifesteInfo 'manifeste a CHAQUE appel de GET /api/langues.'
Write-ManifesteInfo 'Le mobile, lui, garde son index memorise : il faut un rafraichissement'
Write-ManifesteInfo '(liste des langues -> actualiser) pour voir la nouvelle liste.'
Write-Host ''
Write-ManifesteInfo 'Controles a enchainer :'
Write-ManifesteInfo (' 1. .\New-OmikaLanguesManifeste.ps1 -Verifier')
Write-ManifesteInfo ' 2. Invoke-RestMethod https://<URL_SRV_LANGUES>/api/langues | ConvertTo-Json -Depth 6'
exit 0
+738
View File
@@ -0,0 +1,738 @@
# ============================================================
# Omika - Module commun de deploiement des services Windows
# ============================================================
# Utilise par les scripts Install-Omika*.ps1 (UN FICHIER PAR DEPLOIEMENT).
#
# Ce module regroupe ce qui etait duplique ou implicite dans l'ancien
# Install-OmikaServices.ps1 monolithique :
# - controle des droits administrateur ;
# - messages homogenes (etape / info / ok / avertissement / echec) ;
# - creation de dossiers idempotente ;
# - ACL NTFS AVEC CONTROLE DU CODE RETOUR d'icacls ;
# - validation du compte de service (existence + droit « Ouvrir une session
# en tant que service ») ;
# - creation / suppression / verification d'un service Windows.
#
# COMPATIBILITE : Windows PowerShell 5.1 (aucune syntaxe PS 7).
#
# CE MODULE NE DEMARRE JAMAIS UN SERVICE. Il cree et configure uniquement ;
# le demarrage reste une action manuelle et explicite (doctrine Phase 5C.2C).
#
# A PROPOS DE -WhatIf :
# Les variables de preference ($WhatIfPreference) NE se propagent PAS dans
# l'etat de session d'un module. Les scripts appelants doivent donc transmettre
# explicitement -WhatIf:$Simulation a chaque appel. Chaque fonction marquee
# [CmdletBinding(SupportsShouldProcess)] l'accepte.
# ============================================================
# --- Messages ----------------------------------------------------------------
function Write-OmikaStep {
param([Parameter(Mandatory = $true)][string]$Message)
Write-Host ''
Write-Host "== $Message ==" -ForegroundColor Cyan
}
function Write-OmikaInfo {
param([Parameter(Mandatory = $true)][string]$Message)
Write-Host " .. $Message"
}
function Write-OmikaOk {
param([Parameter(Mandatory = $true)][string]$Message)
Write-Host " OK $Message" -ForegroundColor Green
}
function Write-OmikaWarn {
param([Parameter(Mandatory = $true)][string]$Message)
Write-Host " ATTENTION $Message" -ForegroundColor Yellow
}
function Write-OmikaFail {
param([Parameter(Mandatory = $true)][string]$Message)
Write-Host " ECHEC $Message" -ForegroundColor Red
}
# --- Prerequis ---------------------------------------------------------------
function Assert-OmikaAdmin {
<#
.SYNOPSIS
Verifie que la session PowerShell est elevee. Leve une exception sinon.
#>
$identite = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identite)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "Ce script doit etre execute en PowerShell ADMINISTRATEUR (clic droit -> Executer en tant qu'administrateur)."
}
}
function Get-OmikaPaths {
<#
.SYNOPSIS
Resout l'arborescence serveur C:\Omika (meme convention que
Install-OmikaServerFiles.ps1).
#>
param([string]$InstallRoot = 'C:\Omika')
return [pscustomobject]@{
InstallRoot = $InstallRoot
Apps = Join-Path $InstallRoot 'Apps'
Data = Join-Path $InstallRoot 'Data'
Logs = Join-Path $InstallRoot 'Logs'
Config = Join-Path $InstallRoot 'Config'
Backups = Join-Path $InstallRoot 'Backups'
Deploy = Join-Path $InstallRoot 'Deploy'
}
}
# --- Dossiers ----------------------------------------------------------------
function New-OmikaDirectory {
[CmdletBinding(SupportsShouldProcess = $true)]
param([Parameter(Mandatory = $true)][string]$Path)
if (Test-Path -LiteralPath $Path) {
Write-OmikaInfo "Dossier deja present : $Path"
return $false
}
if ($PSCmdlet.ShouldProcess($Path, 'Creer le dossier')) {
New-Item -ItemType Directory -Force -Path $Path | Out-Null
Write-OmikaOk "Dossier cree : $Path"
return $true
}
return $false
}
# --- Compte de service -------------------------------------------------------
function Resolve-OmikaAccountName {
<#
.SYNOPSIS
Normalise un compte de service en forme NON AMBIGUE 'ORDINATEUR\compte'.
.DESCRIPTION
L'ancien script passait la forme '.\svc_omika_api' directement a icacls.
La forme '.\' est une commodite de ligne de commande : elle n'est pas la
forme documentee d'icacls (qui attend 'Compte' ou 'Domaine\Compte').
Comme le bac a sable de developpement ne permet pas de verifier les
ecritures d'ACL, ce module ne pariera pas sur la forme '.\' : il resout
systematiquement en 'ORDINATEUR\compte', forme non ambigue, et le script
CONTROLE le code retour d'icacls (voir Grant-OmikaAcl).
#>
param(
[Parameter(Mandatory = $true)][string]$Account,
[string]$ComputerName = $env:COMPUTERNAME
)
if ($Account -eq '.') { return "$ComputerName\$env:USERNAME" }
if ($Account.StartsWith('.\')) { return ('{0}\{1}' -f $ComputerName, $Account.Substring(2)) }
if ($Account.StartsWith('./')) { return ('{0}\{1}' -f $ComputerName, $Account.Substring(2)) }
return $Account
}
function Test-OmikaLocalAccount {
<#
.SYNOPSIS
Verifie qu'un compte de service existe sur la machine.
.OUTPUTS
PSCustomObject : Nom, FormeResolue, Existe ($true/$false/$null si domaine),
Actif, Sid, Raison.
#>
param(
[Parameter(Mandatory = $true)][string]$Account,
[string]$ComputerName = $env:COMPUTERNAME
)
$resolu = Resolve-OmikaAccountName -Account $Account -ComputerName $ComputerName
$resultat = [pscustomobject]@{
Nom = $Account
FormeResolue = $resolu
Existe = $null
Actif = $null
Sid = $null
Raison = ''
}
# Compte de domaine : non verifiable localement (le DC est autoritaire).
$nomCourt = $resolu
if ($resolu.Contains('\')) { $nomCourt = $resolu.Substring($resolu.IndexOf('\') + 1) }
if ($resolu.Contains('\') -and -not $resolu.StartsWith("$ComputerName\", [System.StringComparison]::OrdinalIgnoreCase)) {
$resultat.Raison = 'compte de domaine : existence non verifiable depuis ce serveur'
return $resultat
}
# 1) Get-LocalUser (module LocalAccounts) : leve UserNotFoundException si le
# compte est absent. C'est le chemin nominal.
if (Get-Command -Name Get-LocalUser -ErrorAction SilentlyContinue) {
try {
$utilisateur = Get-LocalUser -Name $nomCourt -ErrorAction Stop
$resultat.Existe = $true
$resultat.Actif = [bool]$utilisateur.Enabled
if ($utilisateur.SID) { $resultat.Sid = $utilisateur.SID.Value }
return $resultat
}
catch {
if ($_.Exception.GetType().Name -eq 'UserNotFoundException') {
$resultat.Existe = $false
$resultat.Raison = "compte local introuvable : $resolu"
return $resultat
}
# Autre erreur (droits, module...) : on ne conclut pas, on passe au repli.
}
}
# 2) Repli net.exe user : code retour 0 = le compte existe, non nul = absent.
#
# PAS DE REPLI ADSI. Verifie en test sur ce poste :
# [ADSI]("WinNT://ORDINATEUR/compte,user") -> AUCUNE exception pour un
# compte inexistant (et .Name vide) : ce repli produisait un FAUX POSITIF
# « Existe = True », c'est-a-dire exactement le diagnostic inverse de
# celui attendu. Un faux positif est pire qu'une incertitude ici : le
# script laisserait creer un service avec un compte inexistant.
# Le nom est filtre sur le guillemet simple (argument de ligne de commande).
if ($nomCourt -notmatch "'") {
$ancien = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$null = (& net.exe user $nomCourt 2>&1 | Out-String)
$code = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $ancien
}
if ($code -eq 0) {
$resultat.Existe = $true
return $resultat
}
$resultat.Existe = $false
$resultat.Raison = "compte local introuvable : $resolu"
return $resultat
}
# 3) Ni l'un ni l'autre exploitable : on reste INDETERMINE (jamais « existe »).
$resultat.Raison = 'existence non verifiable (ni Get-LocalUser ni net.exe exploitables)'
return $resultat
}
function Get-OmikaServiceLogonRight {
<#
.SYNOPSIS
Lit la liste des comptes titulaires du droit « Ouvrir une session en tant
que service » (SeServiceLogonRight) via un export secedit.
.OUTPUTS
Tableau d'entrees (SID ou nom), ou $null si indeterminable (non admin).
#>
$fichier = Join-Path $env:TEMP ('omika-secedit-{0}.inf' -f ([guid]::NewGuid().ToString('N')))
$ancien = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
& secedit.exe /export /areas USER_RIGHTS /cfg $fichier | Out-Null
if (-not (Test-Path -LiteralPath $fichier)) { return $null }
# Les noms de privileges (SeServiceLogonRight) ne sont PAS localises :
# le filtrage fonctionne sur un Windows francais.
$ligne = Get-Content -LiteralPath $fichier |
Where-Object { $_ -match '^\s*SeServiceLogonRight\s*=' } |
Select-Object -First 1
if (-not $ligne) { return @() }
$valeur = ($ligne -split '=', 2)[1]
$entrees = @($valeur -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
return $entrees
}
catch {
return $null
}
finally {
$ErrorActionPreference = $ancien
if (Test-Path -LiteralPath $fichier) {
Remove-Item -LiteralPath $fichier -Force -ErrorAction SilentlyContinue
}
}
}
function Test-OmikaServiceLogonRight {
<#
.SYNOPSIS
Verifie que le compte possede le droit « Ouvrir une session en tant que
service ». C'est la cause n1 d'echec de demarrage (erreur 1069/1053).
.OUTPUTS
$true, $false, ou $null si le droit n'a pas pu etre determine.
#>
param(
[Parameter(Mandatory = $true)][string]$Account,
[string]$ComputerName = $env:COMPUTERNAME
)
$compte = Test-OmikaLocalAccount -Account $Account -ComputerName $ComputerName
$droits = Get-OmikaServiceLogonRight
if ($null -eq $droits) { return $null }
$candidats = @()
if ($compte.Sid) { $candidats += $compte.Sid }
if ($compte.FormeResolue) { $candidats += $compte.FormeResolue }
$nomCourt = $compte.FormeResolue
if ($nomCourt.Contains('\')) { $nomCourt = $nomCourt.Substring($nomCourt.IndexOf('\') + 1) }
if ($nomCourt) { $candidats += $nomCourt }
foreach ($entree in $droits) {
foreach ($candidat in $candidats) {
if ([string]::IsNullOrWhiteSpace($candidat)) { continue }
if ($entree -eq $candidat) { return $true }
# secedit exporte les comptes locaux sous forme de SID (*S-1-5-21-...).
if ($entree -like "*$candidat*") { return $true }
}
}
return $false
}
function Assert-OmikaServiceAccount {
<#
.SYNOPSIS
Verifie le compte de service avant la creation (existence + droit de
connexion). Leve une exception si le compte n'existe pas ; avertit
seulement si un droit manque (New-Service peut encore reussir, mais le
DEMARRAGE echouera).
#>
param(
[Parameter(Mandatory = $true)][string]$Account,
[string]$ComputerName = $env:COMPUTERNAME
)
$compte = Test-OmikaLocalAccount -Account $Account -ComputerName $ComputerName
Write-OmikaInfo "Compte de service : $($compte.FormeResolue)"
if ($compte.Existe -eq $false) {
throw ("Compte de service introuvable : $($compte.FormeResolue). " +
"Creez le compte local dedie AVANT ce script (jamais LocalSystem, " +
"jamais un compte humain).")
}
if ($compte.Existe -eq $null) {
Write-OmikaWarn "Existence du compte non verifiable ($($compte.Raison))."
}
elseif ($compte.Actif -eq $false) {
Write-OmikaWarn "Le compte $($compte.FormeResolue) est DESACTIVE : le service ne demarrera pas."
}
$droit = Test-OmikaServiceLogonRight -Account $Account -ComputerName $ComputerName
if ($droit -eq $false) {
Write-OmikaWarn "Le compte $($compte.FormeResolue) ne possede PAS le droit"
Write-OmikaWarn " « Ouvrir une session en tant que service » (SeServiceLogonRight)."
Write-OmikaWarn " -> La creation du service reussira, mais le DEMARRAGE echouera (erreur 1069)."
Write-OmikaWarn " -> secpol.msc : Attribution des droits utilisateur / Ouvrir une session en tant que service."
}
elseif ($droit -eq $null) {
Write-OmikaWarn "Droit de connexion en tant que service : non verifiable (secedit inaccessible)."
}
else {
Write-OmikaOk "Droit « Ouvrir une session en tant que service » : present."
}
return $compte
}
# --- ACL ---------------------------------------------------------------------
function Grant-OmikaAcl {
<#
.SYNOPSIS
Pose une ACL NTFS et CONTROLE le code retour d'icacls.
.DESCRIPTION
L'ancien script ignorait totalement le code retour d'icacls : une ACL
refusee passait inapercue a l'installation et ne se manifestait qu'au
premier demarrage du service, par une erreur d'acces illisible.
Deux cas sont distingues :
- dossier absent -> avertissement BRUYANT (sauf -RequireExisting) ;
- icacls en echec -> exception (le script s'arrete).
.PARAMETER Right
M = Modifier (ecriture), R = Lecture seule.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$Account,
[ValidateSet('M', 'R')][string]$Right = 'M',
[switch]$RequireExisting
)
$resolu = Resolve-OmikaAccountName -Account $Account
if (-not (Test-Path -LiteralPath $Path)) {
$message = "Dossier absent, aucune ACL posee : $Path (compte $resolu)"
if ($RequireExisting) { throw $message }
Write-OmikaWarn $message
Write-OmikaWarn ' -> le service subira une erreur d''acces a son premier demarrage.'
return $false
}
$ace = '{0}:(OI)(CI){1}' -f $resolu, $Right
if (-not $PSCmdlet.ShouldProcess($Path, "ACL $Right pour $resolu")) { return $false }
# icacls ecrit ses erreurs sur stderr : sous $ErrorActionPreference='Stop' une
# NativeCommandError deviendrait TERMINANTE et masquerait le vrai code retour.
# On isole donc l'appel.
$ancien = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$sortie = (& icacls.exe $Path /grant:r $ace 2>&1 | Out-String)
$code = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $ancien
}
if ($code -ne 0) {
throw ("icacls a echoue (code $code) sur '$Path' pour '$resolu'.`n" +
"Sortie icacls : $($sortie.Trim())`n" +
"Verifiez que le compte existe et que la forme du nom est acceptee " +
"('ORDINATEUR\compte').")
}
Write-OmikaOk "ACL $Right : $resolu sur $Path"
return $true
}
# --- Service Windows ---------------------------------------------------------
function Get-OmikaServiceState {
<#
.SYNOPSIS
Etat reel d'un service (CIM), ou $null s'il n'existe pas.
Permet de detecter un service existant qui pointe sur un AUTRE binaire
ou tourne sous un AUTRE compte que ceux attendus.
#>
param([Parameter(Mandatory = $true)][string]$Name)
$service = Get-CimInstance -ClassName Win32_Service -Filter ("Name='{0}'" -f $Name) -ErrorAction SilentlyContinue
if (-not $service) { return $null }
return [pscustomobject]@{
Nom = $service.Name
NomAffiche = $service.DisplayName
Etat = $service.State
Demarrage = $service.StartMode
CheminExe = $service.PathName
Compte = $service.StartName
Description = $service.Description
}
}
function Install-OmikaService {
<#
.SYNOPSIS
Cree un service Windows (StartupType Manual par defaut), de facon
IDEMPOTENTE, et verifie ce qui a ete reellement cree.
.DESCRIPTION
- Service inexistant -> creation (mot de passe saisi via Get-Credential,
jamais en dur, jamais affiche).
- Service existant -> aucune creation ; comparaison du binaire et du
compte avec les valeurs attendues, avec avertissement si divergence.
- Ne demarre JAMAIS le service.
.OUTPUTS
PSCustomObject : Nom, Cree, ExistaitDeja, Conforme, Etat.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][string]$DisplayName,
[Parameter(Mandatory = $true)][string]$ExePath,
[Parameter(Mandatory = $true)][string]$Account,
[string]$Description = '',
[ValidateSet('Manual', 'Automatic', 'Disabled')][string]$StartupType = 'Manual'
)
$resultat = [pscustomobject]@{
Nom = $Name
Cree = $false
ExistaitDeja = $false
Conforme = $null
Etat = ''
}
if (-not (Test-Path -LiteralPath $ExePath -PathType Leaf)) {
throw "Executable introuvable pour le service '$Name' : $ExePath"
}
$attendu = '"{0}"' -f $ExePath
$existant = Get-OmikaServiceState -Name $Name
if ($existant) {
$resultat.ExistaitDeja = $true
$resultat.Etat = $existant.Etat
Write-OmikaInfo "Service '$Name' deja present : aucune creation (idempotent)."
$conforme = $true
if ($existant.CheminExe -and $existant.CheminExe.Trim('"') -ne $ExePath) {
Write-OmikaWarn " Binaire DIFFERENT de celui attendu :"
Write-OmikaWarn " attendu : $ExePath"
Write-OmikaWarn " reel : $($existant.CheminExe)"
$conforme = $false
}
$compteAttendu = (Resolve-OmikaAccountName -Account $Account).ToLowerInvariant()
if ($existant.Compte -and $existant.Compte.ToLowerInvariant() -ne $compteAttendu) {
Write-OmikaWarn " Compte DIFFERENT de celui attendu :"
Write-OmikaWarn " attendu : $compteAttendu"
Write-OmikaWarn " reel : $($existant.Compte)"
$conforme = $false
}
$resultat.Conforme = $conforme
return $resultat
}
if ($PSCmdlet.ShouldProcess($Name, "Creer le service Windows (compte $Account, $StartupType)")) {
$cred = Get-Credential -UserName $Account -Message "Mot de passe du compte de service $Name"
$parametres = @{
Name = $Name
DisplayName = $DisplayName
BinaryPathName = $attendu
Credential = $cred
StartupType = $StartupType
}
if ($Description) { $parametres['Description'] = $Description }
New-Service @parametres | Out-Null
Write-OmikaOk "Service cree : $Name ($StartupType, non demarre)."
$resultat.Cree = $true
$etat = Get-OmikaServiceState -Name $Name
if ($etat) { $resultat.Etat = $etat.Etat }
}
return $resultat
}
function Remove-OmikaService {
<#
.SYNOPSIS
Supprime un service Windows (rollback). Refuse si le service tourne
encore : il faut l'arreter explicitement d'abord (aucun arret automatique).
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param([Parameter(Mandatory = $true)][string]$Name)
$existant = Get-OmikaServiceState -Name $Name
if (-not $existant) {
Write-OmikaInfo "Service '$Name' absent : rien a supprimer."
return $false
}
if ($existant.Etat -eq 'Running') {
throw "Le service '$Name' est en cours d'execution : arretez-le explicitement (Stop-Service $Name) avant de le supprimer."
}
if ($PSCmdlet.ShouldProcess($Name, 'Supprimer le service Windows')) {
$ancien = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$sortie = (& sc.exe delete $Name 2>&1 | Out-String)
$code = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $ancien
}
if ($code -ne 0) { throw "sc.exe delete a echoue (code $code) pour '$Name' : $($sortie.Trim())" }
Write-OmikaOk "Service supprime : $Name"
return $true
}
return $false
}
# --- Restitutions ------------------------------------------------------------
function Write-OmikaServiceSummary {
<#
.SYNOPSIS
Affiche un recapitulatif de fin d'installation : etat reel du service
et rappels de ce qui reste MANUEL.
#>
param(
[Parameter(Mandatory = $true)][string]$Name,
[string]$ExePath = '',
[string]$Account = '',
[string[]]$Rappels = @(),
[switch]$Supprime
)
Write-Host ''
if ($Supprime) {
Write-Host ("{0} : SUPPRIME." -f $Name) -ForegroundColor Green
return
}
Write-Host ("{0} : INSTALLATION TERMINEE (service NON demarre)." -f $Name) -ForegroundColor Green
$etat = Get-OmikaServiceState -Name $Name
if ($etat) {
Write-Host ''
Write-Host ' Etat reel du service :'
Write-Host (" Nom : {0}" -f $etat.Nom)
Write-Host (" Nom affiche : {0}" -f $etat.NomAffiche)
Write-Host (" Etat : {0}" -f $etat.Etat)
Write-Host (" Demarrage : {0}" -f $etat.Demarrage)
Write-Host (" Binaire : {0}" -f $etat.CheminExe)
Write-Host (" Compte : {0}" -f $etat.Compte)
}
else {
Write-OmikaWarn "Le service '$Name' est introuvable apres installation (mode -WhatIf ?)."
}
if ($Rappels.Count -gt 0) {
Write-Host ''
Write-Host ' RAPPELS (jamais automatises par ce script) :' -ForegroundColor Yellow
foreach ($rappel in $Rappels) {
Write-Host (" - {0}" -f $rappel) -ForegroundColor Yellow
}
}
Write-Host ''
Write-Host ' Le demarrage est une action MANUELLE et explicite :' -ForegroundColor Yellow
Write-Host (" Start-Service {0}" -f $Name) -ForegroundColor Yellow
Write-Host ''
}
# --- Port loopback -----------------------------------------------------------
function Get-OmikaDebutPlageEphemere {
<#
.SYNOPSIS
Borne basse de la plage de ports ephemere Windows (49152 par defaut).
.DESCRIPTION
La sortie de netsh est LOCALISEE : on prend le premier nombre de la
reponse, qui est toujours le port de debut, quelle que soit la langue.
#>
$texte = (netsh int ipv4 show dynamicport tcp | Out-String)
$nombres = [regex]::Matches($texte, '\d+')
if ($nombres.Count -ge 1) { return [int]$nombres[0].Value }
return 49152
}
function Get-OmikaPlagesReservees {
<#
.SYNOPSIS
Plages TCP reservees par Windows (Hyper-V / WSL / Docker peuvent en
reserver arbitrairement). Necessite une session elevee.
#>
$plages = @()
$ancien = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$lignes = netsh int ipv4 show excludedportrange protocol=tcp
}
finally {
$ErrorActionPreference = $ancien
}
foreach ($ligne in $lignes) {
$m = [regex]::Match([string]$ligne, '^\s*(\d{1,5})\s+(\d{1,5})\s*$')
if ($m.Success) {
$plages += [pscustomobject]@{
Debut = [int]$m.Groups[1].Value
Fin = [int]$m.Groups[2].Value
}
}
}
return $plages
}
function Test-OmikaPortLibre {
<#
.SYNOPSIS
Determine si un port TCP est utilisable sur ce serveur.
.DESCRIPTION
Quatre causes de refus sont testees :
1. un processus ecoute deja sur ce port ;
2. le port tombe dans une plage RESERVEE par Windows ;
3. le port tombe dans la plage EPHEMERE (une connexion sortante peut le prendre) ;
4. la reservation reelle sur 127.0.0.1 echoue.
Le test 4 est la preuve : le port est reellement reserve puis relache.
.OUTPUTS
PSCustomObject : Port, Libre (booleen), Raisons (texte).
#>
param(
[Parameter(Mandatory = $true)]
[ValidateRange(1, 65535)]
[int]$Port,
[switch]$VerifierPlagesReservees
)
$refus = New-Object System.Collections.Generic.List[string]
# --- 1. Deja en ecoute ? -----------------------------------------------
$ecoute = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue)
if ($ecoute.Count -gt 0) {
$qui = @()
foreach ($connexion in $ecoute) {
$processus = Get-Process -Id $connexion.OwningProcess -ErrorAction SilentlyContinue
if ($processus) { $qui += ('{0} (PID {1})' -f $processus.ProcessName, $connexion.OwningProcess) }
else { $qui += ('PID {0}' -f $connexion.OwningProcess) }
}
$refus.Add('deja en ecoute par ' + (($qui | Select-Object -Unique) -join ', '))
}
# --- 2. Plage reservee ? -----------------------------------------------
if ($VerifierPlagesReservees) {
foreach ($plage in (Get-OmikaPlagesReservees)) {
if ($Port -ge $plage.Debut -and $Port -le $plage.Fin) {
$refus.Add(('dans une plage RESERVEE par Windows ({0} a {1})' -f $plage.Debut, $plage.Fin))
}
}
}
# --- 3. Plage ephemere ? ----------------------------------------------
$debutEphemere = Get-OmikaDebutPlageEphemere
if ($Port -ge $debutEphemere) {
$refus.Add(('dans la plage EPHEMERE (a partir de {0}) : une connexion sortante peut le prendre' -f $debutEphemere))
}
# --- 4. Test reel : reserver le port sur 127.0.0.1 ---------------------
$verrou = $null
try {
$verrou = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $Port)
$verrou.Start()
}
catch {
$refus.Add("reservation impossible : $($_.Exception.Message)")
}
finally {
if ($verrou) { try { $verrou.Stop() } catch { } }
}
return [pscustomobject]@{
Port = $Port
Libre = ($refus.Count -eq 0)
Raisons = ($refus -join ' ; ')
}
}
Export-ModuleMember -Function @(
'Write-OmikaStep',
'Write-OmikaInfo',
'Write-OmikaOk',
'Write-OmikaWarn',
'Write-OmikaFail',
'Assert-OmikaAdmin',
'Get-OmikaPaths',
'New-OmikaDirectory',
'Resolve-OmikaAccountName',
'Test-OmikaLocalAccount',
'Get-OmikaServiceLogonRight',
'Test-OmikaServiceLogonRight',
'Assert-OmikaServiceAccount',
'Grant-OmikaAcl',
'Get-OmikaServiceState',
'Install-OmikaService',
'Remove-OmikaService',
'Write-OmikaServiceSummary',
'Get-OmikaDebutPlageEphemere',
'Get-OmikaPlagesReservees',
'Test-OmikaPortLibre'
)
+136
View File
@@ -0,0 +1,136 @@
# ============================================================
# Omika - Test de disponibilite d'un port TCP loopback
# ============================================================
# Repond a UNE question : « puis-je utiliser ce port, oui ou non ? »
#
# A executer SUR LE SERVEUR (SRV-OMIKA), en PowerShell ADMINISTRATEUR :
# sans elevation, la verification des plages reservees par Windows
# (Hyper-V / WSL / Docker) ne peut pas etre faite et le resultat est INCOMPLET.
#
# Ce script NE MODIFIE RIEN : il lit, puis reserve brievement le port sur
# 127.0.0.1 pour le relacher aussitot.
#
# Quatre causes de refus sont detectees :
# 1. un processus ecoute deja sur ce port ;
# 2. le port est dans une plage RESERVEE par Windows ;
# 3. le port est dans la plage EPHEMERE (une connexion sortante peut le prendre) ;
# 4. la reservation reelle sur 127.0.0.1 echoue.
#
# Le port n'est PAS choisi ici : il se decide contre le plan de ports Apache
# (voir docs\DEPLOIEMENT_API_LANGUES.md §6bis).
#
# EXEMPLES :
# .\Test-PortDisponible.ps1 -Port 5099
# .\Test-PortDisponible.ps1 -Port 5099 -EssayerSuivants 5
#
# Codes de sortie : 0 = un port disponible trouve, 1 = aucun.
# ============================================================
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateRange(1, 65535)]
[int]$Port,
# Nombre de ports consecutifs a tester en plus (ballotage).
[ValidateRange(0, 50)]
[int]$EssayerSuivants = 0
)
$ErrorActionPreference = 'Stop'
# La logique de test vit dans le module commun : une seule implementation,
# partagee avec Install-OmikaLanguesApi.ps1 (-Port).
$moduleCommun = Join-Path $PSScriptRoot 'Omika.Deploy.Common.psm1'
if (-not (Test-Path -LiteralPath $moduleCommun)) {
Write-Host "ERREUR : module introuvable : $moduleCommun" -ForegroundColor Red
Write-Host "Copiez Omika.Deploy.Common.psm1 dans le meme dossier que ce script." -ForegroundColor Red
exit 1
}
Import-Module $moduleCommun -Force
function Test-EstAdministrateur {
try {
$identite = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identite)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
catch {
return $false
}
}
# --- En-tete -----------------------------------------------------------------
Write-Host ''
Write-Host '=== Test de disponibilite d''un port TCP ===' -ForegroundColor Cyan
Write-Host "Machine : $env:COMPUTERNAME"
Write-Host "Utilisateur : $env:USERNAME"
$estAdmin = Test-EstAdministrateur
if ($estAdmin) {
Write-Host 'Mode : ADMINISTRATEUR (test complet)' -ForegroundColor Green
}
else {
Write-Host 'Mode : NON administrateur' -ForegroundColor Yellow
Write-Host ' -> les plages reservees par Windows ne sont PAS verifiables.' -ForegroundColor Yellow
Write-Host ' -> relancez ce script en tant qu''administrateur.' -ForegroundColor Yellow
}
Write-Host ("Plage ephemere Windows : a partir de {0}" -f (Get-OmikaDebutPlageEphemere))
if ($estAdmin) {
$plages = @(Get-OmikaPlagesReservees)
Write-Host ("Plages reservees : {0} plage(s)" -f $plages.Count)
foreach ($plage in $plages) {
Write-Host (" {0} - {1}" -f $plage.Debut, $plage.Fin)
}
}
else {
Write-Host 'Plages reservees : NON VERIFIE (droits insuffisants)'
}
# --- Tests -------------------------------------------------------------------
Write-Host ''
$resultats = @()
for ($i = 0; $i -le $EssayerSuivants; $i++) {
$resultats += Test-OmikaPortLibre -Port ($Port + $i) -VerifierPlagesReservees:$estAdmin
}
foreach ($resultat in $resultats) {
if ($resultat.Libre) {
Write-Host (" {0} DISPONIBLE" -f $resultat.Port) -ForegroundColor Green
}
else {
Write-Host (" {0} INDISPONIBLE -> {1}" -f $resultat.Port, $resultat.Raisons) -ForegroundColor Red
}
}
$premier = $resultats | Where-Object { $_.Libre } | Select-Object -First 1
# --- Verdict -----------------------------------------------------------------
Write-Host ''
if ($premier) {
Write-Host ("RESULTAT : le port {0} est DISPONIBLE sur ce serveur." -f $premier.Port) -ForegroundColor Green
Write-Host ''
Write-Host 'A poser ensuite en variable MACHINE (variables du service) :' -ForegroundColor Cyan
Write-Host ''
Write-Host (" [Environment]::SetEnvironmentVariable('ASPNETCORE_URLS', 'http://127.0.0.1:{0}', 'Machine')" -f $premier.Port)
Write-Host ''
Write-Host 'Puis, dans le vhost Apache, faire pointer le ProxyPass vers CE MEME port.'
Write-Host 'Un ecart entre les deux envoie les requetes vers un autre service, en silence.'
Write-Host ''
}
else {
Write-Host 'RESULTAT : aucun port disponible dans la plage testee.' -ForegroundColor Red
Write-Host 'Relancez avec davantage de candidats, par exemple :' -ForegroundColor Yellow
Write-Host (" .\Test-PortDisponible.ps1 -Port {0} -EssayerSuivants 10" -f ($Port + $EssayerSuivants + 1))
Write-Host ''
}
if (-not $estAdmin) {
Write-Host 'RAPPEL : sans droits administrateur, ce resultat est INCOMPLET.' -ForegroundColor Yellow
Write-Host 'Un port peut etre dans une plage reservee et echouer au demarrage de Kestrel.' -ForegroundColor Yellow
Write-Host ''
}
if ($premier) { exit 0 } else { exit 1 }
+181
View File
@@ -0,0 +1,181 @@
# ============================================================
# Omika Diagnostic Center — Déploiement Phase 5B2 (SERVER uniquement)
# ============================================================
# CIBLE : Windows Server 2025, PowerShell ADMINISTRATEUR.
# NE PAS exécuter depuis le poste de développement.
#
# Ce script :
# 1. Vérifie les prérequis (admin + runtime .NET 10 présent).
# 2. Crée C:\Omika\Apps\DiagnosticsWorker et C:\Omika\Apps\DiagnosticsWeb.
# 3. Copie les publications (binaires framework-dependent win-x64).
# 4. Sauvegarde l'ancienne version avant remplacement.
# 5. Copie le Worker (Windows Service REEL). StartupType MANUAL + STOPPED :
# JAMAIS de demarrage avant Phase 5C. Si le service n'existe pas, sa
# creation est DIFFEREE a la Phase 5C (compte A_DETERMINER_EN_PHASE_5C,
# aucun compte LocalSystem/NetworkService fige automatiquement).
# 6. Préserve la configuration serveur déjà injectée (jamais écrasée).
#
# Ce script NE FAIT PAS (interdictions Phase 5B2A) :
# - AUCUN mot de passe PostgreSQL en dur ; AUCUNE donnée secrète écrite.
# - AUCUNE ouverture de port 5432 ni 4318 ; AUCUNE règle firewall.
# - AUCUNE modification du Collector OpenTelemetry.
# - AUCUNE modification de HFSQL ni de Omika.Api.
# - AUCUN démarrage de la Phase 5C.
# - AUCUN démarrage du Worker (service MANUAL + STOPPED, jamais Automatic).
# - AUCUN choix d'hébergement Web (IIS vs Kestrel) : le Web est copié seulement.
#
# IDEMPOTENT : une seconde exécution ne crée pas de service en double, ne
# ré-applique pas le schéma, n'écrase pas la config secrète, ne multiplie pas
# les dossiers, et ne perd ni logs ni checkpoints.
# ============================================================
[CmdletBinding()]
param(
[string]$PackageRoot,
[string]$ServiceName = 'OmikaDiagnosticsWorker',
[string]$DisplayName = 'Omika Diagnostics Worker',
[string]$WorkerTarget = 'C:\Omika\Apps\DiagnosticsWorker',
[string]$WebTarget = 'C:\Omika\Apps\DiagnosticsWeb',
[string]$BackupRoot = 'C:\Omika\Backups'
)
$ErrorActionPreference = 'Stop'
# --- Resolution de PackageRoot APRES le bloc param (bug : $PSScriptRoot est
# vide dans une valeur par defaut de param()). Valeur par defaut : dossier
# PARENT du dossier du script (le package phase5b2 contient Deploy\ +
# DiagnosticsWorker\ + DiagnosticsWeb\ + Database\).
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 de phase5b2>' 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
}
}
function Deploy-Folder([string]$Name, [string]$Source, [string]$Target, [string[]]$Preserve) {
$backup = $null
if (Test-Path -LiteralPath $Target) {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$backup = Join-Path $BackupRoot ("{0}-{1}" -f $Name, $stamp)
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"
}
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 (secret en 5B2B) :
# on restaure les fichiers de config depuis la sauvegarde s'ils existaient.
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"
}
}
}
}
# --- 1. Prérequis ---------------------------------------------------------
Write-Step 'Prérequis'
Assert-Admin
$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue
if (-not $dotnet) {
Write-Error "Runtime .NET introuvable. Installez 'ASP.NET Core Runtime 10.x' (hébergement) sur le serveur."
exit 1
}
$hasAspNetCore10 = (& $dotnet.Source --list-runtimes 2>$null) -match 'Microsoft\.AspNetCore\.App\s+10\.'
if (-not $hasAspNetCore10) {
Write-Warning "Runtime 'Microsoft.AspNetCore.App 10.x' non détecté. Le Worker et le Web (framework-dependent) en ont besoin. Installez le ASP.NET Core Runtime 10.x avant de démarrer les applications."
}
$workerSource = Join-Path $PackageRoot 'DiagnosticsWorker'
$webSource = Join-Path $PackageRoot 'DiagnosticsWeb'
if (-not (Test-Path -LiteralPath $workerSource)) { Write-Error "Publication Worker introuvable : $workerSource"; exit 1 }
if (-not (Test-Path -LiteralPath $webSource)) { Write-Error "Publication Web introuvable : $webSource"; exit 1 }
# --- 2+3+4. Dossiers + copie + sauvegarde ---------------------------------
Write-Step 'Copie des publications (avec sauvegarde)'
New-Item -ItemType Directory -Force -Path $BackupRoot | Out-Null
Deploy-Folder 'DiagnosticsWorker' $workerSource $WorkerTarget @('appsettings.json', 'appsettings.Production.json')
Deploy-Folder 'DiagnosticsWeb' $webSource $WebTarget @('appsettings.json', 'appsettings.Production.json')
# --- 5+6. Service Windows (Worker uniquement) -----------------------------
Write-Step 'Service Windows Worker'
$workerExe = Join-Path $WorkerTarget 'Omika.Diagnostics.Worker.exe'
if (-not (Test-Path -LiteralPath $workerExe)) {
Write-Error "Exécutable Worker introuvable : $workerExe"
exit 1
}
# Binaire = VRAI Windows Service ?
$serviceCapable = Test-Path -LiteralPath (Join-Path $WorkerTarget 'Microsoft.Extensions.Hosting.WindowsServices.dll')
if (-not $serviceCapable) {
Write-Error "Le binaire Worker fourni n'est PAS un Windows Service (Microsoft.Extensions.Hosting.WindowsServices.dll absent). Re-publier avec EnableWindowsService=true. Aucun service ne sera cree."
exit 1
}
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
$serviceInstalled = $false
if ($existing) {
# Deja installe : NE JAMAIS demarrer. Forcer Manual + Stopped tant que la
# Phase 5C n'a pas valide la source JSONL distante.
Set-Service -Name $ServiceName -StartupType Manual
Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
Write-Host " Service '$ServiceName' existant : force en Manual + Stopped (jamais demarre avant Phase 5C)."
$serviceInstalled = $true
} else {
# Compte de service definitif = A_DETERMINER_EN_PHASE_5C. Pour ne JAMAIS
# figer LocalSystem/NetworkService automatiquement, la creation du service
# est DIFFEREE a la Phase 5C. Seuls les fichiers sont installes ici.
Write-Warning "Creation du service '$ServiceName' DIFFEREE a la Phase 5C (compte definitif = A_DETERMINER_EN_PHASE_5C). Fichiers installes uniquement, aucun service cree, aucun compte fige."
}
# --- Résumé ----------------------------------------------------------------
Write-Host ''
Write-Host 'DEPLOIEMENT 5B2 TERMINE (fichiers copies ; service STOPPE / Manual).' -ForegroundColor Green
Write-Host " Worker : $WorkerTarget"
Write-Host " Web : $WebTarget (hébergement NON décidé ici — voir README-DEPLOYMENT.txt)"
Write-Host ''
if ($serviceInstalled) {
Write-Host 'OmikaDiagnosticsWorker installed but intentionally stopped.' -ForegroundColor Yellow
Write-Host 'Phase 5C is required before enabling or starting the service.' -ForegroundColor Yellow
} else {
Write-Host 'OmikaDiagnosticsWorker service creation deferred to Phase 5C.' -ForegroundColor Yellow
Write-Host 'No service installed: no Windows restart can start the Worker before Phase 5C.' -ForegroundColor Yellow
Write-Host 'Phase 5C is required before enabling or starting the service.' -ForegroundColor Yellow
}
Write-Host ''
Write-Host 'RAPPELS (a faire manuellement, jamais automatise ici) :' -ForegroundColor Yellow
Write-Host " - Injecter la connection string PostgreSQL via variable d'environnement"
Write-Host " ConnectionStrings__DiagnosticsDatabase (ou appsettings.Production.json local, ACL restreinte)."
Write-Host " - Définir le dossier JSONL : Observability__Remote__Directory."
Write-Host " - Appliquer le schéma : Deploy\Install-DatabaseSchema.ps1 (mot de passe jamais en dur)."
Write-Host " - Durcir PostgreSQL (003) : Deploy\Harden-DatabaseRoles.ps1 (contexte administrateur)."
Write-Host " - Décider l'hébergement Web (IIS ou Kestrel) après vérification des rôles serveur."
Write-Host " - Aucune règle firewall 5432/4318 n'a été ni ne doit être ajoutée."
Write-Host " - Phase 5C : determiner le compte de service (A_DETERMINER_EN_PHASE_5C),"
Write-Host " creer/activer le service, puis seulement alors demarrer le Worker."
+143
View File
@@ -0,0 +1,143 @@
# ============================================================
# Omika Diagnostic Center - Tests NON invasifs des scripts de deploiement
# (Phase 5B2B.3). AUCUN changement systeme : aucune creation de service,
# aucune connexion PostgreSQL, aucun psql execute, aucun fichier serveur.
# ============================================================
# Valide :
# 1. Syntaxe PowerShell 5.1 (0 erreur) des 3 scripts.
# 2. Resolution des chemins par defaut ($PSScriptRoot corrige).
# 3. Detection psql (explicite / PATH / installation standard / absent).
# 4. Garanties install-phase5b2.ps1 : aucun service cree, aucun demarrage.
# ============================================================
$ErrorActionPreference = 'Stop'
$repo = 'D:\Avalonia\Omika'
$deploySrc = Join-Path $repo 'infra\deploy'
$deployPkg = Join-Path $repo 'artifacts\phase5b2\Deploy'
$tests = 0
$fails = 0
function Assert([bool]$condition, [string]$label) {
$script:tests++
if ($condition) {
Write-Host ("PASS {0}" -f $label) -ForegroundColor Green
} else {
$script:fails++
Write-Host ("FAIL {0}" -f $label) -ForegroundColor Red
}
}
Write-Host "=== 1. Syntaxe PowerShell 5.1 (0 erreur) ==="
$scripts = @(
(Join-Path $deploySrc 'Install-DatabaseSchema.ps1'),
(Join-Path $deploySrc 'install-phase5b2.ps1'),
(Join-Path $deploySrc 'Harden-DatabaseRoles.ps1')
)
foreach ($s in $scripts) {
$tokens = $null; $errors = $null
[System.Management.Automation.Language.Parser]::ParseFile($s, [ref]$tokens, [ref]$errors) | Out-Null
Assert (($errors -eq $null) -or ($errors.Count -eq 0)) ("Syntaxe OK : " + (Split-Path $s -Leaf))
}
Write-Host ""
Write-Host "=== 2. Resolution des chemins par defaut (bug PSScriptRoot corrige) ==="
# Miroir EXACT des expressions des scripts (calculees APRES le bloc param).
$packageRoot = Split-Path -Parent $deployPkg # phase5b2 (parent de Deploy)
$databaseDir = Join-Path $packageRoot 'Database'
Assert (Test-Path -LiteralPath $packageRoot) "PackageRoot par defaut resout vers le package existant"
Assert (Test-Path -LiteralPath $databaseDir) "DatabaseDir par defaut resout vers ..\Database existant"
Assert (Test-Path -LiteralPath (Join-Path $packageRoot 'DiagnosticsWorker')) "Package contient DiagnosticsWorker"
Assert (Test-Path -LiteralPath (Join-Path $packageRoot 'DiagnosticsWeb')) "Package contient DiagnosticsWeb"
Write-Host ""
Write-Host "=== 3. Detection psql (resolver) ==="
function Resolve-PsqlPath {
param([string]$ExplicitPath)
if ($ExplicitPath) {
if (-not (Test-Path -LiteralPath $ExplicitPath)) { return $null }
return (Resolve-Path -LiteralPath $ExplicitPath).Path
}
$inPath = Get-Command psql.exe -ErrorAction SilentlyContinue
if ($inPath) { return $inPath.Source }
$candidates = @(Get-ChildItem -Path 'C:\Program Files\PostgreSQL\*\bin\psql.exe' -ErrorAction SilentlyContinue)
if ($candidates.Count -gt 0) {
$best = $candidates | ForEach-Object {
$version = 0
$verDir = $_.Directory.Parent.Name
if ($verDir -match '^\d+$') { $version = [int]$verDir }
[PSCustomObject]@{ Path = $_.FullName; Version = $version }
} | Sort-Object -Property Version -Descending | Select-Object -First 1
return $best.Path
}
return $null
}
# A. chemin explicite existant -> retourne ce chemin (le resolver ne verifie
# que Test-Path, donc tout fichier existant sert ici de jeton de test).
$explicitProbe = Join-Path $deploySrc 'Install-DatabaseSchema.ps1'
Assert ((Resolve-PsqlPath -ExplicitPath $explicitProbe) -eq (Resolve-Path -LiteralPath $explicitProbe).Path) "PsqlPath explicite existant -> utilise tel quel"
# B. chemin explicite inexistant -> le script doit s'arreter avec erreur claire
# (on verifie ici que le resolver retourne null, et plus bas que le script
# contient bien Write-Error + exit 1).
Assert ((Resolve-PsqlPath -ExplicitPath 'C:\inexistant\psql.exe') -eq $null) "PsqlPath explicite inexistant -> non resolvable (arret clair du script)"
# C. installation standard PostgreSQL -> detectee (si presente sur ce poste) ;
# sinon le fallback retourne null (comportement documente).
$std = Get-ChildItem -Path 'C:\Program Files\PostgreSQL\*\bin\psql.exe' -ErrorAction SilentlyContinue
Assert ($std -ne $null) "Installation standard PostgreSQL detectee (ou absente : fallback null attendu)"
Write-Host (" (poste dev : {0} psql.exe trouve(s) sous C:\Program Files\PostgreSQL)" -f @($std).Count)
Write-Host ""
Write-Host "=== 4. Garanties install-phase5b2.ps1 (aucun service, aucun demarrage) ==="
$installContent = Get-Content -LiteralPath (Join-Path $deploySrc 'install-phase5b2.ps1') -Raw
Assert ($installContent -notmatch 'New-Service') "Aucun New-Service (creation differee a Phase 5C)"
Assert ($installContent -notmatch 'Start-Service') "Aucun Start-Service (jamais de demarrage)"
Assert ($installContent -notmatch '-StartupType\s+Automatic') "Aucun StartupType Automatic"
Assert ($installContent -match 'Set-Service\s+-Name\s+\$ServiceName\s+-StartupType\s+Manual') "Service existant force en Manual"
Assert ($installContent -match 'Stop-Service') "Service existant force en Stopped"
Assert ($installContent -match 'A_DETERMINER_EN_PHASE_5C') "Compte de service non fige (A_DETERMINER_EN_PHASE_5C)"
Write-Host ""
Write-Host "=== 5. Migration 003 (SQL statique, aucun PostgreSQL requis) ==="
$sql003 = Get-Content -LiteralPath (Join-Path $repo 'infra\postgresql\003_diagnostics_roles.sql') -Raw
# 0 commande ALTER INDEX (les commentaires ne comptent pas : ancrage en debut de ligne).
$alterIndexCmds = @([regex]::Matches($sql003, '(?m)^\s*ALTER\s+INDEX\b') | ForEach-Object { $_.Value })
Assert ($alterIndexCmds.Count -eq 0) "Aucune commande ALTER INDEX OWNER (0 occurrence)"
Assert ($sql003 -match 'ALTER\s+TABLE\s+diagnostic_event\s+OWNER\s+TO\s+omika_diagnostics_owner') "ALTER TABLE OWNER diagnostic_event"
Assert ($sql003 -match 'ALTER\s+TABLE\s+telemetry_metric_hourly\s+OWNER\s+TO\s+omika_diagnostics_owner') "ALTER TABLE OWNER telemetry_metric_hourly"
Assert ($sql003 -match 'ALTER\s+SEQUENCE\s+diagnostic_event_id_seq\s+OWNER\s+TO\s+omika_diagnostics_owner') "ALTER SEQUENCE OWNER diagnostic_event_id_seq"
Assert ($sql003 -match 'ALTER\s+SEQUENCE\s+telemetry_metric_hourly_id_seq\s+OWNER\s+TO\s+omika_diagnostics_owner') "ALTER SEQUENCE OWNER telemetry_metric_hourly_id_seq"
Assert ($sql003 -notmatch '(?m)^\s*REASSIGN\s+OWNED') "Aucun REASSIGN OWNED"
Assert ($sql003 -notmatch '(?m)^\s*DROP\b') "Aucun DROP"
Assert ($sql003 -notmatch '(?m)^\s*TRUNCATE\b') "Aucun TRUNCATE"
Assert ($sql003 -notmatch '(?m)^\s*DELETE\s+FROM') "Aucun DELETE"
Assert ($sql003 -notmatch "(?i)\bPASSWORD\s+'") "Aucun mot de passe en dur"
Assert ($sql003 -match 'GRANT\s+CONNECT\s+ON\s+DATABASE\s+omika_diagnostics\s+TO\s+omika_diagnostics_worker') "Worker : CONNECT database"
Assert ($sql003 -match 'GRANT\s+USAGE\s+ON\s+SCHEMA\s+public\s+TO\s+omika_diagnostics_worker') "Worker : USAGE schema public"
Assert ($sql003 -match 'GRANT\s+INSERT\s+ON\s+TABLE\s+diagnostic_event\s+TO\s+omika_diagnostics_worker') "Worker : INSERT diagnostic_event"
Assert ($sql003 -match 'GRANT\s+USAGE\s+ON\s+SEQUENCE\s+diagnostic_event_id_seq\s+TO\s+omika_diagnostics_worker') "Worker : USAGE sequence"
Assert ($sql003 -notmatch 'GRANT\s+(SELECT|UPDATE|DELETE|TRUNCATE|REFERENCES|TRIGGER)\s+ON\s+TABLE\s+diagnostic_event\s+TO\s+omika_diagnostics_worker') "Worker : aucun SELECT/UPDATE/DELETE/TRUNCATE/REFERENCES/TRIGGER"
Assert ($sql003 -match 'GRANT\s+CONNECT\s+ON\s+DATABASE\s+omika_diagnostics\s+TO\s+[^;]*omika_diagnostics_web') "Web : CONNECT database"
Assert ($sql003 -match 'GRANT\s+USAGE\s+ON\s+SCHEMA\s+public\s+TO\s+[^;]*omika_diagnostics_web') "Web : USAGE schema public"
Assert ($sql003 -match 'GRANT\s+SELECT\s+ON\s+TABLE\s+diagnostic_event\s+TO\s+omika_diagnostics_web') "Web : SELECT diagnostic_event"
Assert ($sql003 -notmatch 'GRANT\s+(INSERT|UPDATE|DELETE|TRUNCATE|REFERENCES|TRIGGER)\s+ON\s+TABLE\s+diagnostic_event\s+TO\s+omika_diagnostics_web') "Web : aucun INSERT/UPDATE/DELETE/TRUNCATE/REFERENCES/TRIGGER"
Assert ($sql003 -notmatch 'GRANT\s+\w+\s+ON\s+(TABLE\s+)?telemetry_metric_hourly') "Aucun droit sur telemetry_metric_hourly"
Assert ($sql003 -match 'ALTER\s+ROLE\s+omika_diagnostics_owner\s+.*NOLOGIN') "owner : NOLOGIN"
Assert ($sql003 -match 'ALTER\s+ROLE\s+omika_diagnostics_worker\s+.*LOGIN') "worker : LOGIN"
Assert ($sql003 -match 'ALTER\s+ROLE\s+omika_diagnostics_web\s+.*LOGIN') "web : LOGIN"
Assert ($sql003 -notmatch 'DROP\s+ROLE\s+omika_diagnostics_app') "Ancien role : aucun DROP ROLE"
Assert ($sql003 -notmatch 'ALTER\s+ROLE\s+omika_diagnostics_app\s+NOLOGIN') "Ancien role : aucun NOLOGIN automatique"
Write-Host ""
Write-Host ("=== RESULTAT : {0} tests, {1} echec(s) ===" -f $tests, $fails)
if ($fails -gt 0) { exit 1 } else { exit 0 }
@@ -0,0 +1,175 @@
# ============================================================
# Omika - Tests NON invasifs de Install-OmikaServerFiles.ps1 (Phase 5C.2B)
# ============================================================
# Valide le correctif WhatIf / SHA256 :
# A. Get-Sha256Hex sur fichier connu.
# B. Get-Sha256Hex sous $WhatIfPreference = $true (aucun null).
# C. Package valide + WhatIf -> SHA256 calcule, exit 0, aucune exception.
# D. Hash incorrect + WhatIf -> mismatch detecte, arret avant copie (exit 1).
# E. Aucune mutation filesystem sous -WhatIf (dossiers + copie).
# F. PackageRoot explicite honore.
# G. PackageRoot automatique ($PSScriptRoot parent).
# AUCUN changement systeme : aucune copie reelle, aucun service, aucun admin.
# ============================================================
[CmdletBinding(SupportsShouldProcess = $true)]
param()
$ErrorActionPreference = 'Stop'
$repo = 'D:\Avalonia\Omika'
$scriptPath = Join-Path $repo 'infra\deploy\Install-OmikaServerFiles.ps1'
$pkgRoot = Join-Path $repo 'artifacts\server-full'
$tests = 0
$fails = 0
function Assert([bool]$condition, [string]$label) {
$script:tests++
if ($condition) {
Write-Host ("PASS {0}" -f $label) -ForegroundColor Green
}
else {
$script:fails++
Write-Host ("FAIL {0}" -f $label) -ForegroundColor Red
}
}
# Exécute un bloc dans un process Windows PowerShell 5.1 enfant (exit-safe),
# retourne @{ Output; ExitCode }. On baisse localement $ErrorActionPreference à
# 'Continue' pendant l'appel natif : ainsi la stderr du natif (ErrorRecord) ne
# lève pas, et 2>&1 la capture dans la sortie.
function Invoke-ChildPS51([string]$scriptText) {
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($scriptText))
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$out = @(& powershell.exe -NoProfile -NonInteractive -EncodedCommand $encoded 2>&1)
$exitCode = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $prevEAP
}
$text = ($out | ForEach-Object { $_.ToString() }) -join "`n"
return @{ Output = $text; ExitCode = $exitCode }
}
Write-Host "=== 1. Syntaxe PowerShell 5.1 (0 erreur) ==="
$tokens = $null; $errors = $null
[System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$tokens, [ref]$errors) | Out-Null
Assert (($errors -eq $null) -or ($errors.Count -eq 0)) "Syntaxe OK : Install-OmikaServerFiles.ps1"
Write-Host ""
Write-Host "=== 2. Extraction des fonctions (AST) + module temporaire ==="
$content = Get-Content -LiteralPath $scriptPath -Raw
$ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$tokens, [ref]$errors)
$want = @('Get-Sha256Hex', 'Test-PackageChecksums', 'Deploy-AppFolder')
$module = New-Object System.Text.StringBuilder
foreach ($f in $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) {
if ($want -contains $f.Name) {
[void]$module.AppendLine($f.Extent.Text)
}
}
foreach ($name in $want) {
Assert ($module.ToString() -match ("function\s+" + [regex]::Escape($name) + "\b")) ("Fonction extraite : " + $name)
}
$modulePath = Join-Path $env:TEMP 'omika-isf-functions.ps1'
[System.IO.File]::WriteAllText($modulePath, $module.ToString(), (New-Object System.Text.UTF8Encoding($true)))
. $modulePath
Write-Host ""
Write-Host "=== A/B. Get-Sha256Hex (unitaire) ==="
$knownFile = Join-Path $env:TEMP 'omika-isf-known.txt'
[System.IO.File]::WriteAllText($knownFile, 'abc', (New-Object System.Text.UTF8Encoding($false)))
$expectedHash = 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
Assert ((Get-Sha256Hex -Path $knownFile) -eq $expectedHash) "A. Hash connu 'abc' = ba7816bf... correct"
$prevWhatIf = $WhatIfPreference
$WhatIfPreference = $true
try {
$hashUnderWhatIf = Get-Sha256Hex -Path $knownFile
Assert ($hashUnderWhatIf -eq $expectedHash) "B. Sous WhatIfPreference=true : hash correct (aucun null)"
Assert ($hashUnderWhatIf -match '^[0-9a-f]{64}$') "B. Format hex lowercase 64 caracteres"
}
finally {
$WhatIfPreference = $prevWhatIf
}
Write-Host ""
Write-Host "=== C. Package valide + WhatIf (Test-PackageChecksums) ==="
$childC = @"
`$ErrorActionPreference = 'Stop'
. '$modulePath'
`$WhatIfPreference = `$true
Test-PackageChecksums -packageRoot '$pkgRoot'
"@
$resC = Invoke-ChildPS51 $childC
Assert ($resC.ExitCode -eq 0) "C. Package valide + WhatIf : exit 0"
Assert ($resC.Output -match 'SHA256 OK') "C. Package valide + WhatIf : SHA256 calcule et OK"
Write-Host ""
Write-Host "=== D. Hash incorrect + WhatIf (mismatch detecte, arret) ==="
$corruptDir = Join-Path $env:TEMP 'omika-isf-corrupt'
if (Test-Path -LiteralPath $corruptDir) { Remove-Item -LiteralPath $corruptDir -Recurse -Force }
New-Item -ItemType Directory -Force -Path $corruptDir | Out-Null
[System.IO.File]::WriteAllText((Join-Path $corruptDir 'test.txt'), 'hello', (New-Object System.Text.UTF8Encoding($false)))
[System.IO.File]::WriteAllText((Join-Path $corruptDir 'SHA256SUMS.txt'), '0000000000000000000000000000000000000000000000000000000000000000 test.txt', (New-Object System.Text.UTF8Encoding($false)))
$childD = @"
`$ErrorActionPreference = 'Stop'
. '$modulePath'
`$WhatIfPreference = `$true
Test-PackageChecksums -packageRoot '$corruptDir'
"@
$resD = Invoke-ChildPS51 $childD
Assert ($resD.ExitCode -eq 1) "D. Hash incorrect + WhatIf : exit 1 (arret avant copie)"
Assert ($resD.Output -match 'Checksum SHA256 KO') "D. Hash incorrect + WhatIf : mismatch detecte"
Write-Host ""
Write-Host "=== E. Aucune mutation filesystem sous -WhatIf ==="
$tempRoot = Join-Path $env:TEMP ('omika-isf-mut-' + [guid]::NewGuid().ToString('N'))
# E1 : boucle de creation d'arborescence (replique de la logique top-level).
$WhatIfPreference = $true
$appsRoot = Join-Path $tempRoot 'Apps'
$folders = @(
$appsRoot,
(Join-Path $appsRoot 'OmikaApi'),
(Join-Path $appsRoot 'OmikaWorker'),
(Join-Path $tempRoot 'Data\Api\Diagnostics\Jsonl'),
(Join-Path $tempRoot 'Data\DiagnosticsWorker\Checkpoint'),
(Join-Path $tempRoot 'Logs\Api'),
(Join-Path $tempRoot 'Config'),
(Join-Path $tempRoot 'Backups'),
(Join-Path $tempRoot 'Deploy')
)
foreach ($folder in $folders) {
if ($PSCmdlet.ShouldProcess($folder, 'Créer le dossier')) {
New-Item -ItemType Directory -Force -Path $folder | Out-Null
}
}
# E2 : Deploy-AppFolder (sauvegarde + copie) sous WhatIf.
Deploy-AppFolder -Name 'OmikaApi' `
-Source (Join-Path $pkgRoot 'OmikaApi') `
-Target (Join-Path $appsRoot 'OmikaApi') `
-BackupRoot (Join-Path $tempRoot 'Backups') `
-Preserve @('appsettings.json')
$WhatIfPreference = $prevWhatIf
Assert (-not (Test-Path -LiteralPath $tempRoot)) "E. Aucun dossier/fichier cree sous WhatIf (arborescence + copie)"
Write-Host ""
Write-Host "=== F/G. Resolution PackageRoot ==="
# G : automatique = parent du dossier Deploy (mirroir exact de $PSScriptRoot).
$autoResolved = Split-Path -Parent (Join-Path $pkgRoot 'Deploy')
Assert ($autoResolved -eq $pkgRoot) "G. PackageRoot automatique (parent du dossier Deploy) resout vers le package"
# F : explicite = honore (le script ne resout que si -not $PackageRoot).
Assert ($content -match 'if \(-not \$PackageRoot\)') "F. Garde 'if (-not PackageRoot)' presente (explicite honore)"
Write-Host ""
Write-Host ("=== RESULTAT : {0} tests, {1} echec(s) ===" -f $tests, $fails)
if ($fails -gt 0) { exit 1 } else { exit 0 }
@@ -0,0 +1,361 @@
# ============================================================
# Omika - Tests NON invasifs des scripts d'installation des services
# ============================================================
# AUCUN changement systeme : aucune creation de service, aucune ACL posee,
# aucune connexion PostgreSQL/HFSQL. Seule une reservation BRIEF de ports
# loopback est faite (pour tester le module), immediatement relachee.
#
# Valide :
# 1. Syntaxe PowerShell 5.1 (0 erreur) de TOUS les fichiers de deploiement.
# 2. Import du module commun + presence des fonctions attendues.
# 3. Garanties : aucun demarrage de service, aucune creation de service hors
# du module, aucun port en dur, -WhatIf / -Desinstaller supportes.
# 4. Fiche reelle de chaque service (nom, executable, compte par defaut).
# 5. Comportement reel de la verification de port (3 cas).
# ============================================================
$ErrorActionPreference = 'Stop'
$deploySrc = Split-Path -Parent $PSScriptRoot
$repo = Split-Path -Parent (Split-Path -Parent $deploySrc)
$tests = 0
$fails = 0
function Assert([bool]$condition, [string]$label) {
$script:tests++
if ($condition) {
Write-Host ("PASS {0}" -f $label) -ForegroundColor Green
}
else {
Write-Host ("FAIL {0}" -f $label) -ForegroundColor Red
$script:fails++
}
}
$moduleCommun = Join-Path $deploySrc 'Omika.Deploy.Common.psm1'
# --- 1. Syntaxe PowerShell 5.1 ----------------------------------------------
Write-Host '=== 1. Syntaxe PowerShell 5.1 (0 erreur) ==='
$fichiers = @(
$moduleCommun,
(Join-Path $deploySrc 'Install-OmikaApi.ps1'),
(Join-Path $deploySrc 'Install-OmikaWorker.ps1'),
(Join-Path $deploySrc 'Install-OmikaDiagnosticsApi.ps1'),
(Join-Path $deploySrc 'Install-OmikaDiagnosticsWorker.ps1'),
(Join-Path $deploySrc 'Install-OmikaDiagnosticsWeb.ps1'),
(Join-Path $deploySrc 'Install-OmikaLanguesApi.ps1'),
(Join-Path $deploySrc 'Install-OmikaServices.ps1'),
(Join-Path $deploySrc 'Test-PortDisponible.ps1'),
(Join-Path $deploySrc 'New-OmikaLanguesManifeste.ps1'),
(Join-Path $deploySrc 'Install-OmikaServerFiles.ps1')
)
foreach ($fichier in $fichiers) {
if (-not (Test-Path -LiteralPath $fichier)) {
Assert $false ("Fichier present : " + (Split-Path $fichier -Leaf))
continue
}
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile($fichier, [ref]$tokens, [ref]$errors) | Out-Null
$ok = (($null -eq $errors) -or ($errors.Count -eq 0))
Assert $ok ("Syntaxe OK : " + (Split-Path $fichier -Leaf))
if (-not $ok) {
foreach ($e in $errors) { Write-Host (" " + $e.Message) -ForegroundColor DarkRed }
}
}
# --- 2. Module commun --------------------------------------------------------
Write-Host ''
Write-Host '=== 2. Module commun : import + fonctions exportees ==='
Import-Module $moduleCommun -Force
Assert $true 'Import du module sans erreur'
$fonctionsAttendues = @(
'Assert-OmikaAdmin', 'Get-OmikaPaths', 'New-OmikaDirectory',
'Resolve-OmikaAccountName', 'Assert-OmikaServiceAccount',
'Test-OmikaServiceLogonRight', 'Grant-OmikaAcl',
'Get-OmikaServiceState', 'Install-OmikaService', 'Remove-OmikaService',
'Write-OmikaServiceSummary', 'Test-OmikaPortLibre'
)
foreach ($nom in $fonctionsAttendues) {
$existe = $null -ne (Get-Command -Name $nom -ErrorAction SilentlyContinue)
Assert $existe ("Fonction exportee : $nom")
}
# Le controle du code retour d'icacls est la correction cle de l'ancien script.
$contenuModule = Get-Content -LiteralPath $moduleCommun -Raw
Assert ($contenuModule -match 'LASTEXITCODE') 'Grant-OmikaAcl controle le code retour (LASTEXITCODE)'
Assert ($contenuModule -match 'icacls\.exe') 'ACL posees via icacls.exe (appel explicite)'
Assert ($contenuModule -match 'SeServiceLogonRight') 'Droit « Ouvrir une session en tant que service » verifie'
Assert ($contenuModule -notmatch '(?im)^\s*Start-Service\b') 'Le module ne demarre aucun service'
Assert ($contenuModule -notmatch '(?im)^\s*Restart-Service\b') 'Le module ne redemarre aucun service'
Assert ($contenuModule -match 'Resolve-OmikaAccountName') 'Compte normalise en ORDINATEUR\compte (forme non ambigue)'
# --- Non-regression : detection d'existence d'un compte ----------------------
# Le repli ADSI ([ADSI]("WinNT://ORDINATEUR/compte,user")) ne leve AUCUNE erreur
# pour un compte inexistant : il annoncait « Existe = True » a tort. Un faux
# positif est le pire des resultats ici (le script creerait un service avec un
# compte absent). Le repli est desormais net.exe user. Ces deux assertions
# verrouillent la correction.
# On ne cherche [ADSI] que dans le CODE : le module cite legitimement ADSI dans
# le commentaire qui documente la raison de son retrait.
$codeModule = @(Get-Content -LiteralPath $moduleCommun | Where-Object { $_ -notmatch '^\s*#' })
$adsiDansLeCode = @($codeModule | Where-Object { $_ -match '\[ADSI\]' })
Assert ($adsiDansLeCode.Count -eq 0) 'Aucun appel ADSI dans le code (source du faux positif)'
$compteAbsent = Test-OmikaLocalAccount -Account '.\svc_omika_compte_absent_xyz'
Assert ($compteAbsent.Existe -eq $false) 'Compte inexistant detecte ABSENT (aucun faux positif)'
$comptePresent = Test-OmikaLocalAccount -Account (".\" + $env:USERNAME)
Assert ($comptePresent.Existe -eq $true) ("Compte existant detecte PRESENT ({0})" -f $env:USERNAME)
# --- 3. Fiche reelle de chaque service --------------------------------------
Write-Host ''
Write-Host '=== 3. Un fichier par deploiement, fiche conforme ==='
$servicesAttendus = @(
@{ Script = 'Install-OmikaApi.ps1'; Service = 'OmikaApi'; Exe = 'Omika.Api.exe'; Compte = '\.\\svc_omika_api' }
@{ Script = 'Install-OmikaWorker.ps1'; Service = 'OmikaWorker'; Exe = 'Omika.Worker.exe'; Compte = '\.\\svc_omika_worker' }
@{ Script = 'Install-OmikaDiagnosticsApi.ps1'; Service = 'OmikaDiagnosticsApi'; Exe = 'Omika.Diagnostics.Api.exe'; Compte = '\.\\svc_omika_diagapi' }
@{ Script = 'Install-OmikaDiagnosticsWorker.ps1'; Service = 'OmikaDiagnosticsWorker'; Exe = 'Omika.Diagnostics.Worker.exe'; Compte = '\.\\svc_omika_diag' }
@{ Script = 'Install-OmikaDiagnosticsWeb.ps1'; Service = 'OmikaDiagnosticsWeb'; Exe = 'Omika.Diagnostics.Web.exe'; Compte = '\.\\svc_omika_diagweb' }
@{ Script = 'Install-OmikaLanguesApi.ps1'; Service = 'OmikaLanguesApi'; Exe = 'Omika.Langues.Api.exe'; Compte = '\.\\svc_omika_langues' }
)
foreach ($fiche in $servicesAttendus) {
$chemin = Join-Path $deploySrc $fiche.Script
$contenu = Get-Content -LiteralPath $chemin -Raw
Assert ($contenu -match ('\$ServiceName\s*=\s*''' + $fiche.Service + '''')) `
("{0} : nom de service par defaut = {1}" -f $fiche.Script, $fiche.Service)
Assert ($contenu -match [regex]::Escape($fiche.Exe)) `
("{0} : executable attendu {1}" -f $fiche.Script, $fiche.Exe)
# Motif concatene en CHAINES SIMPLES : en chaine double, PowerShell
# n'echappe pas avec '\' -> "\$Account" deviendrait '\' + la valeur de la
# variable $Account (vide), et le motif ne correspondrait jamais.
Assert ($contenu -match ('\$Account\s*=\s*''' + $fiche.Compte + '''')) `
("{0} : compte par defaut = {1}" -f $fiche.Script, $fiche.Compte)
Assert ($contenu -match 'CmdletBinding\(SupportsShouldProcess\s*=\s*\$true\)') `
("{0} : prend en charge -WhatIf" -f $fiche.Script)
Assert ($contenu -match '\$Desinstaller') `
("{0} : prend en charge -Desinstaller (rollback)" -f $fiche.Script)
Assert ($contenu -match 'Assert-OmikaServiceAccount') `
("{0} : valide le compte de service" -f $fiche.Script)
Assert ($contenu -match 'Import-Module') `
("{0} : utilise le module commun" -f $fiche.Script)
Assert ($contenu -notmatch '(?im)^\s*Start-Service\b') `
("{0} : ne demarre aucun service" -f $fiche.Script)
Assert ($contenu -notmatch '(?im)^\s*New-Service\b') `
("{0} : ne cree pas de service directement (passe par Install-OmikaService)" -f $fiche.Script)
Assert ($contenu -notmatch '(?im)^\s*Set-Service\b.*-StartupType\s+Automatic') `
("{0} : ne force pas le demarrage automatique" -f $fiche.Script)
}
# --- 4. Garanties transverses ------------------------------------------------
Write-Host ''
Write-Host '=== 4. Garanties transverses ==='
# Aucun port en dur : le port se decide contre le plan Apache (doc §6bis).
# On ne cherche PAS le simple mot « 5099 » (il apparait legitimement dans les
# exemples d'usage et dans le test de port) mais les deux formes qui
# constitueraient un CHOIX code en dur : une URL ASPNETCORE_URLS et une valeur
# par defaut de parametre.
$portEnDur = @()
foreach ($fichier in (Get-ChildItem -LiteralPath $deploySrc -Filter *.ps1 -Recurse)) {
$contenu = Get-Content -LiteralPath $fichier.FullName -Raw
if ($contenu -match '127\.0\.0\.1:5099') {
$portEnDur += ("{0} (ASPNETCORE_URLS en dur)" -f $fichier.Name)
}
# 0 est autorise : il signifie « aucun port fourni », ce n'est pas un choix.
elseif ($contenu -match '(?im)^\s*\[int\]\s*\$Port\w*\s*=\s*[1-9]\d*') {
$portEnDur += ("{0} (valeur par defaut de -Port)" -f $fichier.Name)
}
}
Assert ($portEnDur.Count -eq 0) ("Aucun port code en dur dans infra\deploy ({0} fichier(s))" -f $portEnDur.Count)
if ($portEnDur.Count -gt 0) { Write-Host (" " + ($portEnDur -join ', ')) -ForegroundColor DarkRed }
# L'orchestrateur appelle bien les 6 scripts dedies.
$orchestrateur = Get-Content -LiteralPath (Join-Path $deploySrc 'Install-OmikaServices.ps1') -Raw
foreach ($fiche in $servicesAttendus) {
Assert ($orchestrateur -match [regex]::Escape($fiche.Script)) `
("Orchestrateur : appelle {0}" -f $fiche.Script)
}
Assert ($orchestrateur -match 'SupportsShouldProcess') 'Orchestrateur : prend en charge -WhatIf'
Assert ($orchestrateur -match 'InclureLangues') 'Orchestrateur : service Langues optionnel (-InclureLangues)'
Assert ($orchestrateur -match 'SansDiagnosticsApi') 'Orchestrateur : API centrale optionnelle (-SansDiagnosticsApi)'
# --- 5. Comportement reel de la verification de port ------------------------
Write-Host ''
Write-Host '=== 5. Verification de port : 3 cas reels ==='
# Normalisation du compte (forme '.\' -> 'ORDINATEUR\compte').
$normalise = Resolve-OmikaAccountName -Account '.\svc_omika_api'
Assert ($normalise -eq ('{0}\svc_omika_api' -f $env:COMPUTERNAME)) "Compte normalise : $normalise"
# A. Port libre.
$verdictLibre = Test-OmikaPortLibre -Port 5099 -VerifierPlagesReservees:$false
Assert ($verdictLibre.Libre) ("Port 5099 detecte DISPONIBLE" + $(if ($verdictLibre.Libre) { '' } else { " (raison : $($verdictLibre.Raisons))" }))
# B. Port reellement occupe.
$verrou = $null
$verdictOccupe = $null
try {
$verrou = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 5123)
$verrou.Start()
$verdictOccupe = Test-OmikaPortLibre -Port 5123 -VerifierPlagesReservees:$false
}
finally {
if ($verrou) { try { $verrou.Stop() } catch { } }
}
Assert (($null -ne $verdictOccupe) -and (-not $verdictOccupe.Libre)) 'Port 5123 occupe detecte INDISPONIBLE'
# C. Port dans la plage ephemere.
$verdictEphemere = Test-OmikaPortLibre -Port 50000 -VerifierPlagesReservees:$false
Assert (-not $verdictEphemere.Libre) 'Port 50000 (plage ephemere) detecte INDISPONIBLE'
Assert ($verdictEphemere.Raisons -match 'EPHEMERE') 'La raison invoquee mentionne la plage ephemere'
# --- 6. Manifeste des packs de langue : generation reelle --------------------
# Le manifeste contient deux champs MESURES (tailleOctets, sha256) : une valeur
# inventee ne se voit que sur le telephone, ou le pack est refuse. On verifie
# donc la generation contre une source independante (Get-FileHash / FileInfo).
Write-Host ''
Write-Host '=== 6. Manifeste des packs de langue : generation reelle ==='
$generateur = Join-Path $deploySrc 'New-OmikaLanguesManifeste.ps1'
Assert (Test-Path -LiteralPath $generateur) 'Generateur de manifeste present'
$bac = Join-Path ([System.IO.Path]::GetTempPath()) ('omika-manifeste-' + [guid]::NewGuid().ToString('N').Substring(0, 8))
try {
$null = New-Item -ItemType Directory -Path (Join-Path $bac 'es\versFrancais') -Force
$null = New-Item -ItemType Directory -Path (Join-Path $bac 'es\depuisFrancais') -Force
[System.IO.File]::WriteAllBytes((Join-Path $bac 'es\versFrancais\encoder.onnx'), (New-Object byte[] 4096))
[System.IO.File]::WriteAllBytes((Join-Path $bac 'es\versFrancais\vocab.spm'), (New-Object byte[] 256))
[System.IO.File]::WriteAllBytes((Join-Path $bac 'es\depuisFrancais\encoder.onnx'), (New-Object byte[] 4096))
# Fichier inutile volontairement livre : exclu du chargement par l'application,
# mais telecharge par chaque telephone s'il est declare. Doit etre signale.
[System.IO.File]::WriteAllBytes((Join-Path $bac 'es\versFrancais\decoder_with_past_model.onnx'), (New-Object byte[] 512))
$sortieGeneration = (& $generateur -RootPathLangues $bac 6>&1 | Out-String)
$manifeste = Join-Path $bac 'manifest.json'
Assert (($LASTEXITCODE -eq 0) -and (Test-Path -LiteralPath $manifeste)) 'Generation : manifeste ecrit (code 0)'
Assert ($sortieGeneration -match 'past_key_values') 'Generation : decodeur « _with_past » signale (poids mobile inutile)'
$json = Get-Content -LiteralPath $manifeste -Raw -Encoding UTF8 | ConvertFrom-Json
Assert (@($json.packs).Count -eq 1) 'Manifeste : 1 pack'
Assert (@($json.packs[0].fichiers).Count -eq 4) 'Manifeste : 4 fichiers declares'
$ecarts = 0
foreach ($pack in $json.packs) {
foreach ($fichier in $pack.fichiers) {
$chemin = Join-Path (Join-Path (Join-Path $bac $pack.code) $fichier.sens) $fichier.nomFichier
if ((Get-FileHash -LiteralPath $chemin -Algorithm SHA256).Hash.ToLowerInvariant() -ne $fichier.sha256) { $ecarts++ }
if ((Get-Item -LiteralPath $chemin).Length -ne $fichier.tailleOctets) { $ecarts++ }
}
}
Assert ($ecarts -eq 0) 'Manifeste : sha256 et tailles conformes (source independante)'
$octetsManifeste = [System.IO.File]::ReadAllBytes($manifeste)
Assert (-not ($octetsManifeste[0] -eq 0xEF -and $octetsManifeste[1] -eq 0xBB -and $octetsManifeste[2] -eq 0xBF)) `
'Manifeste : UTF-8 sans BOM (fichier de donnees)'
$null = & $generateur -RootPathLangues $bac -Verifier 6>&1
Assert ($LASTEXITCODE -eq 0) 'Verification : manifeste conforme (code 0)'
# Un modele remplace APRES generation doit etre detecte : c'est le controle
# qui evite un pack silencieusement refuse sur le telephone.
[System.IO.File]::WriteAllBytes((Join-Path $bac 'es\versFrancais\vocab.spm'), (New-Object byte[] 300))
$sortieManifeste = (& $generateur -RootPathLangues $bac -Verifier 6>&1 | Out-String)
Assert ($LASTEXITCODE -eq 1) 'Verification : modele modifie detecte (code 1)'
Assert ($sortieManifeste -match 'empreinte differente') 'Verification : ecart d''empreinte annonce'
# Le mobile compare le sens de facon sensible a la casse : un sens a la
# mauvaise casse ferait refuser le pack sans aucune erreur cote serveur.
$casse = Join-Path $bac 'manifest-casse.json'
(Get-Content -LiteralPath $manifeste -Raw -Encoding UTF8).Replace('"sens": "versFrancais"', '"sens": "VersFrancais"') |
Set-Content -LiteralPath $casse -Encoding UTF8
$null = & $generateur -RootPathLangues $bac -Sortie $casse -Verifier 6>&1
Assert ($LASTEXITCODE -eq 1) 'Verification : sens a la mauvaise casse detecte (refus mobile)'
Remove-Item -LiteralPath $casse -Force -ErrorAction SilentlyContinue
# --- Cohabitation avec le script d'export ---------------------------------
# Le dossier des packs peut contenir le cache HuggingFace du script d'export
# (.hf-cache, des milliers de fichiers) et la sauvegarde du manifeste : ni
# l'un ni l'autre n'est un modele, et les signaler noierait le vrai controle
# sous des faux positifs.
$null = New-Item -ItemType Directory -Path (Join-Path $bac '.hf-cache\hub\models--Helsinki-NLP--opus-mt-es-fr') -Force
[System.IO.File]::WriteAllBytes((Join-Path $bac '.hf-cache\hub\models--Helsinki-NLP--opus-mt-es-fr\blob-pytorch'), (New-Object byte[] 128))
# Deuxieme ecriture : le generateur conserve manifest.json.bak
$null = & $generateur -RootPathLangues $bac 6>&1
Assert (Test-Path -LiteralPath (Join-Path $bac 'manifest.json.bak')) 'Regeneration : sauvegarde manifest.json.bak ecrite'
$sortieTechnique = (& $generateur -RootPathLangues $bac -Verifier 6>&1 | Out-String)
Assert ($LASTEXITCODE -eq 0) 'Cohabitation : verification conforme malgre .hf-cache et .bak'
Assert ($sortieTechnique -notmatch 'hf-cache') 'Cohabitation : .hf-cache jamais signale comme fichier non declare'
Assert ($sortieTechnique -notmatch 'manifest\.json\.bak') 'Cohabitation : la sauvegarde .bak n''est pas un faux positif'
}
finally {
if (Test-Path -LiteralPath $bac) {
Remove-Item -LiteralPath $bac -Recurse -Force -ErrorAction SilentlyContinue
}
}
# --- 6bis. Manifeste : poids partages d'un modele familial (commun/) ---------
# Un modele familial range ses ~1,15 Go dans <code>/commun/ et ne laisse qu'un
# modele.json par sens. Si le generateur traitait « commun » comme un sens
# inconnu, il l'ignorerait : le telephone ne pourrait JAMAIS telecharger les
# poids, et le pack serait declare mais inutilisable.
Write-Host ''
Write-Host '=== 6bis. Manifeste : poids partages (commun/) ==='
$bacFamille = Join-Path ([System.IO.Path]::GetTempPath()) ('omika-manifeste-famille-' + [guid]::NewGuid().ToString('N').Substring(0, 8))
try {
$null = New-Item -ItemType Directory -Path (Join-Path $bacFamille 'pt\commun') -Force
$null = New-Item -ItemType Directory -Path (Join-Path $bacFamille 'pt\versFrancais') -Force
$null = New-Item -ItemType Directory -Path (Join-Path $bacFamille 'pt\depuisFrancais') -Force
[System.IO.File]::WriteAllBytes((Join-Path $bacFamille 'pt\commun\encoder_model.onnx'), (New-Object byte[] 2048))
[System.IO.File]::WriteAllBytes((Join-Path $bacFamille 'pt\commun\source.spm'), (New-Object byte[] 128))
[System.IO.File]::WriteAllText((Join-Path $bacFamille 'pt\versFrancais\modele.json'), '{ "version": 1, "jetonCible": ">>fra<<" }')
[System.IO.File]::WriteAllText((Join-Path $bacFamille 'pt\depuisFrancais\modele.json'), '{ "version": 1, "jetonCible": ">>por<<" }')
$sortieFamille = (& $generateur -RootPathLangues $bacFamille 6>&1 | Out-String)
$manifesteFamille = Join-Path $bacFamille 'manifest.json'
Assert (($LASTEXITCODE -eq 0) -and (Test-Path -LiteralPath $manifesteFamille)) 'Manifeste familial : ecrit (code 0)'
Assert ($sortieFamille -notmatch 'sens inconnu') 'Manifeste familial : commun/ n''est pas traite comme un sens inconnu'
$jsonFamille = Get-Content -LiteralPath $manifesteFamille -Raw -Encoding UTF8 | ConvertFrom-Json
$sensFamille = @($jsonFamille.packs[0].fichiers | ForEach-Object { $_.sens } | Sort-Object -Unique)
Assert ($sensFamille -contains 'commun') 'Manifeste familial : le dossier commun est declare'
Assert ($sensFamille -contains 'versFrancais' -and $sensFamille -contains 'depuisFrancais') `
'Manifeste familial : les deux sens (modele.json) sont declares'
Assert ((@($jsonFamille.packs[0].fichiers | Where-Object { $_.sens -eq 'commun' })).Count -eq 2) `
'Manifeste familial : les poids sont declares une seule fois, sous commun'
$null = & $generateur -RootPathLangues $bacFamille -Verifier 6>&1
Assert ($LASTEXITCODE -eq 0) 'Manifeste familial : verification conforme (code 0)'
}
finally {
if (Test-Path -LiteralPath $bacFamille) {
Remove-Item -LiteralPath $bacFamille -Recurse -Force -ErrorAction SilentlyContinue
}
}
# --- Resultat ----------------------------------------------------------------
Write-Host ''
Write-Host ("=== RESULTAT : {0} tests, {1} echec(s) ===" -f $tests, $fails)
if ($fails -gt 0) { exit 1 } else { exit 0 }