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