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
@@ -0,0 +1,47 @@
# ============================================================
# OpenTelemetry Collector — configuration EXEMPLE (Phase 5)
# Destinée au Windows Server 2025 (Phase serveur).
# NE PAS installer sur le poste développeur (§47).
#
# Pipeline retenu : recevoir OTLP HTTP → batch → memory limiter
# → envoyer vers le backend Omika (→ PostgreSQL). Le transport
# exact Collector → backend Omika est finalisé en Phase serveur
# (§48/§49) : PAS d'exporter PostgreSQL maison, PAS de faux
# récepteur OTLP codé dans cette phase.
# ============================================================
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
processors:
batch:
send_batch_size: 512
timeout: 2s
memory_limiter:
check_interval: 5s
limit_mib: 256
spike_limit_mib: 64
exporters:
# Exporter PLACEHOLDER (standard). À remplacer par l'exporteur vers le
# backend Omika défini en Phase serveur. Aucun exporter PostgreSQL
# non standard n'est inventé ici (§48).
debug:
verbosity: basic
service:
pipelines:
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug]
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug]
+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 }
+20
View File
@@ -0,0 +1,20 @@
# =============================================================================
# Omika — Observabilité externe : variables d'environnement (exemple)
# =============================================================================
# Copier vers `.env` et adapter. NE JAMAIS versionner `.env` ni les secrets réels.
# Aucun secret Omika (mot de passe utilisateur, token mobile, clé licence, secret
# SMTP) ne doit être réutilisé ici (§30).
# --- Réseau / endpoints OTLP ---
# Endpoint OTLP du Collector Omika, vu depuis l'API/Worker Omika.
# (ex. http://<ip-vm-observability>:4318)
OTLP_ENDPOINT=
# Endpoint du Collector SigNoz (interne à la pile, jamais exposé au mobile).
SIGNOZ_OTLP_ENDPOINT=signoz-otel-collector:4317
# --- Identifiants ClickHouse (générés, spécifiques à la pile d'observabilité) ---
CLICKHOUSE_PASSWORD=change-me
# --- Rétention (configurée côté SigNoz/ClickHouse, voir docs) ---
# Logs / traces / métriques : fenêtres proposées par défaut (voir docs/observability).
+145
View File
@@ -0,0 +1,145 @@
# =============================================================================
# Omika — Pile d'observabilité externe (Phase 5)
# =============================================================================
# Topologie cible (VM Ubuntu dédiée via Hyper-V, §21) :
#
# API/Worker Omika ──OTLP HTTP/protobuf──▶ omika-otel-collector ──▶ SigNoz
# │
# ClickHouse
#
# Ce fichier est une référence MINIMALE et commentée. Les images SigNoz doivent
# être épinglées aux versions stables officielles au moment de l'installation
# (l'installateur officiel SigNoz reste la référence exacte) :
# curl -sL https://github.com/SigNoz/signoz/raw/main/scripts/install.sh | bash
#
# RÈGLE ABSOLUE : rien ici n'est requis pour qu'Omika fonctionne. Arrêter toute
# cette pile ne casse jamais le métier (fail-open, §2/§97).
# =============================================================================
services:
# ---------------------------------------------------------------------------
# 1. Notre Collector Omika (le SEUL point d'entrée OTLP d'Omika).
# Reçoit OTLP des composants autorisés, limite la mémoire, batche, exporte
# vers SigNoz. Ports 4317/4318 = réseau privé uniquement, jamais publics.
# ---------------------------------------------------------------------------
omika-otel-collector:
image: otel/opentelemetry-collector:0.128.0
container_name: omika-otel-collector
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
environment:
SIGNOZ_OTLP_ENDPOINT: "signoz-otel-collector:4317"
ports:
# Exposer uniquement sur le réseau privé de la VM (bind 127.0.0.1 ou VPN).
# JAMAIS "0.0.0.0" sur un réseau public.
- "127.0.0.1:4318:4318" # HTTP/protobuf (utilisé par Omika)
- "127.0.0.1:4317:4317" # gRPC (non utilisé par Omika, désactivable)
deploy:
resources:
limits:
cpus: "1.0"
memory: 768M # cohérent avec memory_limiter (512 MiB + marge)
reservations:
memory: 256M
networks: [observability]
restart: unless-stopped
# ---------------------------------------------------------------------------
# 2. ClickHouse (stockage des signaux — infrastructure externe, jamais embarqué
# dans Omika, §22). Réseau restreint au réseau interne de la pile.
# ---------------------------------------------------------------------------
clickhouse:
image: clickhouse/clickhouse-server:24.8
container_name: signoz-clickhouse
ports:
- "127.0.0.1:8123:8123"
- "127.0.0.1:9000:9000"
volumes:
- clickhouse-data:/var/lib/clickhouse
environment:
CLICKHOUSE_DB: signoz_metrics
CLICKHOUSE_USER: signoz
CLICKHOUSE_PASSWORD: "${CLICKHOUSE_PASSWORD:-change-me}"
deploy:
resources:
limits:
cpus: "2.0"
memory: 4G
reservations:
memory: 1G
networks: [observability]
restart: unless-stopped
# ---------------------------------------------------------------------------
# 3. Collector SigNoz : reçoit l'OTLP de notre collector et écrit dans
# ClickHouse. Jamais exposé au mobile ni au public (§5/§28).
# ---------------------------------------------------------------------------
signoz-otel-collector:
image: signoz/otel-collector:0.128.1
container_name: signoz-otel-collector
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./signoz/otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
ports:
- "127.0.0.1:4319:4317" # entrée OTLP SigNoz (interne à la pile)
environment:
CLICKHOUSE_HOST: clickhouse
CLICKHOUSE_USER: signoz
CLICKHOUSE_PASSWORD: "${CLICKHOUSE_PASSWORD:-change-me}"
deploy:
resources:
limits:
cpus: "1.0"
memory: 768M
reservations:
memory: 256M
depends_on: [clickhouse]
networks: [observability]
restart: unless-stopped
# ---------------------------------------------------------------------------
# 4. Query service SigNoz (backend API) + 5. Frontend (UI support/dev).
# Réservés à l'équipe support autorisée, jamais aux utilisateurs Omika (§27).
# ---------------------------------------------------------------------------
signoz-query-service:
image: signoz/query-service:0.72.0
container_name: signoz-query-service
ports:
- "127.0.0.1:8085:8085"
environment:
ClickHouseUrl: "tcp://signoz:@clickhouse:9000"
STORAGE: clickhouse
SIGNOZ_LOCAL_DB_PATH: /var/lib/signoz
volumes:
- signoz-query-data:/var/lib/signoz
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
depends_on: [clickhouse]
networks: [observability]
restart: unless-stopped
signoz-frontend:
image: signoz/frontend:0.72.0
container_name: signoz-frontend
ports:
- "127.0.0.1:3301:3301"
depends_on: [signoz-query-service]
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
networks: [observability]
restart: unless-stopped
volumes:
clickhouse-data:
signoz-query-data:
networks:
observability:
driver: bridge
@@ -0,0 +1,70 @@
# =============================================================================
# Omika — OpenTelemetry Collector (Phase 5)
# =============================================================================
# Collector MINIMAL et compréhensible : reçoit l'OTLP des composants Omika
# autorisés (API/Worker), limite strictement la mémoire, batche, puis exporte
# vers SigNoz (qui écrit dans ClickHouse).
#
# Réseau : le port OTLP (4317 gRPC / 4318 HTTP) NE DOIT JAMAIS être exposé
# publiquement. Il reste sur le réseau privé de la VM d'observabilité, ouvert
# uniquement aux composants Omika autorisés (firewall).
#
# Omika n'utilise QUE le protocole OTLP HTTP/protobuf (port 4318) — voir
# docs/observability/README.md pour le choix gRPC vs HTTP/protobuf.
# =============================================================================
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
# Limites mémoire STRICTES : le Collector ne peut jamais consommer toute la
# RAM du serveur/VM (§20). memory_limiter doit rester le PREMIER processor.
memory_limiter:
check_interval: 5s
limit_mib: 512 # plafond mémoire global du Collector
spike_limit_mib: 128 # marge de pointe autorisée au-dessus du plafond
# Regroupement en lots (bornes explicites, §14).
batch:
send_batch_size: 512
send_batch_max_size: 1024
timeout: 2s
exporters:
# Export vers le Collector SigNoz (qui écrit dans ClickHouse). Endpoint privé,
# injecté via variable d'environnement (jamais codé en dur en production).
otlp/signoz:
endpoint: "${SIGNOZ_OTLP_ENDPOINT:-signoz-otel-collector:4317}"
tls:
# Segment privé (VM) : TLS désactivé par défaut. Pour un segment non privé,
# retirer `insecure: true` et configurer les certificats correctement.
# JAMAIS DangerousAcceptAnyServerCertificateValidator (§29).
insecure: true
# Exporteur de debug pour la validation d'installation UNIQUEMENT.
# À retirer (ou laisser au niveau basic) une fois la chaîne validée (§88).
debug:
verbosity: basic
service:
telemetry:
logs:
level: "warn"
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/signoz, debug]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/signoz, debug]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/signoz, debug]
@@ -0,0 +1,77 @@
-- ============================================================
-- Omika Diagnostic Center — base dédiée omika_diagnostics (§14)
-- Outil SUPPORT uniquement : jamais utilisé par le métier (§13).
-- 100 % Windows / PostgreSQL. Aucune dépendance Linux/Docker.
-- PostgreSQL est NON critique : Omika fonctionne sans cette base.
-- ============================================================
-- ------------------------------------------------------------
-- Table principale des événements (§15)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS diagnostic_event (
id BIGSERIAL PRIMARY KEY,
event_id UUID NOT NULL, -- clé de déduplication (§16)
timestamp_utc TIMESTAMPTZ NOT NULL,
priority TEXT, -- P0..P4
level TEXT, -- trace..critical
event_code TEXT NOT NULL,
component TEXT,
trace_id UUID, -- X-Omika-TraceId conservé (§33)
session_id UUID,
operation_id UUID,
app_version TEXT,
platform TEXT,
error_code TEXT,
message TEXT, -- Phase 6 (crash reporting)
exception_type TEXT, -- Phase 6
stack_trace TEXT, -- Phase 6
duration_ms BIGINT,
source TEXT, -- mobile/api/worker/collector
metadata_json JSONB, -- whitelist sécurité (§18/§19)
ingested_at_utc TIMESTAMPTZ NOT NULL
);
-- Déduplication at-least-once : INSERT ... ON CONFLICT (event_id) DO NOTHING (§16)
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostic_event_event_id
ON diagnostic_event (event_id);
-- Index utiles uniquement (§17) — pas d'index sur chaque colonne
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_timestamp_utc ON diagnostic_event (timestamp_utc);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_trace_id ON diagnostic_event (trace_id);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_operation_id ON diagnostic_event (operation_id);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_session_id ON diagnostic_event (session_id);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_event_code ON diagnostic_event (event_code);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_component ON diagnostic_event (component);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_level ON diagnostic_event (level);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_error_code ON diagnostic_event (error_code);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_app_version ON diagnostic_event (app_version);
-- ------------------------------------------------------------
-- Métriques agrégées horaires (§28) — low-cardinality (§30)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS telemetry_metric_hourly (
id BIGSERIAL PRIMARY KEY,
bucket_utc TIMESTAMPTZ NOT NULL,
metric_name TEXT NOT NULL,
component TEXT NOT NULL DEFAULT '', -- '' = métrique globale
value DOUBLE PRECISION NOT NULL,
count BIGINT NOT NULL,
min_value DOUBLE PRECISION NOT NULL,
max_value DOUBLE PRECISION NOT NULL,
tags_json JSONB NOT NULL DEFAULT '{}'::jsonb
);
-- Upsert : un agrégat unique par (bucket, métrique, composant, tags)
CREATE UNIQUE INDEX IF NOT EXISTS ux_telemetry_metric_hourly
ON telemetry_metric_hourly (bucket_utc, metric_name, component, tags_json);
CREATE INDEX IF NOT EXISTS ix_telemetry_metric_hourly_bucket
ON telemetry_metric_hourly (bucket_utc);
-- ------------------------------------------------------------
-- Rétention (§45) — valeur configurable (Diagnostics:RetentionDays, défaut 60).
-- NON exécutée automatiquement : à planifier en maintenance (purge destructive
-- documentée). Exemple (à adapter) :
-- DELETE FROM diagnostic_event
-- WHERE timestamp_utc < now() - interval '60 days';
-- ------------------------------------------------------------
+23
View File
@@ -0,0 +1,23 @@
-- ============================================================
-- Omika Diagnostic Center — 002_actor_key.sql (Phase 5B2A.1)
-- Pseudonymisation ActorKey (HMAC-SHA256) pour la recherche support.
-- ============================================================
-- IDEMPOTENT : ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible (syntaxe disponible depuis 9.6).
--
-- ActorKey = HMAC-SHA256( "v1|tenant:<CodeClient>|collaborateur:<CollaborateurId>" )
-- calculé côté SERVEUR (Omika.Api) depuis l'identité AUTHENTIFIÉE (§3).
-- Format : hexadécimal minuscule, 64 caractères. NULL = système / sans
-- utilisateur / clé HMAC indisponible (fail-open).
-- ActorKeyVersion = version de l'algorithme (1 aujourd'hui) → rotation future.
-- ============================================================
ALTER TABLE diagnostic_event
ADD COLUMN IF NOT EXISTS actor_key TEXT,
ADD COLUMN IF NOT EXISTS actor_key_version INTEGER;
-- Recherche support « tous les diagnostics d'un collaborateur » (§12) :
-- index composé (actor_key, timestamp_utc DESC).
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_actor_key_timestamp
ON diagnostic_event (actor_key, timestamp_utc DESC);
+123
View File
@@ -0,0 +1,123 @@
-- ============================================================
-- Omika Diagnostic Center -- 003_diagnostics_roles.sql (Phase 5B2B.3)
-- PostgreSQL hardening : least privilege (Worker / Web / Owner).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres),
-- BEFORE Phase 5C. DO NOT run from the development PC.
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op.
-- - Non destructive : NO DROP, no role/object/data removal.
-- - NO password, NO secret (LOGIN role passwords are set manually via
-- \password, never in this file).
--
-- Role model :
-- omika_diagnostics_owner NOLOGIN : owner of DB/schema/tables/sequences/indexes.
-- omika_diagnostics_worker LOGIN : Worker runtime (JSONL import).
-- omika_diagnostics_web LOGIN : Web runtime (read only).
-- omika_diagnostics_app (existing) : KEPT (controlled transition),
-- NOT disabled here.
--
-- Server apply order : 001 (schema) -> 002 (actor_key) -> 003 (roles).
-- ============================================================
-- ------------------------------------------------------------
-- 1. Create roles if absent (NO password here).
-- ------------------------------------------------------------
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_owner') THEN
CREATE ROLE omika_diagnostics_owner;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_worker') THEN
CREATE ROLE omika_diagnostics_worker;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_web') THEN
CREATE ROLE omika_diagnostics_web;
END IF;
END
$$;
-- ------------------------------------------------------------
-- 2. Secure attributes (idempotent).
-- All: NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS.
-- ------------------------------------------------------------
ALTER ROLE omika_diagnostics_owner NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS NOLOGIN;
ALTER ROLE omika_diagnostics_worker NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
ALTER ROLE omika_diagnostics_web NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
-- ------------------------------------------------------------
-- 3. Ownership transfer (explicit -- no global REASSIGN OWNED).
-- Only Omika Diagnostics objects are targeted.
-- ------------------------------------------------------------
ALTER DATABASE omika_diagnostics OWNER TO omika_diagnostics_owner;
ALTER SCHEMA public OWNER TO omika_diagnostics_owner;
ALTER TABLE diagnostic_event OWNER TO omika_diagnostics_owner;
ALTER TABLE telemetry_metric_hourly OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostic_event_id_seq OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE telemetry_metric_hourly_id_seq OWNER TO omika_diagnostics_owner;
-- PostgreSQL 18:
-- Index ownership follows table ownership.
-- No ALTER INDEX OWNER is required.
-- (Sequences are NOT auto-transferred by ALTER TABLE: the ALTER SEQUENCE
-- OWNER commands above are therefore kept and required.)
-- ------------------------------------------------------------
-- 4. Schema "public" hardening.
-- Decision : YES, REVOKE CREATE -- the database is dedicated to the
-- Diagnostic Center; only the owner/admin should create objects.
-- (PostgreSQL 15+ already revokes it for new databases; this is an
-- idempotent belt-and-suspenders.)
-- ------------------------------------------------------------
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- ------------------------------------------------------------
-- 5. MINIMUM runtime privileges (from real code audit).
--
-- WORKER (omika_diagnostics_worker):
-- * diagnostic_event : INSERT only
-- (INSERT ... ON CONFLICT (event_id) DO NOTHING -- dedup).
-- * sequence diagnostic_event_id_seq : USAGE (id BIGSERIAL).
-- * NO SELECT/UPDATE/DELETE (60-day retention is NOT implemented;
-- telemetry_metric_hourly upsert is registered but has NO caller).
--
-- WEB (omika_diagnostics_web): SELECT ONLY on diagnostic_event
-- (QueryAsync count+page, GetByTraceId, GetByOperationId, IsAvailableAsync).
--
-- NO privilege granted on telemetry_metric_hourly while no code uses it
-- (least privilege).
-- ------------------------------------------------------------
-- Explicit connection intent (PUBLIC keeps default CONNECT; documented only,
-- not hardened beyond requested scope).
GRANT CONNECT ON DATABASE omika_diagnostics TO omika_diagnostics_worker, omika_diagnostics_web;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_worker, omika_diagnostics_web;
-- Worker : JSONL import -> diagnostic_event.
GRANT INSERT ON TABLE diagnostic_event TO omika_diagnostics_worker;
GRANT USAGE ON SEQUENCE diagnostic_event_id_seq TO omika_diagnostics_worker;
-- Web : read only.
GRANT SELECT ON TABLE diagnostic_event TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- 6. Legacy role omika_diagnostics_app : KEPT (no DROP, no NOLOGIN here).
-- After transfer it owns no Diagnostics object and has no object privilege;
-- disabling it (ALTER ROLE ... NOLOGIN) is an EXPLICIT administrator action
-- AFTER validating the new roles.
-- ------------------------------------------------------------
-- ============================================================
-- END 003. Manual next steps (outside this script):
-- 1. Set passwords: psql -> \password omika_diagnostics_worker
-- then \password omika_diagnostics_web (never in script/log/history).
-- 2. Point Worker: Username=omika_diagnostics_worker ; Web:
-- Username=omika_diagnostics_web (ConnectionStrings__DiagnosticsDatabase).
-- 3. Validate Worker (INSERT) and Web (SELECT), then only after,
-- disable omika_diagnostics_app (ALTER ROLE ... NOLOGIN) if desired.
-- ============================================================
@@ -0,0 +1,62 @@
-- ============================================================
-- Omika Diagnostic Center -- 003_diagnostics_roles_preprod.sql
-- VARIANTE PRÉPROD LOCALE de 003_diagnostics_roles.sql (T21).
-- ============================================================
-- ⚠️ EXCLUSIVEMENT LOCAL / PRÉPROD — jamais en Production.
--
-- TRANSFORMATION UNIQUE par rapport à l'original 003 :
-- « ALTER DATABASE omika_diagnostics OWNER TO ... »
-- « GRANT CONNECT ON DATABASE omika_diagnostics TO ... »
-- → base cible renommée « omika_diagnostics_preprod ».
-- Rôles, privilèges, ownership et idempotence STRICTEMENT identiques.
-- (L'original code en dur le nom de la base centrale Production
-- « omika_diagnostics » ; cette variante ne change QUE ce nom.)
--
-- Teste réellement, sur une base locale jetable, l'exécutabilité des
-- rôles/grants 003 : création de rôles cluster-wide, ownership, REVOKE
-- public, GRANT worker/web. Aucun mot de passe (posé manuellement via
-- \password, jamais dans ce fichier).
-- ============================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_owner') THEN
CREATE ROLE omika_diagnostics_owner;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_worker') THEN
CREATE ROLE omika_diagnostics_worker;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_web') THEN
CREATE ROLE omika_diagnostics_web;
END IF;
END
$$;
ALTER ROLE omika_diagnostics_owner NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS NOLOGIN;
ALTER ROLE omika_diagnostics_worker NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
ALTER ROLE omika_diagnostics_web NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
ALTER DATABASE omika_diagnostics_preprod OWNER TO omika_diagnostics_owner;
ALTER SCHEMA public OWNER TO omika_diagnostics_owner;
ALTER TABLE diagnostic_event OWNER TO omika_diagnostics_owner;
ALTER TABLE telemetry_metric_hourly OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostic_event_id_seq OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE telemetry_metric_hourly_id_seq OWNER TO omika_diagnostics_owner;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
GRANT CONNECT ON DATABASE omika_diagnostics_preprod TO omika_diagnostics_worker, omika_diagnostics_web;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_worker, omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostic_event TO omika_diagnostics_worker;
GRANT USAGE ON SEQUENCE diagnostic_event_id_seq TO omika_diagnostics_worker;
GRANT SELECT ON TABLE diagnostic_event TO omika_diagnostics_web;
-- ============================================================
-- END 003 (variante preprod). Les mots de passe des rôles LOGIN
-- (worker/web) sont posés manuellement via \password si nécessaire.
-- ============================================================
@@ -0,0 +1,39 @@
-- ============================================================
-- Omika Diagnostic Center -- 004_diagnostics_retention.sql (Phase 5C.2C)
-- PostgreSQL : moindre privilège pour la rétention diagnostic.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres).
-- DO NOT run from the development PC.
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, NO role/object removal.
-- - NO password, NO secret.
--
-- Context (Phase 5C.2C) : the retention loop (DiagnosticRetentionWorker) purges
-- diagnostic_event by BOUNDED batches :
--
-- DELETE FROM diagnostic_event
-- WHERE id IN (
-- SELECT id FROM diagnostic_event
-- WHERE timestamp_utc < @cutoff
-- ORDER BY timestamp_utc ASC, id ASC
-- LIMIT @batchSize
-- )
--
-- This SQL requires :
-- * DELETE on diagnostic_event (the target of the DELETE) ;
-- * SELECT on diagnostic_event (the sub-query reads the table).
--
-- The worker role (omika_diagnostics_worker) currently holds only INSERT +
-- USAGE sequence (see 003). Least privilege : grant ONLY SELECT + DELETE.
-- NO UPDATE, NO TRUNCATE, NO DDL, NO privilege on telemetry_metric_hourly.
-- ============================================================
GRANT SELECT, DELETE ON TABLE diagnostic_event TO omika_diagnostics_worker;
-- ============================================================
-- END 004. Apply order on server : 001 -> 002 -> 003 -> 004.
-- No server action is performed by the development PC.
-- ============================================================
@@ -0,0 +1,45 @@
-- ============================================================
-- Omika Diagnostic Center -- 005_diagnostics_client_context.sql
-- Tâche 13-A : contexte client / serveur / environnement (additif).
-- ============================================================
-- IDEMPOTENT : ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible (syntaxe disponible depuis 9.6).
--
-- Ajoute au diagnostic_event trois dimensions de contexte de déploiement,
-- NULLABLES : les événements antérieurs restent parfaitement valides
-- (client_code = NULL, server_instance = NULL, environment = NULL),
-- AUCUN backfill, AUCUNE valeur inventée (« UNKNOWN »/« N/A » interdits).
--
-- client_code : CodeClient de référence = ClientBase.CL_Code
-- (identifiant NON secret du client Omika, jamais
-- Client.CL_CodeClient, CL_CleAPIInstall, clé licence,
-- ActorKey ni nom société).
-- server_instance : identifiant d'instance serveur (configuré, ex.
-- « API-SRV-01 ») — T13-D.
-- environment : environnement ASP.NET (Development / Production / …).
--
-- CHOIX DE TYPE (cohérence schéma) : le schéma existant utilise TEXT sans
-- VARCHAR ni CHECK de longueur (priority, level, component, app_version…).
-- On reste cohérent : colonnes TEXT, PAS de CHECK de longueur arbitraire.
-- La validation de longueur (<= 100 / <= 100 / <= 20) sera portée par le
-- code au moment de la résolution (T13-D), comme pour les autres champs.
-- ============================================================
ALTER TABLE diagnostic_event
ADD COLUMN IF NOT EXISTS client_code TEXT,
ADD COLUMN IF NOT EXISTS server_instance TEXT,
ADD COLUMN IF NOT EXISTS environment TEXT;
-- Recherche support « tous les diagnostics d'un client » : index composé
-- (client_code, timestamp_utc DESC), aligné sur le style de
-- ix_diagnostic_event_actor_key_timestamp (002).
--
-- PAS d'index dédié sur server_instance : aucun usage immédiat ne justifie
-- le coût d'écriture/maintenance d'un second index composé (T13 — « composite
-- éventuel »). À réévaluer en T13-F si un filtre serveur devient un hot path.
--
-- PAS d'index sur environment : très faible cardinalité (2-3 valeurs), un
-- index serait peu sélectif et sans bénéfice réel.
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_client_code_timestamp
ON diagnostic_event (client_code, timestamp_utc DESC);
@@ -0,0 +1,69 @@
-- ============================================================
-- Omika Diagnostic Center -- 006_diagnostics_device_license.sql
-- Tâche 17-B : dimensions appareil + licence (additif).
-- ============================================================
-- IDEMPOTENT : ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible (syntaxe disponible depuis 9.6).
--
-- Ajoute au diagnostic_event les dimensions appareil/licence, TOUTES NULLABLES :
-- les événements antérieurs restent parfaitement valides (toutes nouvelles colonnes
-- à NULL), AUCUN backfill, AUCUNE valeur inventée (« UNKNOWN »/« N/A » interdits).
--
-- license_number : numéro de licence fonctionnel = Licence.LI_Numero,
-- dérivé SERVEUR du token authentifié
-- (MobileTokenIdentity.NumeroLicence). JAMAIS la PK HFSQL
-- LI_PKID, JAMAIS un secret, JAMAIS fourni par le mobile.
-- device_key : pseudonyme d'appareil HMAC-SHA256 calculé SERVEUR =
-- HMAC("v1|tenant:<CodeClient>|device:<DeviceId>").
-- JAMAIS le DeviceId brut.
-- device_key_version : version de l'algorithme DeviceKey (1 aujourd'hui).
-- device_manufacturer : fabricant (mobile, déclaratif).
-- device_model : modèle d'appareil (mobile, déclaratif).
-- operating_system : nom de l'OS (mobile, déclaratif).
-- operating_system_version : version de l'OS (mobile, déclaratif).
-- app_build : numéro de build applicatif (mobile, déclaratif).
-- screen_width : largeur d'écran physique (px, >= 0).
-- screen_height : hauteur d'écran physique (px, >= 0).
-- logical_width : largeur d'écran logique (unités indépendantes densité).
-- logical_height : hauteur d'écran logique.
-- display_scale : facteur d'échelle (ex. 1, 1.25, 1.5, 2, 3).
--
-- CHOIX DE TYPE (cohérence schéma) : le schéma existant utilise TEXT sans VARCHAR
-- pour les chaînes (priority, level, component, app_version…) ; on reste cohérent.
-- Les dimensions écran physiques/logiques sont des entiers non négatifs (INTEGER) ;
-- display_scale est un réel (DOUBLE PRECISION, valeurs décimales 1.25/1.5/2.75/3).
-- La validation (longueur, bornes, non-négativité) est portée par le code au moment
-- de la résolution (T17-C), comme pour les autres champs.
-- ============================================================
ALTER TABLE diagnostic_event
ADD COLUMN IF NOT EXISTS license_number TEXT,
ADD COLUMN IF NOT EXISTS device_key TEXT,
ADD COLUMN IF NOT EXISTS device_key_version INTEGER,
ADD COLUMN IF NOT EXISTS device_manufacturer TEXT,
ADD COLUMN IF NOT EXISTS device_model TEXT,
ADD COLUMN IF NOT EXISTS operating_system TEXT,
ADD COLUMN IF NOT EXISTS operating_system_version TEXT,
ADD COLUMN IF NOT EXISTS app_build TEXT,
ADD COLUMN IF NOT EXISTS screen_width INTEGER,
ADD COLUMN IF NOT EXISTS screen_height INTEGER,
ADD COLUMN IF NOT EXISTS logical_width INTEGER,
ADD COLUMN IF NOT EXISTS logical_height INTEGER,
ADD COLUMN IF NOT EXISTS display_scale DOUBLE PRECISION;
-- Index composés immédiatement justifiés (recherche support « tous les diagnostics
-- d'un appareil / d'une licence »), alignés sur le style de
-- ix_diagnostic_event_actor_key_timestamp (002) et
-- ix_diagnostic_event_client_code_timestamp (005).
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_device_key_timestamp
ON diagnostic_event (device_key, timestamp_utc DESC);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_license_number_timestamp
ON diagnostic_event (license_number, timestamp_utc DESC);
-- PAS d'index dédié sur device_manufacturer / device_model / operating_system /
-- operating_system_version / app_build / screen_* : dimensions de filtrage/agrégation
-- futures (T17-E) mais sans usage immédiat justifiant le coût d'écriture/maintenance.
-- PAS d'index sur display_scale (valeur continue, jamais filtrée par égalité).
-- À réévaluer en T17-E lorsque les statistiques de parc seront définies.
@@ -0,0 +1,47 @@
-- ============================================================
-- Omika Diagnostic Center -- 007_diagnostics_usage.sql
-- Tâche 18 : télémétrie d'utilisation des écrans / fonctionnalités (additif).
-- ============================================================
-- IDEMPOTENT : ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible (syntaxe disponible depuis 9.6).
--
-- Ajoute au diagnostic_event deux dimensions d'USAGE fonctionnel, NULLABLES :
-- les événements antérieurs restent parfaitement valides (screen_code = NULL,
-- feature_code = NULL), AUCUN backfill, AUCUNE valeur inventée (« UNKNOWN »/« N/A »
-- interdits).
--
-- screen_code : code fonctionnel STABLE d'un écran (ex. AGENDA, SITE_DETAIL).
-- Produit DEPUIS le catalogue applicatif (jamais un nom de
-- Page/ViewModel, jamais un identifiant métier, jamais une saisie
-- utilisateur). Null pour les événements background sans écran.
-- feature_code: code fonctionnel STABLE d'une action métier significative
-- (ex. EMAIL_SEND, REPORT_GENERATE). Attaché à l'événement précis
-- de l'action, jamais à un « contexte courant » global. Null si non
-- applicable.
--
-- CHOIX DE TYPE (cohérence schéma) : TEXT sans VARCHAR, comme les autres chaînes
-- (priority, level, component, license_number…). La validation (longueur, valeurs
-- bornées) est portée par le code au moment de la résolution, comme les autres
-- champs. Les deux colonnes sont NON SENSIBLES : jamais de DeviceId/UserId/
-- CollaborateurId/email/téléphone/token/id métier dynamique (§9/§10/§72).
-- ============================================================
ALTER TABLE diagnostic_event
ADD COLUMN IF NOT EXISTS screen_code TEXT,
ADD COLUMN IF NOT EXISTS feature_code TEXT;
-- Index de recherche « tous les ScreenView d'un écran dans le temps » (T18, §49) :
-- les agrégations d'usage filtrent par (event_code, screen_code) puis par période,
-- mais l'index composé (screen_code, timestamp_utc DESC) couvre le filtre écran +
-- le tri temporel des diagnostics filtrés (§60), aligné sur le style de
-- ix_diagnostic_event_device_key_timestamp (006).
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_screen_code_timestamp
ON diagnostic_event (screen_code, timestamp_utc DESC);
-- Index (feature_code, timestamp_utc DESC) : filtre fonctionnalité + tri temporel.
-- FeatureCode est une dimension de catalogue à cardinalité TRÈS faible (< 10), donc
-- un index est peu sélectif ; il est néanmoins ajouté par symétrie avec screen_code
-- car le filtre « Tous les diagnostics d'une fonctionnalité » (§60) est prévu.
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_feature_code_timestamp
ON diagnostic_event (feature_code, timestamp_utc DESC);
@@ -0,0 +1,38 @@
-- ============================================================
-- Omika Diagnostic Center -- 008_diagnostics_clients.sql
-- Tâche 19 : registre des clients autorisés (serveurs clients) de l'API centrale.
-- ============================================================
-- IDEMPOTENT : CREATE TABLE IF NOT EXISTS + CREATE UNIQUE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible.
--
-- Une ligne = un SERVEUR CLIENT autorisé à transmettre ses diagnostics vers
-- Omika.Diagnostics.Api (CodeClient métier, ex. BATIK.01).
--
-- client_code : CodeClient métier autoritaire (unique, normalisé UPPER côté code).
-- enabled : FALSE => le client ne peut plus envoyer (réponse 403).
-- api_key_hash : hash SHA-256 (hex 64) de la clé d'accès. JAMAIS la clé en clair.
-- key_prefix : préfixe d'affichage de la clé (ex. "omkdiag_"), NON secret,
-- permet d'identifier la clé dans l'interface sans l'exposer.
-- created_at_utc : création du client.
-- key_created_at_utc : génération de la clé COURANTE (utile après régénération).
-- last_seen_at_utc : dernière réception d'un lot accepté ; NULL si jamais.
--
-- CHOIX DE TYPE : aligné sur diagnostic_event (TEXT, TIMESTAMPTZ). Aucun secret
-- réversible stocké ; la clé n'existe que sous forme de hash SHA-256 (§29/§30).
-- ============================================================
CREATE TABLE IF NOT EXISTS diagnostics_clients (
id BIGSERIAL PRIMARY KEY,
client_code TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
api_key_hash TEXT NOT NULL,
key_prefix TEXT NOT NULL,
created_at_utc TIMESTAMPTZ NOT NULL,
key_created_at_utc TIMESTAMPTZ NOT NULL,
last_seen_at_utc TIMESTAMPTZ
);
-- Unicité par CodeClient (un seul client par code, §26).
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostics_clients_client_code
ON diagnostics_clients (client_code);
@@ -0,0 +1,103 @@
-- ============================================================
-- Omika Diagnostic Center -- 009_diagnostics_clients_grants.sql
-- T22 : moindre privilège pour la table diagnostics_clients (008).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d omika_diagnostics -f 009_...),
-- AFTER 008 (table diagnostics_clients) and AFTER 003/004 (rôles owner/worker/web).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op.
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret (LOGIN role password is set manually via \password).
--
-- Motivations (T22) :
-- 008 crée diagnostics_clients APRÈS 003/004. La table reste donc possédée
-- par le rôle qui exécute 008 (postgres) et ne reçoit AUCUN GRANT applicatif :
-- l'API centrale (authentification ClientCode+ApiKey + last_seen_at_utc) et
-- l'administration Web (/Clients) échoueraient avec leurs comptes de service.
--
-- Rôle AJOUTÉ : omika_diagnostics_api (LOGIN). Le modèle 003 ne prévoyait que
-- owner/worker/web ; Omika.Diagnostics.Api (service distinct du Worker et du
-- Web) n'y figurait pas. Elle n'accède QU'À diagnostics_clients.
--
-- Ownership : même propriétaire logique que les autres tables Diagnostics
-- (omika_diagnostics_owner), comme 003. Les index suivent automatiquement la
-- table ; la séquence BIGSERIAL est transférée explicitement.
--
-- Note portabilité : GRANT CONNECT utilise current_database() — la migration
-- s'applique donc à la base courante (psql -d <base>), sans coder en dur le
-- nom (contrairement à 003 qui, pour ALTER DATABASE, devait le fixer).
-- ============================================================
-- ------------------------------------------------------------
-- 1. Créer le rôle API s'il est absent (aucun mot de passe ici).
-- ------------------------------------------------------------
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_api') THEN
CREATE ROLE omika_diagnostics_api;
END IF;
END
$$;
ALTER ROLE omika_diagnostics_api NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
-- ------------------------------------------------------------
-- 2. Ownership : omika_diagnostics_owner (comme 003).
-- ------------------------------------------------------------
ALTER TABLE diagnostics_clients OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostics_clients_id_seq OWNER TO omika_diagnostics_owner;
-- ------------------------------------------------------------
-- 3. Défense PUBLIC (ceinture + bretelles, idempotent) : aucune
-- table Diagnostics n'est accessible à PUBLIC.
-- ------------------------------------------------------------
REVOKE ALL ON TABLE diagnostics_clients FROM PUBLIC;
-- ------------------------------------------------------------
-- 4. Connexion explicite (intention documentée, comme 003) + schéma.
-- ------------------------------------------------------------
DO $$
BEGIN
EXECUTE format('GRANT CONNECT ON DATABASE %I TO omika_diagnostics_api', current_database());
END
$$;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_api;
-- ------------------------------------------------------------
-- 5. API centrale (Omika.Diagnostics.Api) : SELECT (auth : client,
-- enabled, api_key_hash…) + UPDATE(last_seen_at_utc) uniquement.
-- AUCUN INSERT, AUCUN DELETE, AUCUNE modification de enabled /
-- api_key_hash / key_prefix (moindre privilège, anti-falsification
-- de l'autorisation en cas de compromission de l'API).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_clients TO omika_diagnostics_api;
GRANT UPDATE (last_seen_at_utc) ON TABLE diagnostics_clients TO omika_diagnostics_api;
-- ------------------------------------------------------------
-- 6. Web (Omika.Diagnostics.Web, admin /Clients) : liste + création +
-- activation/désactivation + régénération de clé. AUCUN DELETE
-- (aucune opération de suppression n'existe dans le code T19).
-- USAGE séquence : INSERT ... RETURNING id (id BIGSERIAL).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_clients TO omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostics_clients TO omika_diagnostics_web;
GRANT UPDATE (enabled, api_key_hash, key_prefix, key_created_at_utc)
ON TABLE diagnostics_clients TO omika_diagnostics_web;
GRANT USAGE ON SEQUENCE diagnostics_clients_id_seq TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- 7. Worker (Omika.Diagnostics.Worker) : AUCUN droit sur
-- diagnostics_clients (il n'y accède jamais : import JSONL ->
-- diagnostic_event + rétention diagnostic_event uniquement).
-- Rien à accorder. Aucun REVOKE nécessaire : le rôle n'a reçu
-- aucun privilège sur cette table.
-- ------------------------------------------------------------
-- ============================================================
-- END 009. Ordre serveur : 001 -> 002 -> 003 -> 004 -> 005 -> 006
-- -> 007 -> 008 -> 009. Mots de passe posés manuellement (\password),
-- jamais dans un script/log.
-- ============================================================
@@ -0,0 +1,99 @@
-- ============================================================
-- Omika Diagnostic Center -- 010_diagnostics_web_users.sql
-- T23 : gestion des utilisateurs de Omika.Diagnostics.Web.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 010_...),
-- AFTER 003 (rôles owner/worker/web), 008 (diagnostics_clients) and 009 (rôle api + grants clients).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (CREATE ... IF NOT EXISTS + GRANT idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret (no column stores a clear-text password).
--
-- Une ligne = une PERSONNE autorisée à ouvrir Omika.Diagnostics.Web
-- (username + password_hash + rôle). À NE PAS confondre avec
-- diagnostics_clients (serveurs clients, ClientCode + ApiKey).
--
-- username : identifiant unique (unicité insensible à la casse).
-- password_hash : hash PBKDF2-SHA256 (DiagnosticPasswordHasher). JAMAIS en clair.
-- role : 'Support' ou 'Admin' (validé par le code, pas de CHECK arbitraire).
-- enabled : FALSE => l'utilisateur ne peut plus se connecter.
-- created_at_utc : création du compte.
-- updated_at_utc : dernière mutation du compte.
-- password_changed_at_utc : dernier changement de mot de passe.
-- last_login_at_utc : dernier login réussi ; NULL si jamais.
--
-- CHOIX DE TYPE : aligné sur diagnostic_event / diagnostics_clients (TEXT, TIMESTAMPTZ,
-- BOOLEAN). Aucun VARCHAR, aucun CHECK de longueur (validation portée par le code,
-- comme 005/007/008). Aucune colonne password/plain_password/clear_password.
-- ============================================================
CREATE TABLE IF NOT EXISTS diagnostics_users (
id BIGSERIAL PRIMARY KEY,
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at_utc TIMESTAMPTZ NOT NULL,
updated_at_utc TIMESTAMPTZ NOT NULL,
password_changed_at_utc TIMESTAMPTZ NOT NULL,
last_login_at_utc TIMESTAMPTZ
);
-- Unicité insensible à la casse (§T23/11) : un index sur LOWER(username) garantit
-- qu'un même identifiant ne peut exister deux fois quelle que soit la casse.
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostics_users_username
ON diagnostics_users (LOWER(username));
-- ------------------------------------------------------------
-- Ownership : omika_diagnostics_owner (comme 003/009).
-- ------------------------------------------------------------
ALTER TABLE diagnostics_users OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostics_users_id_seq OWNER TO omika_diagnostics_owner;
-- ------------------------------------------------------------
-- Défense PUBLIC (ceinture + bretelles, idempotent).
-- ------------------------------------------------------------
REVOKE ALL ON TABLE diagnostics_users FROM PUBLIC;
-- ------------------------------------------------------------
-- Connexion explicite + schéma pour omika_diagnostics_web
-- (déjà accordés par 003 ; ré-affirmé idempotent, intention documentée).
-- ------------------------------------------------------------
DO $$
BEGIN
EXECUTE format('GRANT CONNECT ON DATABASE %I TO omika_diagnostics_web', current_database());
END
$$;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- Web (Omika.Diagnostics.Web, admin /Users) : SELECT + INSERT +
-- UPDATE colonne-ciblé (moindre privilège) + USAGE séquence.
-- - INSERT : création utilisateur (id BIGSERIAL).
-- - UPDATE (password_hash, role, enabled, updated_at_utc,
-- password_changed_at_utc, last_login_at_utc) : reset mot de passe,
-- changement rôle, activation/désactivation, last login.
-- - username n'est JAMAIS mis à jour (aucun renommage en V1).
-- - AUCUN DELETE (aucune suppression physique en V1).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_users TO omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostics_users TO omika_diagnostics_web;
GRANT UPDATE (password_hash, role, enabled, updated_at_utc, password_changed_at_utc, last_login_at_utc)
ON TABLE diagnostics_users TO omika_diagnostics_web;
GRANT USAGE ON SEQUENCE diagnostics_users_id_seq TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- API (omika_diagnostics_api) : AUCUN droit sur diagnostics_users.
-- Worker (omika_diagnostics_worker) : AUCUN droit sur diagnostics_users.
-- Aucun GRANT accordé ; les rôles n'ont aucun privilège par défaut.
-- (Aucun REVOKE nécessaire : aucun privilège n'a été accordé.)
-- ------------------------------------------------------------
-- ============================================================
-- END 010. Ordre serveur : 001 -> 002 -> 003 -> 004 -> 005 -> 006
-- -> 007 -> 008 -> 009 -> 010. Mots de passe posés manuellement
-- (\password), jamais dans un script/log.
-- ============================================================
@@ -0,0 +1,140 @@
-- ============================================================
-- Omika Diagnostic Center -- 011_diagnostics_reference.sql
-- T24-A : référentiel administrable ScreenCode / FeatureCode.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 011_...),
-- AFTER 010 (diagnostics_users). Order: 001 -> ... -> 010 -> 011.
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (CREATE ... IF NOT EXISTS + GRANT idempotent
-- + seed ON CONFLICT DO NOTHING).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- PURPOSE :
-- diagnostic_event reste la VÉRITÉ sur ce qui a réellement été observé. Ce
-- référentiel est un ENRICHISSEMENT administrable (libellé / description /
-- ordre / état) depuis Diagnostics.Web. Il ne détermine JAMAIS quels événements
-- sont acceptés : un nouveau code observé reste visible même non référencé.
--
-- reference_type : 'SCREEN_CODE' ou 'FEATURE_CODE' (V1). Extensible plus tard.
-- code : code technique, normalisé TRIM + UPPER (unique par type).
-- label : libellé métier français.
-- description : documentation métier (nullable).
-- enabled : FALSE => archivé (aucun DELETE physique en V1).
-- sort_order : ordre d'affichage.
-- created_at_utc / updated_at_utc : TIMESTAMPTZ (comme 005/007/008/010).
--
-- Ownership : omika_diagnostics_owner (comme 003/008/009/010).
-- ============================================================
CREATE TABLE IF NOT EXISTS diagnostics_reference (
id BIGSERIAL PRIMARY KEY,
reference_type TEXT NOT NULL,
code TEXT NOT NULL,
label TEXT NOT NULL,
description TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at_utc TIMESTAMPTZ NOT NULL,
updated_at_utc TIMESTAMPTZ NOT NULL
);
-- Unicité d'un code POUR SON TYPE, insensible à la casse (T24-A §20) : l'index
-- porte sur UPPER(code), donc 'agenda' / 'AGENDA' / 'Agenda' sont le même code.
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostics_reference_type_code
ON diagnostics_reference (reference_type, UPPER(code));
-- ------------------------------------------------------------
-- Ownership : omika_diagnostics_owner.
-- ------------------------------------------------------------
ALTER TABLE diagnostics_reference OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostics_reference_id_seq OWNER TO omika_diagnostics_owner;
-- ------------------------------------------------------------
-- Défense PUBLIC (ceinture + bretelles, idempotent).
-- ------------------------------------------------------------
REVOKE ALL ON TABLE diagnostics_reference FROM PUBLIC;
-- ------------------------------------------------------------
-- Connexion explicite + schéma pour omika_diagnostics_web
-- (déjà accordés par 003 ; ré-affirmé idempotent).
-- ------------------------------------------------------------
DO $$
BEGIN
EXECUTE format('GRANT CONNECT ON DATABASE %I TO omika_diagnostics_web', current_database());
END
$$;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- Web (Omika.Diagnostics.Web, admin /Referentiel/Admin) : SELECT + INSERT +
-- UPDATE colonne-ciblé (moindre privilège) + USAGE séquence.
-- - INSERT : création d'une entrée (id BIGSERIAL).
-- - UPDATE (label, description, enabled, sort_order, updated_at_utc) :
-- modification + archivage (enabled=false). Le CODE est immuable.
-- - AUCUN DELETE (aucune suppression physique en V1, §24).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_reference TO omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostics_reference TO omika_diagnostics_web;
GRANT UPDATE (label, description, enabled, sort_order, updated_at_utc)
ON TABLE diagnostics_reference TO omika_diagnostics_web;
GRANT USAGE ON SEQUENCE diagnostics_reference_id_seq TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- API (omika_diagnostics_api) : AUCUN droit sur diagnostics_reference.
-- Worker (omika_diagnostics_worker) : AUCUN droit sur diagnostics_reference.
-- Aucun GRANT accordé ; les rôles n'ont aucun privilège par défaut.
-- (Testé négativement : SELECT référentiel REFUSÉ pour api et worker.)
-- ------------------------------------------------------------
-- ------------------------------------------------------------
-- INITIALISATION IDEMPOTENTE (T24-A §36) : les 29 ScreenCode et 3 FeatureCode
-- du catalogue applicatif. ON CONFLICT DO NOTHING => un libellé modifié par un
-- Admin n'est JAMAIS écrasé au redémarrage. Les libellés correspondent EXACTEMENT
-- à DiagnosticScreenCodeCatalog / DiagnosticFeatureCodeCatalog.
-- ------------------------------------------------------------
INSERT INTO diagnostics_reference
(reference_type, code, label, description, enabled, sort_order, created_at_utc, updated_at_utc)
VALUES
('SCREEN_CODE', 'AGENDA', 'Agenda', NULL, TRUE, 10, now(), now()),
('SCREEN_CODE', 'INTERVENTION_SEARCH', 'Recherche d''interventions', NULL, TRUE, 20, now(), now()),
('SCREEN_CODE', 'DOSSIER_LIST', 'Dossiers', NULL, TRUE, 30, now(), now()),
('SCREEN_CODE', 'TEAM', 'Équipe', NULL, TRUE, 40, now(), now()),
('SCREEN_CODE', 'TEAM_DETAIL', 'Détail d''équipe', NULL, TRUE, 50, now(), now()),
('SCREEN_CODE', 'CLIENT_LIST', 'Clients', NULL, TRUE, 60, now(), now()),
('SCREEN_CODE', 'SITE_LIST', 'Sites', NULL, TRUE, 70, now(), now()),
('SCREEN_CODE', 'SETTINGS', 'Paramètres', NULL, TRUE, 80, now(), now()),
('SCREEN_CODE', 'RENDEZVOUS_DETAIL', 'Détail de rendez-vous', NULL, TRUE, 90, now(), now()),
('SCREEN_CODE', 'AGENDA_TEAM', 'Agenda d''équipe', NULL, TRUE, 100, now(), now()),
('SCREEN_CODE', 'INTERVENTION_DETAIL', 'Détail d''intervention', NULL, TRUE, 110, now(), now()),
('SCREEN_CODE', 'INTERVENTION_DOCUMENTS', 'Documents d''intervention', NULL, TRUE, 120, now(), now()),
('SCREEN_CODE', 'INTERVENTION_NOTES', 'Notes d''intervention', NULL, TRUE, 130, now(), now()),
('SCREEN_CODE', 'INTERVENTION_NOTES_ADMIN', 'Notes administratives', NULL, TRUE, 140, now(), now()),
('SCREEN_CODE', 'INTERVENTION_EMAILS', 'Courriels d''intervention', NULL, TRUE, 150, now(), now()),
('SCREEN_CODE', 'EMAIL', 'Nouveau courriel', NULL, TRUE, 160, now(), now()),
('SCREEN_CODE', 'RAPPORT', 'Rapport', NULL, TRUE, 170, now(), now()),
('SCREEN_CODE', 'INTERVENTION_RESOURCES', 'Contenus d''intervention', NULL, TRUE, 180, now(), now()),
('SCREEN_CODE', 'PHOTO_VIEWER', 'Visionneuse photo', NULL, TRUE, 190, now(), now()),
('SCREEN_CODE', 'IMAGE_EDITOR', 'Éditeur d''image', NULL, TRUE, 200, now(), now()),
('SCREEN_CODE', 'INTERVENTION_CONTACTS', 'Contacts d''intervention', NULL, TRUE, 210, now(), now()),
('SCREEN_CODE', 'CONTACT_DETAIL', 'Détail de contact', NULL, TRUE, 220, now(), now()),
('SCREEN_CODE', 'DOSSIER_DETAIL', 'Détail de dossier', NULL, TRUE, 230, now(), now()),
('SCREEN_CODE', 'CLIENT_DETAIL', 'Détail de client', NULL, TRUE, 240, now(), now()),
('SCREEN_CODE', 'SITE_DETAIL', 'Détail de site', NULL, TRUE, 250, now(), now()),
('SCREEN_CODE', 'COLLABORATEUR_DETAIL', 'Détail de collaborateur', NULL, TRUE, 260, now(), now()),
('SCREEN_CODE', 'INTERVENTION_EDIT', 'Formulaire intervention', NULL, TRUE, 270, now(), now()),
('SCREEN_CODE', 'SUIVI_TEMPS_CREATE', 'Saisie de temps', NULL, TRUE, 280, now(), now()),
('SCREEN_CODE', 'COMPTE_RENDU_CREATE', 'Compte rendu', NULL, TRUE, 290, now(), now()),
('FEATURE_CODE', 'INTERVENTION_SEARCH_RUN', 'Recherche d''intervention', NULL, TRUE, 10, now(), now()),
('FEATURE_CODE', 'REPORT_GENERATE', 'Génération de rapport', NULL, TRUE, 20, now(), now()),
('FEATURE_CODE', 'EMAIL_SEND', 'Envoi de courriel', NULL, TRUE, 30, now(), now())
ON CONFLICT DO NOTHING;
-- ============================================================
-- END 011. Ordre serveur : 001 -> 002 -> 003 -> 004 -> 005 -> 006
-- -> 007 -> 008 -> 009 -> 010 -> 011. Aucun DELETE, aucune donnée écrasée.
-- ============================================================
@@ -0,0 +1,90 @@
-- ============================================================
-- Omika Diagnostic Center -- 012_diagnostics_tasks.sql
-- T25 : tâches de traitement des bugs récurrents.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 012_...),
-- AFTER 003 (rôles owner/worker/web) and 010 (diagnostics_users).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (CREATE ... IF NOT EXISTS + GRANT idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Une ligne = une TÂCHE de traitement liée à un « type de bug », c'est-à-dire une
-- signature d'erreur (code événement + code erreur + exception + composant normalisés).
-- La signature est stockée sous sa forme URL-safe (DiagnosticErrorGroup.UrlIdentifier),
-- identique à celle utilisée par la page /Erreurs/{id}. Une seule tâche par signature
-- (unicité) : toutes les occurrences du même type de bug sont couvertes par la tâche.
--
-- group_url_identifier : identifiant URL-safe du groupe d'erreurs lié (unique, immuable).
-- title : intitulé de la tâche.
-- description : description libre du bug (nullable).
-- cause : cause de l'erreur / analyse root cause (nullable).
-- status : 'Todo' | 'InProgress' | 'Resolved' | 'Rejected' (liste fermée,
-- validée par le code, pas de CHECK arbitraire).
-- created_by_username : username de l'auteur (immuable, jamais renommé en V1).
-- assignee_username : username du responsable (nullable, optionnel).
-- created_at_utc : création.
-- updated_at_utc : dernière mutation.
-- resolved_at_utc : date de résolution/rejet ; NULL si statut non terminal.
--
-- CHOIX DE TYPE : aligné sur diagnostics_clients / diagnostics_users (TEXT, TIMESTAMPTZ,
-- BOOLEAN). Aucun VARCHAR, aucun CHECK de longueur (validation portée par le code).
-- Aucune colonne secret. Aucune colonne « password ».
-- ============================================================
CREATE TABLE IF NOT EXISTS diagnostics_tasks (
id BIGSERIAL PRIMARY KEY,
group_url_identifier TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
cause TEXT,
status TEXT NOT NULL,
created_by_username TEXT NOT NULL,
assignee_username TEXT,
created_at_utc TIMESTAMPTZ NOT NULL,
updated_at_utc TIMESTAMPTZ NOT NULL,
resolved_at_utc TIMESTAMPTZ
);
-- Une seule tâche par signature d'erreur (T25 §4).
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostics_tasks_group_url_identifier
ON diagnostics_tasks (group_url_identifier);
-- ------------------------------------------------------------
-- Ownership : omika_diagnostics_owner (comme 003/008/010).
-- ------------------------------------------------------------
ALTER TABLE diagnostics_tasks OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostics_tasks_id_seq OWNER TO omika_diagnostics_owner;
-- ------------------------------------------------------------
-- Défense PUBLIC (ceinture + bretelles, idempotent).
-- ------------------------------------------------------------
REVOKE ALL ON TABLE diagnostics_tasks FROM PUBLIC;
-- ------------------------------------------------------------
-- Web (Omika.Diagnostics.Web) : SELECT + INSERT + UPDATE
-- colonne-ciblé (moindre privilège) + USAGE séquence.
-- - INSERT : création d'une tâche (id BIGSERIAL).
-- - UPDATE (title, description, cause, status, assignee_username,
-- updated_at_utc, resolved_at_utc) : édition de la tâche.
-- - group_url_identifier et created_by_username ne sont JAMAIS mis à jour.
-- - AUCUN DELETE (aucune suppression physique en V1).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_tasks TO omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostics_tasks TO omika_diagnostics_web;
GRANT UPDATE (title, description, cause, status, assignee_username, updated_at_utc, resolved_at_utc)
ON TABLE diagnostics_tasks TO omika_diagnostics_web;
GRANT USAGE ON SEQUENCE diagnostics_tasks_id_seq TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- API (omika_diagnostics_api) : AUCUN droit sur diagnostics_tasks.
-- Worker (omika_diagnostics_worker) : AUCUN droit sur diagnostics_tasks.
-- Aucun GRANT accordé ; les rôles n'ont aucun privilège par défaut.
-- (Aucun REVOKE nécessaire : aucun privilège n'a été accordé.)
-- ------------------------------------------------------------
-- ============================================================
-- END 012. Ordre serveur : 001 -> 002 -> ... -> 011 -> 012.
-- ============================================================
@@ -0,0 +1,24 @@
-- ============================================================
-- Omika Diagnostic Center -- 013_diagnostics_tasks_delete.sql
-- T25 : autorise la suppression d'une tâche de traitement (V2).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 013_...),
-- AFTER 012 (table diagnostics_tasks + rôles Web).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Jusqu'en V1, aucune suppression physique n'était prévue : 012 n'accordait AUCUN
-- DELETE. La V2 ajoute un bouton « Supprimer » sur /Taches : on accorde DELETE au
-- seul rôle Web (moindre privilège). L'API (omika_diagnostics_api) et le Worker
-- (omika_diagnostics_worker) n'ont TOUJOURS aucun droit de suppression.
-- ============================================================
GRANT DELETE ON TABLE diagnostics_tasks TO omika_diagnostics_web;
-- ============================================================
-- END 013. Ordre serveur : 001 -> 002 -> ... -> 012 -> 013.
-- ============================================================
@@ -0,0 +1,40 @@
-- ============================================================
-- Omika Diagnostic Center -- 014_diagnostics_telemetry_retention.sql
-- Rétention télémétrie : privilège de purge de telemetry_metric_hourly.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 014_...),
-- AFTER 003 (rôles) et 001 (table telemetry_metric_hourly).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Context : la boucle DiagnosticTelemetryRetentionWorker purge les agrégats horaires
-- plus anciens que Diagnostics__TelemetryRetentionDays (défaut 180 j = 6 mois), par
-- LOTS BORNÉS :
--
-- DELETE FROM telemetry_metric_hourly
-- WHERE id IN (
-- SELECT id FROM telemetry_metric_hourly
-- WHERE bucket_utc < @cutoff
-- ORDER BY bucket_utc ASC, id ASC
-- LIMIT @batchSize
-- )
--
-- Cette requête requiert :
-- * DELETE sur telemetry_metric_hourly (cible du DELETE) ;
-- * SELECT sur telemetry_metric_hourly (la sous-requête lit la table).
--
-- Jusqu'ici le rôle worker (omika_diagnostics_worker) n'avait AUCUN droit sur cette
-- table (least privilege, cf. 003). On accorde le MINIMUM pour la rétention :
-- SELECT + DELETE uniquement. NO UPDATE, NO TRUNCATE, NO DDL, NO INSERT (l'upsert de
-- télémétrie n'a toujours pas de caller, la table reste non alimentée côté worker).
-- ============================================================
GRANT SELECT, DELETE ON TABLE telemetry_metric_hourly TO omika_diagnostics_worker;
-- ============================================================
-- END 014. Ordre serveur : 001 -> 002 -> 003 -> 004 -> ... -> 013 -> 014.
-- ============================================================
@@ -0,0 +1,51 @@
-- ============================================================
-- Omika Diagnostic Center -- 015_diagnostics_retention_untreated.sql
-- Rétention : ne pas purger les erreurs non traitées (privilège SELECT colonne-ciblé).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 015_...),
-- AFTER 004 (SELECT/DELETE sur diagnostic_event) et 012 (table diagnostics_tasks).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Règle de conservation : la purge DiagnosticRetentionWorker supprime un événement
-- ancien selon DEUX cas :
-- 1) événement NON-erreur (trace/debug/info/warning, ou level NULL) → toujours purgé ;
-- 2) événement d'ERREUR (error/critical) → purgé SEULEMENT si son groupe possède une
-- tâche (diagnostics_tasks) en statut TERMINAL (Resolved/Rejected). Une erreur non
-- traitée (aucune tâche, ou Todo/InProgress) est conservée même au-delà du cutoff.
--
-- Le DELETE joint donc diagnostics_tasks sur l'identifiant URL-safe du groupe (recalculé
-- en SQL, sans colonne fingerprint) :
--
-- DELETE FROM diagnostic_event
-- WHERE id IN (
-- SELECT id FROM diagnostic_event
-- WHERE timestamp_utc < @cutoff
-- AND (
-- level IS NULL OR LOWER(level) NOT IN ('error', 'critical') OR
-- (LOWER(level) IN ('error', 'critical') AND <group_url_identifier recalculé> IN (
-- SELECT group_url_identifier FROM diagnostics_tasks
-- WHERE LOWER(status) IN ('resolved','rejected')
-- ))
-- )
-- ORDER BY timestamp_utc ASC, id ASC
-- LIMIT @batchSize
-- )
--
-- Jusqu'ici le rôle worker (omika_diagnostics_worker) n'avait AUCUN droit sur
-- diagnostics_tasks (012). On accorde le MINIMUM pour le rapprochement : SELECT
-- UNIQUEMENT sur group_url_identifier (la clé) et status (le statut terminal).
-- NO SELECT sur title/description/cause (notes de support, potentiellement sensibles),
-- NO SELECT sur assignee/created_by (usernames), NO UPDATE/INSERT/DELETE.
-- ============================================================
GRANT SELECT (group_url_identifier, status)
ON TABLE diagnostics_tasks TO omika_diagnostics_worker;
-- ============================================================
-- END 015. Ordre serveur : 001 -> 002 -> ... -> 013 -> 014 -> 015.
-- ============================================================
@@ -0,0 +1,31 @@
-- ============================================================
-- Omika Diagnostic Center -- 016_diagnostics_event_delete.sql
-- Erreurs : autorise la suppression d'un groupe d'erreurs complet (V2).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 016_...),
-- AFTER 003 (rôles owner/worker/web).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Jusqu'ici le rôle Web (omika_diagnostics_web) n'avait que SELECT sur
-- diagnostic_event (migration 003) : il lisait les événements mais ne pouvait jamais en
-- supprimer. La V2 ajoute, sur la page /Erreurs, un bouton « Supprimer le groupe » qui
-- supprime TOUTES les occurrences d'un groupe d'erreurs (signature normalisée :
-- event_code + error_code/exception_type/component). Le DELETE est exécuté par le Web,
-- donc on accorde le privilège DELETE au seul rôle Web (moindre privilège).
--
-- Le Worker (omika_diagnostics_worker) conserve déjà SELECT + DELETE (migration 004,
-- rétention par lots bornés) ; son périmètre est inchangé. L'API n'a AUCUN droit de
-- suppression. Le DELETE du Web est borné par la signature du groupe (jamais un DELETE
-- massif non filtré) et restreint aux niveaux d'erreur réels par le code applicatif.
-- ============================================================
GRANT DELETE ON TABLE diagnostic_event TO omika_diagnostics_web;
-- ============================================================
-- END 016. Ordre serveur : 001 -> 002 -> ... -> 015 -> 016.
-- ============================================================