Premiere commit - Omika
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
)
|
||||
@@ -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 }
|
||||
@@ -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."
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user