Ajout de la partie application des changements sur le disque
Ajout de l'installation de Windows Correction du chargement des images iso. Correction de la définition de la taille de WinRE.
This commit is contained in:
1 parent
1c67cb2630
commit
8d5014b4aa
13 files changed
+2524
-1445
No files matched your search
@@ -10,3 +10,7 @@ Paths=%DEPLOY_VOLUME%
|
||||
; Laisser vide pour générer un mot de passe aléatoire à chaque lancement.
|
||||
; Pour les tests, renseigner un mot de passe ici. Il sera encodé dans ultravnc.ini par createpassword64.exe.
|
||||
Password=
|
||||
|
||||
[Partitioning]
|
||||
; Taille de la partition WinRE en Mo ; défaut : 1024 (1 Go), minimum : 990.
|
||||
; WinRESizeMB=1024
|
||||
+179
-1318
File diff suppressed because it is too large.
Load diff
+4
-101
@@ -1,101 +1,4 @@
|
||||
|
||||
# HardwareInfo.ps1
|
||||
# DeployCenter - Inventaire matériel
|
||||
# Compatible Windows PowerShell 5.1 / WinPE
|
||||
|
||||
function Get-HardwareInfo {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$info = [ordered]@{
|
||||
Manufacturer = "Inconnu"
|
||||
Model = "Inconnu"
|
||||
SerialNumber = "Inconnu"
|
||||
Firmware = "Inconnu"
|
||||
Architecture = $env:PROCESSOR_ARCHITECTURE
|
||||
WinPE = $false
|
||||
Disks = @()
|
||||
Errors = @()
|
||||
}
|
||||
|
||||
# Fabricant et modele du PC
|
||||
try {
|
||||
$computer = Get-CimInstance `
|
||||
-ClassName Win32_ComputerSystem `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($computer.Manufacturer) {
|
||||
$info.Manufacturer = $computer.Manufacturer.Trim()
|
||||
}
|
||||
|
||||
if ($computer.Model) {
|
||||
$info.Model = $computer.Model.Trim()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Modele du PC : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Numero de serie
|
||||
try {
|
||||
$bios = Get-CimInstance `
|
||||
-ClassName Win32_BIOS `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($bios.SerialNumber) {
|
||||
$info.SerialNumber = $bios.SerialNumber.Trim()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Numero de serie : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Detection du firmware
|
||||
try {
|
||||
$firmwareKey = Get-ItemProperty `
|
||||
-Path "HKLM:\SYSTEM\CurrentControlSet\Control" `
|
||||
-Name PEFirmwareType `
|
||||
-ErrorAction Stop
|
||||
|
||||
switch ([int]$firmwareKey.PEFirmwareType) {
|
||||
1 { $info.Firmware = "BIOS (Legacy)" }
|
||||
2 { $info.Firmware = "UEFI" }
|
||||
default { $info.Firmware = "Indetermine" }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Mode firmware indisponible"
|
||||
}
|
||||
|
||||
# Detection de WinPE
|
||||
$info.WinPE = Test-Path `
|
||||
"HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT"
|
||||
|
||||
# Inventaire des disques physiques
|
||||
try {
|
||||
$disks = @(Get-Disk -ErrorAction Stop)
|
||||
|
||||
foreach ($disk in $disks) {
|
||||
$operationalStatus = @($disk.OperationalStatus | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
$status = if ($operationalStatus.Count -gt 0) { $operationalStatus -join ', ' } else { 'Inconnu' }
|
||||
|
||||
$info.Disks += [PSCustomObject]@{
|
||||
Index = [int]$disk.Number
|
||||
Model = [string]$disk.FriendlyName
|
||||
SizeBytes = [long]$disk.Size
|
||||
SizeGB = [math]::Round(
|
||||
([double]$disk.Size / 1GB), 1
|
||||
)
|
||||
BusType = [string]$disk.BusType
|
||||
Status = $status
|
||||
HealthStatus = [string]$disk.HealthStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Inventaire des disques : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Retourner un objet exploitable par les autres modules
|
||||
[PSCustomObject]$info
|
||||
}
|
||||
# Point d'entrée historique conservé pour les appels directs à HardwareInfo.ps1.
|
||||
$hardwareInventoryModule = Join-Path $PSScriptRoot 'Modules\HardwareInventory.psm1'
|
||||
Import-Module $hardwareInventoryModule -Force
|
||||
Get-HardwareInfo
|
||||
@@ -0,0 +1,42 @@
|
||||
# DeployCenter - Lecture des préférences utilisateur
|
||||
# Lecture seule : ce module ne modifie jamais DeployCenter.ini.
|
||||
|
||||
function Read-DeployCenterConfig {
|
||||
[CmdletBinding()]
|
||||
param([Parameter(Mandatory=$true)][string]$ConfigPath)
|
||||
|
||||
$config = @{
|
||||
Theme = 'Light'
|
||||
SourcePaths = '%DEPLOY_VOLUME%'
|
||||
VncPassword = ''
|
||||
WinRESizeMB = 1024
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) { return $config }
|
||||
|
||||
$section = ''
|
||||
foreach ($line in (Get-Content -LiteralPath $ConfigPath -ErrorAction SilentlyContinue)) {
|
||||
$trimmed = $line.Trim()
|
||||
if (-not $trimmed -or $trimmed.StartsWith(';') -or $trimmed.StartsWith('#')) { continue }
|
||||
if ($trimmed -match '^\[(.+)\]$') { $section = $matches[1].Trim(); continue }
|
||||
if ($trimmed -notmatch '^([^=]+)=(.*)$') { continue }
|
||||
|
||||
$key = $matches[1].Trim()
|
||||
$value = $matches[2].Trim()
|
||||
if ($section -ieq 'General' -and $key -ieq 'Theme' -and $value -match '^[A-Za-z0-9_-]+$') {
|
||||
$config.Theme = $value
|
||||
} elseif ($section -ieq 'Sources' -and $key -ieq 'Paths') {
|
||||
$config.SourcePaths = $value
|
||||
} elseif ($section -ieq 'UltraVNC' -and $key -ieq 'Password') {
|
||||
$config.VncPassword = $value
|
||||
} elseif ($section -ieq 'Partitioning' -and $key -ieq 'WinRESizeMB') {
|
||||
$sizeMB = 0
|
||||
if (-not [int]::TryParse($value, [ref]$sizeMB) -or $sizeMB -lt 990) {
|
||||
throw "Le paramètre [Partitioning] WinRESizeMB doit être un entier supérieur ou égal à 990. Valeur reçue : '$value'."
|
||||
}
|
||||
$config.WinRESizeMB = $sizeMB
|
||||
}
|
||||
}
|
||||
return $config
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Read-DeployCenterConfig
|
||||
@@ -0,0 +1,577 @@
|
||||
# DeployCenter - Partitionnement et installation d'une edition Windows
|
||||
# Compatible Windows PowerShell 5.1 / WinPE.
|
||||
|
||||
function Write-DeployCenterDeploymentLog {
|
||||
param([AllowNull()][scriptblock]$LogAction, [string]$Message, [string]$Level = 'INFO')
|
||||
if ($LogAction) { & $LogAction $Message $Level }
|
||||
}
|
||||
|
||||
function ConvertTo-DeployCenterNativeArgument {
|
||||
param([AllowNull()][string]$Value)
|
||||
|
||||
if ($null -eq $Value) { $Value = '' }
|
||||
if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') { return $Value }
|
||||
|
||||
$builder = New-Object System.Text.StringBuilder
|
||||
[void]$builder.Append('"')
|
||||
$backslashCount = 0
|
||||
foreach ($character in $Value.ToCharArray()) {
|
||||
if ($character -eq [char]92) {
|
||||
$backslashCount++
|
||||
continue
|
||||
}
|
||||
if ($character -eq [char]34) {
|
||||
if ($backslashCount -gt 0) { [void]$builder.Append(('\' * (2 * $backslashCount))) }
|
||||
[void]$builder.Append('\"')
|
||||
$backslashCount = 0
|
||||
continue
|
||||
}
|
||||
if ($backslashCount -gt 0) { [void]$builder.Append(('\' * $backslashCount)) }
|
||||
[void]$builder.Append($character)
|
||||
$backslashCount = 0
|
||||
}
|
||||
if ($backslashCount -gt 0) { [void]$builder.Append(('\' * (2 * $backslashCount))) }
|
||||
[void]$builder.Append('"')
|
||||
return $builder.ToString()
|
||||
}
|
||||
|
||||
function New-DeployCenterDismSourceAccess {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ImagePath,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
|
||||
$lease = [pscustomobject]@{ ImagePath = $ImagePath; DriveLetter = $null; VolumeRoot = $null }
|
||||
if (-not $ImagePath.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase)) { return $lease }
|
||||
$volumeEnd = $ImagePath.IndexOf('}\', [StringComparison]::OrdinalIgnoreCase)
|
||||
if ($volumeEnd -lt 0) { throw "Chemin de volume GUID invalide pour l’image : $ImagePath" }
|
||||
$volumeRoot = $ImagePath.Substring(0, $volumeEnd + 2)
|
||||
$relativePath = $ImagePath.Substring($volumeEnd + 2)
|
||||
|
||||
$usedLetters = @{}
|
||||
foreach ($volume in @(Get-Volume -ErrorAction SilentlyContinue)) {
|
||||
if ($volume.DriveLetter) { $usedLetters[([string]$volume.DriveLetter).ToUpperInvariant()] = $true }
|
||||
}
|
||||
foreach ($drive in @(Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue)) {
|
||||
$usedLetters[([string]$drive.Name).ToUpperInvariant()] = $true
|
||||
}
|
||||
$letter = $null
|
||||
foreach ($candidate in @('Z','Y','X','W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E')) {
|
||||
if ($candidate -notin @('C','D') -and -not $usedLetters.ContainsKey($candidate)) {
|
||||
$letter = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $letter) { throw 'Aucune lettre temporaire disponible pour accéder à la source Windows.' }
|
||||
|
||||
$mountVolPath = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolPath -PathType Leaf)) { $mountVolPath = 'mountvol.exe' }
|
||||
$letterPath = ("{0}:\{1}" -f $letter, $relativePath)
|
||||
$mountError = $null
|
||||
try {
|
||||
Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $letter), $volumeRoot) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
} catch {
|
||||
$mountError = $_.Exception
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $letterPath -PathType Leaf)) {
|
||||
try {
|
||||
Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $letter), '/D') `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
} catch { }
|
||||
throw "La source Windows n’est pas accessible après l’affectation temporaire de $letter`:."
|
||||
}
|
||||
if ($mountError) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("mountvol n’a pas fourni de code de sortie exploitable, mais la source est accessible via {0}:. L’affectation est confirmée." -f $letter) `
|
||||
-Level 'WARNING'
|
||||
}
|
||||
$lease.ImagePath = $letterPath
|
||||
$lease.DriveLetter = $letter
|
||||
$lease.VolumeRoot = $volumeRoot
|
||||
return $lease
|
||||
}
|
||||
|
||||
function Remove-DeployCenterDismSourceAccess {
|
||||
param(
|
||||
[AllowNull()]$Lease,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
if ($null -eq $Lease -or -not $Lease.DriveLetter) { return }
|
||||
$mountVolPath = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolPath -PathType Leaf)) { $mountVolPath = 'mountvol.exe' }
|
||||
$releaseError = $null
|
||||
try {
|
||||
Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $Lease.DriveLetter), '/D') `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
} catch {
|
||||
$releaseError = $_.Exception
|
||||
}
|
||||
if (Test-Path -LiteralPath ([string]$Lease.ImagePath) -PathType Leaf) {
|
||||
$details = if ($releaseError) { $releaseError.Message } else { 'Le fichier source reste accessible.' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message "Impossible de confirmer la libération de la lettre temporaire $($Lease.DriveLetter): $details" -Level 'WARNING'
|
||||
} elseif ($releaseError) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("mountvol n’a pas fourni de code de sortie exploitable, mais la source n’est plus accessible via {0}:. La libération est confirmée." -f $Lease.DriveLetter) `
|
||||
-Level 'WARNING'
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DeployCenterNativeProgressPercent {
|
||||
param([Parameter(Mandatory=$true)][string]$OutputPath)
|
||||
|
||||
$stream = $null
|
||||
$reader = $null
|
||||
try {
|
||||
$stream = [System.IO.File]::Open($OutputPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
||||
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::Default, $true)
|
||||
$content = $reader.ReadToEnd()
|
||||
} catch {
|
||||
return $null
|
||||
} finally {
|
||||
if ($reader) { $reader.Dispose() }
|
||||
elseif ($stream) { $stream.Dispose() }
|
||||
}
|
||||
|
||||
$progressMatches = [regex]::Matches($content, '(?<![\d.])(?<percent>\d{1,3}(?:\.\d+)?)\s*%')
|
||||
if ($progressMatches.Count -eq 0) { return $null }
|
||||
$value = [double]$progressMatches[$progressMatches.Count - 1].Groups['percent'].Value
|
||||
return [int][math]::Max(0, [math]::Min(100, [math]::Floor($value)))
|
||||
}
|
||||
|
||||
function Invoke-DeployCenterNativeCommand {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$FilePath,
|
||||
[Parameter(Mandatory=$true)][string[]]$Arguments,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null,
|
||||
[AllowNull()][scriptblock]$ProgressAction = $null
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $TemporaryRoot -PathType Container)) {
|
||||
New-Item -Path $TemporaryRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
$id = [guid]::NewGuid().ToString('N')
|
||||
$stdoutPath = Join-Path $TemporaryRoot ("native_{0}.out" -f $id)
|
||||
$stderrPath = Join-Path $TemporaryRoot ("native_{0}.err" -f $id)
|
||||
$dismLogPath = Join-Path $TemporaryRoot ("dism_{0}.log" -f $id)
|
||||
$isDism = [IO.Path]::GetFileName($FilePath) -ieq 'dism.exe'
|
||||
if ($isDism) {
|
||||
$Arguments += "/LogPath:$dismLogPath"
|
||||
}
|
||||
$argumentLine = ($Arguments | ForEach-Object {
|
||||
ConvertTo-DeployCenterNativeArgument -Value ([string]$_)
|
||||
}) -join ' '
|
||||
|
||||
try {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Commande native : {0} {1}" -f $FilePath, $argumentLine)
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $argumentLine `
|
||||
-PassThru -WindowStyle Hidden -RedirectStandardOutput $stdoutPath `
|
||||
-RedirectStandardError $stderrPath -ErrorAction Stop
|
||||
while (-not $process.WaitForExit(250)) {
|
||||
if ($ProgressAction) {
|
||||
[System.Windows.Forms.Application]::DoEvents()
|
||||
if ($isDism -and (Test-Path -LiteralPath $stdoutPath -PathType Leaf)) {
|
||||
$nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
|
||||
if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent }
|
||||
}
|
||||
}
|
||||
}
|
||||
$process.WaitForExit()
|
||||
$process.Refresh()
|
||||
$exitCode = $process.ExitCode
|
||||
$nativeOutputLines = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
if (Test-Path -LiteralPath $outputPath -PathType Leaf) {
|
||||
foreach ($line in [System.IO.File]::ReadAllLines($outputPath)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($line)) {
|
||||
[void]$nativeOutputLines.Add([string]$line)
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($ProgressAction -and $isDism) {
|
||||
$nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
|
||||
if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent }
|
||||
}
|
||||
if ($null -eq $exitCode -and $isDism -and @($nativeOutputLines | Where-Object { $_ -match '(?i)completed successfully|a r.ussi' }).Count -gt 0) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message 'DISM a confirmé la réussite de l’opération, mais WinPE n’a pas fourni de code de sortie exploitable.' `
|
||||
-Level 'WARNING'
|
||||
return
|
||||
}
|
||||
if ($null -eq $exitCode -and [IO.Path]::GetFileName($FilePath) -ieq 'diskpart.exe') {
|
||||
# Dans certains WinPE, Start-Process ne renseigne pas ExitCode même
|
||||
# lorsque DiskPart a terminé. L’appelant vérifie ensuite l’état réel
|
||||
# des partitions et des lettres avant de poursuivre.
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message "DiskPart n'a pas fourni de code de sortie exploitable ; le résultat sera confirmé par vérification du disque." `
|
||||
-Level 'WARNING'
|
||||
return
|
||||
}
|
||||
if ($null -eq $exitCode -or [int]$exitCode -ne 0) {
|
||||
$diagnostic = @()
|
||||
if ($isDism -and (Test-Path -LiteralPath $dismLogPath -PathType Leaf)) {
|
||||
$diagnostic = @(Get-Content -LiteralPath $dismLogPath -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object { [string]$_ })
|
||||
foreach ($line in $diagnostic) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("DISM : {0}" -f $line) -Level 'ERROR'
|
||||
}
|
||||
}
|
||||
throw "La commande '$FilePath' s'est terminée avec le code '$exitCode'. $(($diagnostic -join ' | '))"
|
||||
}
|
||||
} finally {
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
if (Test-Path -LiteralPath $outputPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $outputPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
if (Test-Path -LiteralPath $dismLogPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $dismLogPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DeployCenterTemporaryDriveLetters {
|
||||
param([Parameter(Mandatory=$true)][int]$DiskNumber, [Parameter(Mandatory=$true)]$Profile)
|
||||
|
||||
$targetLetters = @()
|
||||
try {
|
||||
$targetLetters = @(Get-Partition -DiskNumber $DiskNumber -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.DriveLetter } | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() })
|
||||
} catch { }
|
||||
$occupied = @{}
|
||||
foreach ($volume in @(Get-Volume -ErrorAction SilentlyContinue)) {
|
||||
if (-not $volume.DriveLetter) { continue }
|
||||
$letter = ([string]$volume.DriveLetter).ToUpperInvariant()
|
||||
if ($targetLetters -contains $letter) { continue }
|
||||
$occupied[$letter] = $true
|
||||
}
|
||||
|
||||
$lettersByRole = @{}
|
||||
foreach ($partition in $Profile.Partitions) {
|
||||
if ([string]$partition.Role -eq 'Reserved') { continue }
|
||||
$preferred = [string]$partition.DriveLetter
|
||||
if ([string]$partition.Role -eq 'System') { $preferred = 'S' }
|
||||
if ([string]$partition.Role -eq 'Recovery') { $preferred = 'R' }
|
||||
if ([string]::IsNullOrWhiteSpace($preferred)) { $preferred = 'W' }
|
||||
$preferred = $preferred.ToUpperInvariant()
|
||||
if (-not $occupied.ContainsKey($preferred)) {
|
||||
$selected = $preferred
|
||||
} else {
|
||||
$selected = $null
|
||||
foreach ($candidate in @('W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E','D','C')) {
|
||||
if (-not $occupied.ContainsKey($candidate) -and $candidate -notin $lettersByRole.Values) {
|
||||
$selected = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $selected) { throw "Aucune lettre de lecteur libre pour la partition '$($partition.Name)'." }
|
||||
}
|
||||
if ($selected -in $lettersByRole.Values) { throw "La lettre $selected`: est affectée à plusieurs partitions du profil." }
|
||||
$lettersByRole[[string]$partition.Role] = $selected
|
||||
$occupied[$selected] = $true
|
||||
}
|
||||
return $lettersByRole
|
||||
}
|
||||
|
||||
function New-DeployCenterPartitionPlan {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]$DiskInfo,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||||
[AllowNull()]$SizingState
|
||||
)
|
||||
|
||||
if ([string]$Profile.PartitionStyle -ne 'GPT' -or [string]$Profile.Firmware -ne 'UEFI') {
|
||||
throw "Le profil '$($Profile.Name)' n’est pas pris en charge par le moteur actuel (UEFI/GPT requis)."
|
||||
}
|
||||
$totalMB = [long][math]::Floor(([double]$DiskInfo.SizeBytes / 1MB))
|
||||
$availableMB = $totalMB - 2
|
||||
$usedMB = 0L
|
||||
$plan = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($partition in $Profile.Partitions) {
|
||||
$policy = [string]$partition.Size.Policy
|
||||
$sizeMB = $null
|
||||
switch ($policy) {
|
||||
'FixedMB' { $sizeMB = [long]$partition.Size.Value }
|
||||
'ConfiguredMB' { $sizeMB = [long]$partition.Size.Value }
|
||||
'UserInputGB' {
|
||||
if ($null -eq $SizingState -or [string]$SizingState.ProfileId -ne [string]$Profile.Id) {
|
||||
throw "Le profil '$($Profile.Name)' nécessite une taille utilisateur, qui n’a pas été validée."
|
||||
}
|
||||
$role = [string]$partition.Role
|
||||
if (-not $SizingState.Inputs.Contains($role)) { throw "La taille de la partition '$role' est absente." }
|
||||
$sizeMB = [long][math]::Round(([double]$SizingState.Inputs[$role] * 1024), 0)
|
||||
if ($sizeMB -lt ([long]$partition.Size.MinimumGB * 1024)) { throw "La partition '$role' est sous sa taille minimale." }
|
||||
}
|
||||
{ $_ -in @('Remaining','RemainderAfterInputs') } {
|
||||
$sizeMB = $availableMB - $usedMB
|
||||
if ($sizeMB -lt 1) { throw "Le disque ne dispose plus d’espace pour la partition '$($partition.Name)'." }
|
||||
if ($policy -eq 'RemainderAfterInputs' -and $sizeMB -lt ([long]$partition.Size.MinimumGB * 1024)) {
|
||||
throw "L’espace restant pour '$($partition.Name)' est inférieur au minimum du profil."
|
||||
}
|
||||
}
|
||||
default { throw "Politique de taille non prise en charge lors du partitionnement : $policy" }
|
||||
}
|
||||
if ($null -ne $sizeMB) { $usedMB += [long]$sizeMB }
|
||||
$plan.Add([pscustomobject]@{ Definition = $partition; SizeMB = $sizeMB })
|
||||
}
|
||||
if ($usedMB -gt $availableMB) { throw "La taille demandée dépasse la capacité du disque sélectionné." }
|
||||
return $plan.ToArray()
|
||||
}
|
||||
|
||||
function ConvertTo-DeployCenterUnattendXml {
|
||||
param([Parameter(Mandatory=$true)][string]$HostName, [Parameter(Mandatory=$true)][string]$Architecture)
|
||||
$processorArchitecture = switch -Regex ($Architecture.ToLowerInvariant()) {
|
||||
'arm64' { 'arm64'; break }
|
||||
'x86|32' { 'x86'; break }
|
||||
default { 'amd64' }
|
||||
}
|
||||
return @"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||
<settings pass="specialize">
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="$processorArchitecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<ComputerName>$HostName</ComputerName>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
"@
|
||||
}
|
||||
|
||||
function Invoke-DeployCenterWindowsDeployment {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]$DiskInfo,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||||
[AllowNull()]$SizingState,
|
||||
[Parameter(Mandatory=$true)]$Edition,
|
||||
[Parameter(Mandatory=$true)][string]$HostName,
|
||||
[Parameter(Mandatory=$true)][string]$Firmware,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null,
|
||||
[AllowNull()][scriptblock]$ProgressAction = $null
|
||||
)
|
||||
|
||||
if ($HostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') {
|
||||
throw "Le nom d'hôte doit respecter les règles NetBIOS et ne pas dépasser 15 caractères."
|
||||
}
|
||||
if ([string]$Firmware -ne [string]$Profile.Firmware) {
|
||||
throw "Le profil '$($Profile.Name)' exige le firmware $($Profile.Firmware), détecté : $Firmware."
|
||||
}
|
||||
foreach ($requiredRole in @('System','Reserved','Recovery','Windows')) {
|
||||
$roleCount = @($Profile.Partitions | Where-Object { [string]$_.Role -eq $requiredRole }).Count
|
||||
if ($roleCount -ne 1) { throw "Le profil doit définir exactement une partition de rôle '$requiredRole'. Le disque n’a pas été modifié." }
|
||||
}
|
||||
$recoveryDefinitions = @($Profile.Partitions | Where-Object { [string]$_.Role -eq 'Recovery' })
|
||||
if ($recoveryDefinitions.Count -ne 1 -or [string]$recoveryDefinitions[0].Size.Policy -ne 'ConfiguredMB') {
|
||||
throw "Le profil doit définir une taille configurée pour la partition Recovery. Le disque n’a pas été modifié."
|
||||
}
|
||||
$winrePartitionSizeMB = [long]$recoveryDefinitions[0].Size.Value
|
||||
if ($winrePartitionSizeMB -lt 990) {
|
||||
throw "La partition WinRE doit être configurée à au moins 990 Mo. Le disque n’a pas été modifié."
|
||||
}
|
||||
if (-not $Edition.ImagePath -or -not (Test-Path -LiteralPath ([string]$Edition.ImagePath) -PathType Leaf)) {
|
||||
throw "Le chemin de l’image Windows sélectionnée est introuvable."
|
||||
}
|
||||
if (-not $Edition.Index -or [int]$Edition.Index -lt 1) { throw "L’index de l’édition Windows est invalide." }
|
||||
|
||||
$disk = Get-Disk -Number ([int]$DiskInfo.Index) -ErrorAction Stop
|
||||
if ($disk.IsBoot -or $disk.IsSystem) { throw 'Le disque sélectionné héberge le système WinPE et ne peut pas être effacé.' }
|
||||
if ($disk.IsOffline) { [void](Set-Disk -Number $disk.Number -IsOffline $false -ErrorAction Stop) }
|
||||
if ($disk.IsReadOnly) { [void](Set-Disk -Number $disk.Number -IsReadOnly $false -ErrorAction Stop) }
|
||||
|
||||
if (-not (Test-Path -LiteralPath $TemporaryRoot -PathType Container)) {
|
||||
New-Item -Path $TemporaryRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
$sourceLease = New-DeployCenterDismSourceAccess -ImagePath ([string]$Edition.ImagePath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
try {
|
||||
$deploymentImagePath = [string]$sourceLease.ImagePath
|
||||
$partitionSizingDisk = [pscustomobject]@{ Index = [int]$disk.Number; SizeBytes = [long]$disk.Size }
|
||||
$partitionPlan = @(New-DeployCenterPartitionPlan -DiskInfo $partitionSizingDisk -Profile $Profile -SizingState $SizingState)
|
||||
$driveLetters = Get-DeployCenterTemporaryDriveLetters -DiskNumber ([int]$disk.Number) -Profile $Profile
|
||||
|
||||
$dismPath = Join-Path $env:WINDIR 'System32\dism.exe'
|
||||
if (-not (Test-Path -LiteralPath $dismPath -PathType Leaf)) { $dismPath = 'dism.exe' }
|
||||
$bcdbootPath = Join-Path $env:WINDIR 'System32\bcdboot.exe'
|
||||
if (-not (Test-Path -LiteralPath $bcdbootPath -PathType Leaf)) { $bcdbootPath = 'bcdboot.exe' }
|
||||
$reagentcPath = Join-Path $env:WINDIR 'System32\reagentc.exe'
|
||||
if (-not (Test-Path -LiteralPath $reagentcPath -PathType Leaf)) { $reagentcPath = 'reagentc.exe' }
|
||||
|
||||
$scriptLines = New-Object System.Collections.Generic.List[string]
|
||||
$scriptLines.Add("select disk $($disk.Number)")
|
||||
$scriptLines.Add('clean')
|
||||
$scriptLines.Add('convert gpt')
|
||||
foreach ($item in $partitionPlan) {
|
||||
$definition = $item.Definition
|
||||
$role = [string]$definition.Role
|
||||
switch ($role) {
|
||||
'System' { $scriptLines.Add("create partition efi size=$($item.SizeMB)") }
|
||||
'Reserved' { $scriptLines.Add("create partition msr size=$($item.SizeMB)") }
|
||||
default {
|
||||
if ($null -eq $item.SizeMB -or [long]$item.SizeMB -ge ([long][math]::Floor(([double]$disk.Size / 1MB)) - 2)) {
|
||||
$scriptLines.Add('create partition primary')
|
||||
} else {
|
||||
$scriptLines.Add("create partition primary size=$($item.SizeMB)")
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($role -eq 'Reserved') { continue }
|
||||
$label = [string]$definition.Name
|
||||
if ($label -notmatch '^[A-Za-z0-9_-]{1,32}$') { throw "Nom de partition invalide dans le profil : $label" }
|
||||
if ([string]$definition.FileSystem) {
|
||||
$scriptLines.Add("format fs=$($definition.FileSystem) quick label=$label")
|
||||
}
|
||||
if ($definition.GptTypeId) { $scriptLines.Add("set id=$($definition.GptTypeId)") }
|
||||
if ($definition.GptAttributes) { $scriptLines.Add("gpt attributes=$($definition.GptAttributes)") }
|
||||
if ($driveLetters.ContainsKey($role)) { $scriptLines.Add("assign letter=$($driveLetters[$role])") }
|
||||
}
|
||||
|
||||
$diskpartPath = Join-Path $TemporaryRoot ("partition_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
|
||||
$unattendPath = Join-Path $TemporaryRoot ("unattend_{0}.xml" -f ([guid]::NewGuid().ToString('N')))
|
||||
$partitionCreated = $false
|
||||
$lettersReleased = $false
|
||||
try {
|
||||
[System.IO.File]::WriteAllLines($diskpartPath, $scriptLines.ToArray(), [System.Text.Encoding]::ASCII)
|
||||
if ($ProgressAction) { & $ProgressAction 'Effacement et partitionnement du disque…' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Partitionnement du disque {0} avec le profil '{1}' ; partition WinRE configurée : {2} Mo." -f $disk.Number, $Profile.Name, $winrePartitionSizeMB)
|
||||
$partitionCreated = $true
|
||||
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $diskpartPath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
$createdPartitions = @(Get-Partition -DiskNumber ([int]$disk.Number) -ErrorAction Stop)
|
||||
if ($createdPartitions.Count -ne $partitionPlan.Count) {
|
||||
throw "DiskPart a créé $($createdPartitions.Count) partition(s), alors que le profil en prévoit $($partitionPlan.Count). L’installation est interrompue."
|
||||
}
|
||||
$orderedCreatedPartitions = @($createdPartitions | Sort-Object -Property PartitionNumber)
|
||||
$partitionsByLetter = @{}
|
||||
for ($index = 0; $index -lt $partitionPlan.Count; $index++) {
|
||||
$role = [string]$partitionPlan[$index].Definition.Role
|
||||
if ($role -eq 'Reserved') { continue }
|
||||
$letter = [string]$driveLetters[$role]
|
||||
$driveRoot = '{0}:\' -f $letter
|
||||
if (-not (Test-Path -LiteralPath $driveRoot -PathType Container)) {
|
||||
throw "La lettre temporaire $letter`: n’est pas accessible après le partitionnement. L’installation est interrompue."
|
||||
}
|
||||
# WinPE peut ne pas exposer les partitions EFI/Recovery dans les
|
||||
# requêtes Get-Partition par lettre. Leur ordre sur le disque suit
|
||||
# directement celui des définitions exécutées par DiskPart.
|
||||
$partitionsByLetter[$letter] = $orderedCreatedPartitions[$index]
|
||||
}
|
||||
|
||||
$windowsLetter = [string]$driveLetters['Windows']
|
||||
$windowsPath = "${windowsLetter}:\"
|
||||
$windowsRoot = Join-Path $windowsPath 'Windows'
|
||||
$recoveryLetter = [string]$driveLetters['Recovery']
|
||||
$recoveryPath = "${recoveryLetter}:\Recovery\WindowsRE"
|
||||
$efiLetter = [string]$driveLetters['System']
|
||||
if (-not $windowsLetter -or -not $recoveryLetter -or -not $efiLetter) {
|
||||
throw "Le profil doit définir les rôles System, Windows et Recovery pour permettre l’installation UEFI avec WinRE."
|
||||
}
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction "Application de Windows sur $windowsLetter`:…" }
|
||||
Invoke-DeployCenterNativeCommand -FilePath $dismPath -Arguments @(
|
||||
'/Apply-Image', "/ImageFile:$deploymentImagePath", "/Index:$([int]$Edition.Index)", "/ApplyDir:$windowsPath", '/CheckIntegrity'
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Vérification de la capacité de la partition WinRE…' }
|
||||
$sourceWinre = Join-Path $windowsRoot 'System32\Recovery\winre.wim'
|
||||
if (-not (Test-Path -LiteralPath $sourceWinre -PathType Leaf)) {
|
||||
throw "winre.wim est absent de l’image Windows appliquée ; impossible de configurer WinRE."
|
||||
}
|
||||
$winreFile = Get-Item -LiteralPath $sourceWinre -ErrorAction Stop
|
||||
$recoveryPartition = $partitionsByLetter[$recoveryLetter]
|
||||
$recoveryDriveInfo = New-Object System.IO.DriveInfo -ArgumentList "${recoveryLetter}:\"
|
||||
if (-not $recoveryDriveInfo.IsReady) {
|
||||
throw "Le volume de récupération $recoveryLetter`: n’est pas prêt pour vérifier l’espace WinRE."
|
||||
}
|
||||
$minimumPartitionBytes = [long](990 * 1MB)
|
||||
$minimumFreeBytes = [long]($winreFile.Length + (250 * 1MB))
|
||||
$recoveryFreeBytes = [long]$recoveryDriveInfo.AvailableFreeSpace
|
||||
$expectedFreeBytes = $recoveryFreeBytes - [long]$winreFile.Length
|
||||
if ([long]$recoveryPartition.Size -lt $minimumPartitionBytes -or
|
||||
$recoveryFreeBytes -lt $minimumFreeBytes) {
|
||||
$winreSizeForLog = [math]::Ceiling(([double]$winreFile.Length / 1MB))
|
||||
$availableAfterCopyMB = [math]::Floor(([double]$expectedFreeBytes / 1MB))
|
||||
$message = "La partition WinRE configurée à $winrePartitionSizeMB Mo ne respecte pas les prérequis Microsoft : taille de winre.wim $winreSizeForLog Mo, espace libre requis après copie 250 Mo, estimé $availableAfterCopyMB Mo. L’installation est interrompue avant l’activation de WinRE."
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message $message -Level 'ERROR'
|
||||
throw $message
|
||||
}
|
||||
$winreSizeForLog = [math]::Ceiling(([double]$winreFile.Length / 1MB))
|
||||
$availableAfterCopyMB = [math]::Floor(([double]$expectedFreeBytes / 1MB))
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Vérification WinRE réussie : partition {0} Mo, winre.wim {1} Mo, espace restant estimé après copie {2} Mo." -f $winrePartitionSizeMB, $winreSizeForLog, $availableAfterCopyMB) -Level 'SUCCESS'
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction "Configuration du nom d’hôte…" }
|
||||
$editionArchitecture = if ($Edition.Edition -and $Edition.Edition.Architecture) { [string]$Edition.Edition.Architecture } else { [string]$Edition.Architecture }
|
||||
$unattendXml = ConvertTo-DeployCenterUnattendXml -HostName $HostName -Architecture $editionArchitecture
|
||||
[System.IO.File]::WriteAllText($unattendPath, $unattendXml, (New-Object System.Text.UTF8Encoding($false)))
|
||||
Invoke-DeployCenterNativeCommand -FilePath $dismPath -Arguments @(
|
||||
"/Image:$windowsPath", "/Apply-Unattend:$unattendPath"
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Configuration du démarrage UEFI…' }
|
||||
Invoke-DeployCenterNativeCommand -FilePath $bcdbootPath -Arguments @(
|
||||
("{0}:\Windows" -f $windowsLetter), '/s', ("{0}:" -f $efiLetter), '/f', 'UEFI'
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Configuration de la partition de récupération WinRE…' }
|
||||
New-Item -Path $recoveryPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
Copy-Item -LiteralPath $sourceWinre -Destination (Join-Path $recoveryPath 'winre.wim') -Force -ErrorAction Stop
|
||||
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
|
||||
'/SetReImage', '/Path', $recoveryPath, '/Target', $windowsRoot
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
|
||||
'/Enable', '/Target', $windowsRoot
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Finalisation des partitions…' }
|
||||
$removeLines = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($letter in @($driveLetters.Values | Select-Object -Unique)) {
|
||||
$removeLines.Add("select volume $letter")
|
||||
$removeLines.Add("remove letter=$letter")
|
||||
}
|
||||
$removePath = Join-Path $TemporaryRoot ("cleanup_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
|
||||
try {
|
||||
[System.IO.File]::WriteAllLines($removePath, $removeLines.ToArray(), [System.Text.Encoding]::ASCII)
|
||||
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $removePath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
$remainingLetters = @(
|
||||
Get-Partition -DiskNumber ([int]$disk.Number) -ErrorAction Stop |
|
||||
Where-Object { $_.DriveLetter -and ([string]$_.DriveLetter).ToUpperInvariant() -in @($driveLetters.Values | ForEach-Object { ([string]$_).ToUpperInvariant() }) }
|
||||
)
|
||||
if ($remainingLetters.Count -gt 0) {
|
||||
$remaining = @($remainingLetters | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() } | Select-Object -Unique) -join ', '
|
||||
throw "DiskPart n’a pas libéré toutes les lettres temporaires du disque : $remaining."
|
||||
}
|
||||
$lettersReleased = $true
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $removePath -PathType Leaf) { Remove-Item -LiteralPath $removePath -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Déploiement terminé : Windows {0}, disque {1}, hôte {2}." -f $Edition.Name, $disk.Number, $HostName) -Level 'SUCCESS'
|
||||
} finally {
|
||||
if ($partitionCreated -and -not $lettersReleased) {
|
||||
# En cas d’échec après création, libérer les lettres temporaires même si
|
||||
# l’image n’a pas pu être entièrement déployée.
|
||||
foreach ($letter in @($driveLetters.Values | Select-Object -Unique)) {
|
||||
try {
|
||||
$cleanup = Join-Path $TemporaryRoot ("release_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
|
||||
[System.IO.File]::WriteAllLines($cleanup, @("select volume $letter", "remove letter=$letter"), [System.Text.Encoding]::ASCII)
|
||||
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $cleanup) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
Remove-Item -LiteralPath $cleanup -Force -ErrorAction SilentlyContinue
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
foreach ($temporaryFile in @($diskpartPath, $unattendPath)) {
|
||||
if (Test-Path -LiteralPath $temporaryFile -PathType Leaf) { Remove-Item -LiteralPath $temporaryFile -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Remove-DeployCenterDismSourceAccess -Lease $sourceLease -TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-DeployCenterWindowsDeployment
|
||||
@@ -0,0 +1,151 @@
|
||||
|
||||
|
||||
function Get-HardwareInfo {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$info = [ordered]@{
|
||||
Manufacturer = "Inconnu"
|
||||
Model = "Inconnu"
|
||||
SerialNumber = "Inconnu"
|
||||
Firmware = "Inconnu"
|
||||
Architecture = $env:PROCESSOR_ARCHITECTURE
|
||||
WinPE = $false
|
||||
Disks = @()
|
||||
Errors = @()
|
||||
}
|
||||
|
||||
# Fabricant et modele du PC
|
||||
try {
|
||||
$computer = Get-CimInstance `
|
||||
-ClassName Win32_ComputerSystem `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($computer.Manufacturer) {
|
||||
$info.Manufacturer = $computer.Manufacturer.Trim()
|
||||
}
|
||||
|
||||
if ($computer.Model) {
|
||||
$info.Model = $computer.Model.Trim()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Modele du PC : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Numero de serie
|
||||
try {
|
||||
$bios = Get-CimInstance `
|
||||
-ClassName Win32_BIOS `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($bios.SerialNumber) {
|
||||
$info.SerialNumber = $bios.SerialNumber.Trim()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Numero de serie : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Detection du firmware
|
||||
try {
|
||||
$firmwareKey = Get-ItemProperty `
|
||||
-Path "HKLM:\SYSTEM\CurrentControlSet\Control" `
|
||||
-Name PEFirmwareType `
|
||||
-ErrorAction Stop
|
||||
|
||||
switch ([int]$firmwareKey.PEFirmwareType) {
|
||||
1 { $info.Firmware = "BIOS (Legacy)" }
|
||||
2 { $info.Firmware = "UEFI" }
|
||||
default { $info.Firmware = "Indetermine" }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Mode firmware indisponible"
|
||||
}
|
||||
|
||||
# Detection de WinPE
|
||||
$info.WinPE = Test-Path `
|
||||
"HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT"
|
||||
|
||||
# Inventaire des disques physiques
|
||||
try {
|
||||
$disks = @(Get-Disk -ErrorAction Stop)
|
||||
|
||||
foreach ($disk in $disks) {
|
||||
$operationalStatus = @($disk.OperationalStatus | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
$status = if ($operationalStatus.Count -gt 0) { $operationalStatus -join ', ' } else { 'Inconnu' }
|
||||
|
||||
$info.Disks += [PSCustomObject]@{
|
||||
Index = [int]$disk.Number
|
||||
Model = [string]$disk.FriendlyName
|
||||
SizeBytes = [long]$disk.Size
|
||||
SizeGB = [math]::Round(
|
||||
([double]$disk.Size / 1GB), 1
|
||||
)
|
||||
BusType = [string]$disk.BusType
|
||||
Status = $status
|
||||
HealthStatus = [string]$disk.HealthStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Inventaire des disques : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Retourner un objet exploitable par les autres modules
|
||||
[PSCustomObject]$info
|
||||
}
|
||||
|
||||
function Write-DeployInventory {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$InventoryDir,
|
||||
[Parameter(Mandatory=$true)][string]$InventoryFile,
|
||||
[AllowNull()]$HardwareInfo,
|
||||
[AllowNull()][string]$DeploymentIPv4,
|
||||
[AllowNull()][string]$VncPassword,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $InventoryDir -PathType Container)) {
|
||||
New-Item -Path $InventoryDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$inventory = [ordered]@{
|
||||
GeneratedAt = (Get-Date).ToString('o')
|
||||
DeploymentIPv4 = $DeploymentIPv4
|
||||
VncPassword = $VncPassword
|
||||
Hardware = $HardwareInfo
|
||||
}
|
||||
$json = $inventory | ConvertTo-Json -Depth 8
|
||||
Set-Content -LiteralPath $InventoryFile -Value $json -Encoding UTF8
|
||||
|
||||
# Nettoyer les anciens exports séparés après l'écriture de l'inventaire consolidé.
|
||||
$legacyInventoryFiles = @(
|
||||
(Join-Path $env:TEMP 'HardwareInfo.json'),
|
||||
(Join-Path $InventoryDir 'hardware.json')
|
||||
)
|
||||
foreach ($legacyFile in $legacyInventoryFiles) {
|
||||
if (Test-Path -LiteralPath $legacyFile -PathType Leaf) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $legacyFile -Force -ErrorAction Stop
|
||||
} catch {
|
||||
if ($LogAction) { & $LogAction "Impossible de supprimer l'ancien export d'inventaire '$legacyFile' : $($_.Exception.Message)" 'WARNING' }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if ($LogAction) { & $LogAction "Impossible d'écrire l'inventaire temporaire : $($_.Exception.Message)" 'WARNING' }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SelectedDeployDiskInfo {
|
||||
param(
|
||||
[AllowNull()]$HardwareInfo,
|
||||
[Parameter(Mandatory=$true)][int]$DiskIndex
|
||||
)
|
||||
if ($null -eq $HardwareInfo) { return $null }
|
||||
$matches = @($HardwareInfo.Disks | Where-Object { [int]$_.Index -eq $DiskIndex } | Select-Object -First 1)
|
||||
if ($matches.Count -gt 0) { return $matches[0] }
|
||||
return $null
|
||||
}
|
||||
Export-ModuleMember -Function Get-HardwareInfo, Write-DeployInventory, Get-SelectedDeployDiskInfo
|
||||
@@ -56,6 +56,12 @@ function Import-DeployCenterPartitionProfile {
|
||||
if ([int]$partition.Size.FreeSpaceMB -lt 250) { throw "La marge libre WinRE du profil '$ProfileName' doit être d'au moins 250 Mo." }
|
||||
if ([int]$partition.Size.PreviewMB -le 0) { throw "Le profil '$ProfileName' doit définir une réserve PreviewMB pour estimer la capacité disponible." }
|
||||
}
|
||||
'ConfiguredMB' {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$partition.Size.ConfigKey) -or
|
||||
[int]$partition.Size.Value -lt 990 -or [int]$partition.Size.PreviewMB -lt 990) {
|
||||
throw "La taille configurée de la partition '$($partition.Name)' du profil '$ProfileName' doit définir ConfigKey et une valeur d’au moins 990 Mo."
|
||||
}
|
||||
}
|
||||
'UserInputGB' {
|
||||
if ([int]$partition.Size.MinimumGB -le 0 -or [int]$partition.Size.DefaultPercent -le 0 -or [int]$partition.Size.DefaultPercent -ge 100) {
|
||||
throw "La saisie de taille utilisateur du profil '$ProfileName' doit définir MinimumGB et un DefaultPercent entre 1 et 99."
|
||||
@@ -85,7 +91,7 @@ function Get-DeployCenterPartitionSizingCapacity {
|
||||
$reservedMB = 0.0
|
||||
foreach ($partition in $Profile.Partitions) {
|
||||
switch ([string]$partition.Size.Policy) {
|
||||
'FixedMB' { $reservedMB += [double]$partition.Size.Value }
|
||||
{ $_ -in @('FixedMB','ConfiguredMB') } { $reservedMB += [double]$partition.Size.Value }
|
||||
default {
|
||||
if ($partition.Size.ContainsKey('PreviewMB')) { $reservedMB += [double]$partition.Size.PreviewMB }
|
||||
}
|
||||
@@ -101,4 +107,21 @@ function Get-DeployCenterPartitionSizingCapacity {
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-DeployCenterPartitionProfileNames, Import-DeployCenterPartitionProfile, Get-DeployCenterPartitionSizingCapacity
|
||||
function New-DeployCenterPartitionSizingState {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||||
[Parameter(Mandatory=$true)]$DiskInfo,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Selection
|
||||
)
|
||||
return [pscustomobject]@{
|
||||
ProfileId = [string]$Profile.Id
|
||||
DiskIndex = [int]$DiskInfo.Index
|
||||
Inputs = $Selection.Inputs
|
||||
RemainderRole = [string]$Selection.RemainderRole
|
||||
RemainderGB = [double]$Selection.RemainderGB
|
||||
AvailableGB = [double]$Selection.AvailableGB
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-DeployCenterPartitionProfileNames, Import-DeployCenterPartitionProfile, Get-DeployCenterPartitionSizingCapacity, New-DeployCenterPartitionSizingState
|
||||
+1067
-2
File diff suppressed because it is too large.
Load diff
+13
-13
@@ -9,22 +9,22 @@ function Start-DeployCenterVnc {
|
||||
$sourceIni = Join-Path $sourceVnc 'ultravnc.ini'
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($script:DeployIPv4)) {
|
||||
Write-Log 'Aucune adresse IPv4 de déploiement détectée.' 'WARNING'
|
||||
& $script:SourceLogAction 'Aucune adresse IPv4 de déploiement détectée.' 'WARNING'
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $sourceExe -PathType Leaf)) {
|
||||
Write-Log "UltraVNC non démarré : exécutable absent ($sourceExe)." 'WARNING'
|
||||
Write-DeployInventory
|
||||
& $script:SourceLogAction "UltraVNC non démarré : exécutable absent ($sourceExe)." 'WARNING'
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $passwordTool -PathType Leaf)) {
|
||||
Write-Log "UltraVNC non démarré : createpassword64.exe absent ($passwordTool)." 'WARNING'
|
||||
Write-DeployInventory
|
||||
& $script:SourceLogAction "UltraVNC non démarré : createpassword64.exe absent ($passwordTool)." 'WARNING'
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $sourceIni -PathType Leaf)) {
|
||||
Write-Log "UltraVNC non démarré : fichier absent ($sourceIni)." 'WARNING'
|
||||
Write-DeployInventory
|
||||
& $script:SourceLogAction "UltraVNC non démarré : fichier absent ($sourceIni)." 'WARNING'
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,13 +48,13 @@ function Start-DeployCenterVnc {
|
||||
foreach ($outputLine in $wpeutilOutput) {
|
||||
$outputText = [string]$outputLine
|
||||
if (-not [string]::IsNullOrWhiteSpace($outputText)) {
|
||||
Write-Log "wpeutil DisableFirewall : $outputText" 'INFO'
|
||||
& $script:SourceLogAction "wpeutil DisableFirewall : $outputText" 'INFO'
|
||||
}
|
||||
}
|
||||
if ($wpeutilExitCode -ne 0) {
|
||||
throw "wpeutil DisableFirewall a renvoyé le code $wpeutilExitCode."
|
||||
}
|
||||
Write-Log "Pare-feu WinPE désactivé à la demande de -Vnc (code wpeutil : $wpeutilExitCode)." 'WARNING'
|
||||
& $script:SourceLogAction "Pare-feu WinPE désactivé à la demande de -Vnc (code wpeutil : $wpeutilExitCode)." 'WARNING'
|
||||
|
||||
# Force le mode portable : l'INI actif est celui placé à côté de winvnc.exe.
|
||||
$vncStage = 'activation du mode portable UltraVNC'
|
||||
@@ -97,13 +97,13 @@ function Start-DeployCenterVnc {
|
||||
throw "winvnc.exe s'est arrêté immédiatement (code $($script:VncProcess.ExitCode))."
|
||||
}
|
||||
$script:VncStarted = $true
|
||||
Write-Log "UltraVNC démarré. IP : $($script:DeployIPv4) ; mot de passe $passwordSource." 'SUCCESS'
|
||||
& $script:SourceLogAction "UltraVNC démarré. IP : $($script:DeployIPv4) ; mot de passe $passwordSource." 'SUCCESS'
|
||||
} catch {
|
||||
Write-Log "Échec du démarrage UltraVNC à l'étape '$vncStage' : $($_.Exception.Message)" 'ERROR'
|
||||
& $script:SourceLogAction "Échec du démarrage UltraVNC à l'étape '$vncStage' : $($_.Exception.Message)" 'ERROR'
|
||||
$script:VncStarted = $false
|
||||
$script:VncPassword = $null
|
||||
}
|
||||
Write-DeployInventory
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
}
|
||||
|
||||
function Stop-DeployCenterVnc {
|
||||
@@ -120,7 +120,7 @@ function Stop-DeployCenterVnc {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Arrêt UltraVNC : $($_.Exception.Message)" 'WARNING'
|
||||
& $script:SourceLogAction "Arrêt UltraVNC : $($_.Exception.Message)" 'WARNING'
|
||||
}
|
||||
# Le processus winvnc peut conserver brièvement un handle sur son dossier.
|
||||
# Le dossier VNC est propre à ce processus PowerShell (uvnc_<PID>) : on ne
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
# DeployCenter - Recherche et lecture des sources Windows
|
||||
# Compatible Windows PowerShell 5.1
|
||||
|
||||
function Find-WindowsSources {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$RootPath,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
|
||||
$results = New-Object System.Collections.Generic.List[object]
|
||||
$roots = @($RootPath -split ';' | ForEach-Object {
|
||||
$candidate = $_.Trim().Trim('"')
|
||||
if ($candidate -notmatch '^[A-Za-z]:\\$') { $candidate = $candidate.TrimEnd('\') }
|
||||
$candidate
|
||||
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
|
||||
if ($roots.Count -eq 0) {
|
||||
throw 'Indique au moins un chemin local et/ou un partage SMB, separes par un point-virgule.'
|
||||
}
|
||||
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
|
||||
if ($LogAction) { & $LogAction "Source inaccessible, ignoree : $root" 'WARNING' }
|
||||
continue
|
||||
}
|
||||
|
||||
if ($LogAction) { & $LogAction "Recherche des sources Windows dans : $root" 'INFO' }
|
||||
# Parcours en lecture seule. Les erreurs d'acces aux sous-dossiers sont ignorees.
|
||||
# Afficher les ISO et les fichiers WIM/ESD autonomes.
|
||||
# Exception : masquer boot.wim uniquement lorsqu'il fait partie d'un
|
||||
# support Windows extrait dont le même dossier Sources contient install.wim/esd.
|
||||
$allFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue)
|
||||
$files = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($file in $allFiles) {
|
||||
if ($file.Extension -ieq '.iso') {
|
||||
[void]$files.Add($file)
|
||||
continue
|
||||
}
|
||||
|
||||
if ($file.Extension -ine '.wim' -and $file.Extension -ine '.esd') {
|
||||
continue
|
||||
}
|
||||
|
||||
$isBootWimInExtractedMedia = $false
|
||||
if ($file.Name -ieq 'boot.wim' -and $file.Directory.Name -ieq 'Sources') {
|
||||
$installWim = Join-Path $file.Directory.FullName 'install.wim'
|
||||
$installEsd = Join-Path $file.Directory.FullName 'install.esd'
|
||||
if ((Test-Path -LiteralPath $installWim -PathType Leaf) -or
|
||||
(Test-Path -LiteralPath $installEsd -PathType Leaf)) {
|
||||
$isBootWimInExtractedMedia = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $isBootWimInExtractedMedia) {
|
||||
[void]$files.Add($file)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($file in $files) {
|
||||
$kind = switch ($file.Extension.ToLowerInvariant()) {
|
||||
'.iso' { 'Image ISO' }
|
||||
'.wim' { 'Image WIM Windows' }
|
||||
'.esd' { 'Image ESD Windows' }
|
||||
}
|
||||
$detail = if ($file.Name -ieq 'install.wim' -or $file.Name -ieq 'install.esd') {
|
||||
"Image d’installation dans un support extrait"
|
||||
} elseif ($file.Extension -ieq '.iso') {
|
||||
"Support d’installation ISO"
|
||||
} else {
|
||||
"Image WIM/ESD autonome"
|
||||
}
|
||||
$results.Add([pscustomobject]@{
|
||||
Type = $kind; Nom = $file.Name; Chemin = $file.FullName
|
||||
Taille = ('{0:N2} Go' -f ($file.Length / 1GB)); Detail = $detail
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (-not ($roots | Where-Object { Test-Path -LiteralPath $_ -PathType Container })) {
|
||||
throw "Aucun des chemins n'est accessible : $($roots -join '; ')`nVerifie que le lecteur local et le partage SMB sont accessibles."
|
||||
}
|
||||
if ($LogAction) { & $LogAction ("Detection terminee : {0} element(s) trouve(s)." -f $results.Count) 'SUCCESS' }
|
||||
return @($results | Sort-Object Type, Nom, Chemin -Unique)
|
||||
}
|
||||
|
||||
function Get-WindowsImageEditions {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ImagePath,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) {
|
||||
throw "Fichier image introuvable : $ImagePath"
|
||||
}
|
||||
|
||||
$normalized = @()
|
||||
$getWindowsImage = Get-Command Get-WindowsImage -ErrorAction SilentlyContinue
|
||||
if ($getWindowsImage) {
|
||||
try {
|
||||
$images = @(Get-WindowsImage -ImagePath $ImagePath -ErrorAction Stop)
|
||||
foreach ($image in $images) {
|
||||
$indexValue = $null
|
||||
foreach ($propertyName in @('ImageIndex', 'Index')) {
|
||||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||||
$indexValue = [int]$image.$propertyName
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($null -eq $indexValue) { continue }
|
||||
|
||||
$nameValue = $null
|
||||
foreach ($propertyName in @('ImageName', 'Name', 'ImageDescription')) {
|
||||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||||
$nameValue = [string]$image.$propertyName
|
||||
break
|
||||
}
|
||||
}
|
||||
$archValue = $null
|
||||
foreach ($propertyName in @('Architecture', 'ImageArchitecture')) {
|
||||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||||
$archValue = $image.$propertyName
|
||||
break
|
||||
}
|
||||
}
|
||||
$versionValue = $null
|
||||
foreach ($propertyName in @('Version', 'ImageVersion')) {
|
||||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||||
$versionValue = [string]$image.$propertyName
|
||||
break
|
||||
}
|
||||
}
|
||||
$normalized += [pscustomobject]@{
|
||||
Index = $indexValue
|
||||
Name = $nameValue
|
||||
Description = if ($image.PSObject.Properties['ImageDescription']) { $image.ImageDescription } else { $null }
|
||||
Architecture = $archValue
|
||||
Version = $versionValue
|
||||
Edition = if ($image.PSObject.Properties['EditionId']) { $image.EditionId } else { $null }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if ($LogAction) { & $LogAction "Get-WindowsImage indisponible pour $ImagePath : $($_.Exception.Message)" 'WARNING' }
|
||||
}
|
||||
}
|
||||
|
||||
$dism = Join-Path $env:WINDIR 'System32\dism.exe'
|
||||
if (-not (Test-Path -LiteralPath $dism)) { $dism = 'dism.exe' }
|
||||
|
||||
# Si le module PowerShell n'a pas renvoyé les index/noms, les récupérer avec DISM.
|
||||
if ($normalized.Count -eq 0) {
|
||||
$output = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "DISM n'a pas pu lire l'image : $ImagePath`n$($output -join "`n")"
|
||||
}
|
||||
|
||||
$current = $null
|
||||
foreach ($line in $output) {
|
||||
$lineText = [string]$line
|
||||
if ($lineText -match '^\s*Index\s*:\s*(\d+)\s*$') {
|
||||
if ($null -ne $current) { $normalized += [pscustomobject]$current }
|
||||
$current = @{ Index = [int]$Matches[1]; Name = ''; Description = ''; Architecture = $null; Version = $null; Edition = $null }
|
||||
} elseif ($null -ne $current -and $lineText -match '^\s*Name\s*:\s*(.*?)\s*$') {
|
||||
$current.Name = $Matches[1]
|
||||
} elseif ($null -ne $current -and $lineText -match '^\s*Description\s*:\s*(.*?)\s*$') {
|
||||
$current.Description = $Matches[1]
|
||||
}
|
||||
}
|
||||
if ($null -ne $current) { $normalized += [pscustomobject]$current }
|
||||
}
|
||||
|
||||
if ($normalized.Count -eq 0) {
|
||||
throw "DISM n’a retourné aucune édition exploitable pour : $ImagePath"
|
||||
}
|
||||
|
||||
# Interroger DISM pour chaque index même si Get-WindowsImage a fourni les noms :
|
||||
# selon la version du module, les propriétés Architecture/Version peuvent être absentes.
|
||||
foreach ($edition in $normalized) {
|
||||
if ($edition.Architecture -and $edition.Version) { continue }
|
||||
|
||||
$detailOutput = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" "/Index:$($edition.Index)" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
if ($LogAction) { & $LogAction "DISM n'a pas pu lire les metadonnees de l'index $($edition.Index) : $($detailOutput -join ' ')" 'WARNING' }
|
||||
continue
|
||||
}
|
||||
|
||||
$insideImageDetails = $false
|
||||
$imageVersion = $null
|
||||
$servicePackBuild = $null
|
||||
foreach ($detailLine in $detailOutput) {
|
||||
$detailText = [string]$detailLine
|
||||
if ($detailText -match '^\s*Details for image\s*:') {
|
||||
$insideImageDetails = $true
|
||||
continue
|
||||
}
|
||||
# Ignorer la version de DISM affichée au début de sa sortie.
|
||||
if (-not $insideImageDetails) { continue }
|
||||
|
||||
if (-not $edition.Architecture -and $detailText -match '^\s*Architecture\s*:\s*(.*?)\s*$') {
|
||||
$edition.Architecture = $Matches[1]
|
||||
} elseif ($detailText -match '^\s*Version\s*:\s*(.*?)\s*$') {
|
||||
$imageVersion = $Matches[1]
|
||||
} elseif ($detailText -match '^\s*ServicePack Build\s*:\s*(.*?)\s*$') {
|
||||
$servicePackBuild = $Matches[1]
|
||||
} elseif (-not $edition.Name -and $detailText -match '^\s*Name\s*:\s*(.*?)\s*$') {
|
||||
$edition.Name = $Matches[1]
|
||||
} elseif (-not $edition.Edition -and $detailText -match '^\s*Edition\s*:\s*(.*?)\s*$') {
|
||||
$edition.Edition = $Matches[1]
|
||||
}
|
||||
}
|
||||
if ($imageVersion) {
|
||||
$edition.Version = if ($servicePackBuild -and $servicePackBuild -ne '0') {
|
||||
"$imageVersion.$servicePackBuild"
|
||||
} else {
|
||||
$imageVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Version globale de l'image : ne l'afficher que si les métadonnées des index
|
||||
# donnent une seule version cohérente. Ne pas répéter cette valeur sur chaque ligne.
|
||||
$versionValues = @($normalized | ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$_.Version)) { [string]$_.Version }
|
||||
} | Select-Object -Unique)
|
||||
$globalVersion = $null
|
||||
if ($versionValues.Count -eq 1) {
|
||||
$globalVersion = $versionValues[0]
|
||||
}
|
||||
|
||||
foreach ($edition in $normalized) {
|
||||
if ($edition.Architecture -is [int] -or [string]$edition.Architecture -match '^\d+$') {
|
||||
switch ([string]$edition.Architecture) {
|
||||
'0' { $edition.Architecture = 'x86' }
|
||||
'5' { $edition.Architecture = 'ARM' }
|
||||
'9' { $edition.Architecture = 'x64' }
|
||||
'12' { $edition.Architecture = 'ARM64' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Editions = @($normalized)
|
||||
GlobalVersion = $globalVersion
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-InstallImagePath {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SelectedPath,
|
||||
[AllowNull()][object]$ExistingImage = $null
|
||||
)
|
||||
|
||||
if ([IO.Path]::GetExtension($SelectedPath) -in @('.wim', '.esd')) {
|
||||
return [pscustomobject]@{ SourcePath = $SelectedPath; ImagePath = $SelectedPath; MountedImage = $null }
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $SelectedPath -PathType Container) {
|
||||
foreach ($relative in @('sources\install.wim', 'sources\install.esd', 'Sources\install.wim', 'Sources\install.esd')) {
|
||||
$candidate = Join-Path $SelectedPath $relative
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
||||
return [pscustomobject]@{ SourcePath = $SelectedPath; ImagePath = $candidate; MountedImage = $null }
|
||||
}
|
||||
}
|
||||
throw "Aucun fichier Sources\install.wim ou Sources\install.esd dans : $SelectedPath"
|
||||
}
|
||||
|
||||
if ([IO.Path]::GetExtension($SelectedPath) -ieq '.iso') {
|
||||
if ($ExistingImage -and $ExistingImage.SourcePath -and
|
||||
[string]::Equals([IO.Path]::GetFullPath([string]$ExistingImage.SourcePath), [IO.Path]::GetFullPath($SelectedPath), [StringComparison]::OrdinalIgnoreCase) -and
|
||||
$ExistingImage.MountedImage -and $ExistingImage.ImagePath) {
|
||||
return $ExistingImage
|
||||
}
|
||||
if (-not (Get-Command Mount-DiskImage -ErrorAction SilentlyContinue)) {
|
||||
throw "Le montage ISO n'est pas disponible dans cet environnement (commande Mount-DiskImage absente). Pour analyser cette source, utilise le dossier extrait ou un fichier install.wim/esd."
|
||||
}
|
||||
# Ne pas laisser Windows attribuer une lettre arbitraire (souvent D:) à l'ISO.
|
||||
$diskImage = Mount-DiskImage -ImagePath $SelectedPath -Access ReadOnly -NoDriveLetter -PassThru -ErrorAction Stop
|
||||
$mountInfo = [pscustomobject]@{
|
||||
SourcePath = $SelectedPath
|
||||
ImagePath = $null
|
||||
MountedImage = $diskImage
|
||||
MountedPartition = $null
|
||||
DriveLetter = $null
|
||||
}
|
||||
try {
|
||||
# Partir des volumes explicitement associés à cette image évite de
|
||||
# confondre le lecteur CDROM virtuel et un disque physique dans WinPE.
|
||||
$volumes = @(Get-Volume -DiskImage $diskImage -ErrorAction Stop)
|
||||
if ($volumes.Count -eq 0) { throw 'Aucun volume ISO expose par Windows.' }
|
||||
$usedLetters = @(
|
||||
Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() }
|
||||
Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue | ForEach-Object { ([string]$_.Name).ToUpperInvariant() }
|
||||
)
|
||||
$availableLetter = $null
|
||||
foreach ($candidateLetter in @('Z','Y','X','W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E')) {
|
||||
if ($candidateLetter -notin @('C','D') -and $candidateLetter -notin $usedLetters) {
|
||||
$availableLetter = $candidateLetter
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $availableLetter) { throw 'Aucune lettre de lecteur temporaire disponible pour le montage ISO.' }
|
||||
|
||||
$checkedPaths = @()
|
||||
foreach ($volume in $volumes) {
|
||||
$volumeRoot = [string]$volume.Path
|
||||
if (-not $volumeRoot.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase) -and $volume.DriveLetter) {
|
||||
$mountVolCommand = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolCommand -PathType Leaf)) { $mountVolCommand = 'mountvol.exe' }
|
||||
$volumeMountPath = '{0}:\' -f [string]$volume.DriveLetter
|
||||
$volumeNameOutput = & $mountVolCommand $volumeMountPath '/L' 2>&1
|
||||
$volumeGuidPath = @($volumeNameOutput | ForEach-Object { [string]$_ } | Where-Object { $_.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase) }) | Select-Object -First 1
|
||||
if ($volumeGuidPath) { $volumeRoot = [string]$volumeGuidPath }
|
||||
}
|
||||
if ($volumeRoot -and $volumeRoot.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
if (-not $volumeRoot.EndsWith('\')) { $volumeRoot += '\' }
|
||||
foreach ($relative in @('sources\install.wim', 'sources\install.esd')) {
|
||||
$candidate = $volumeRoot + $relative
|
||||
$checkedPaths += $candidate
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
||||
if ([string]$volume.DriveLetter -eq 'D') {
|
||||
$mountVolCommand = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolCommand -PathType Leaf)) { $mountVolCommand = 'mountvol.exe' }
|
||||
$removeOutput = & $mountVolCommand 'D:\' '/D' 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "Impossible de libérer D: pour le volume ISO : $($removeOutput -join ' ')" }
|
||||
}
|
||||
$mountInfo.ImagePath = $candidate
|
||||
return $mountInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$partition = Get-Partition -Volume $volume -ErrorAction Stop
|
||||
if ($null -eq $partition) { continue }
|
||||
$existingDrivePaths = @($partition.AccessPaths | Where-Object { [string]$_ -match '^[A-Za-z]:\\$' })
|
||||
$reusableDrivePath = $existingDrivePaths | Where-Object { [string]$_ -notmatch '^[CDcd]:\\$' } | Select-Object -First 1
|
||||
if ($reusableDrivePath) {
|
||||
$driveAccessPath = [string]$reusableDrivePath
|
||||
$driveLetter = $driveAccessPath.Substring(0, 1).ToUpperInvariant()
|
||||
} else {
|
||||
foreach ($existingDrivePath in $existingDrivePaths) {
|
||||
Remove-PartitionAccessPath -InputObject $partition -AccessPath ([string]$existingDrivePath) -ErrorAction Stop | Out-Null
|
||||
}
|
||||
$driveLetter = $availableLetter
|
||||
$driveAccessPath = '{0}:\' -f $driveLetter
|
||||
Add-PartitionAccessPath -InputObject $partition -AccessPath $driveAccessPath -ErrorAction Stop
|
||||
}
|
||||
$mountInfo.MountedPartition = $partition
|
||||
$mountInfo.DriveLetter = $driveLetter
|
||||
foreach ($relative in @('sources\install.wim', 'sources\install.esd')) {
|
||||
$candidate = "${driveLetter}:\$relative"
|
||||
$checkedPaths += $candidate
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
||||
$mountInfo.ImagePath = $candidate
|
||||
return $mountInfo
|
||||
}
|
||||
}
|
||||
if ($driveLetter -eq $availableLetter) {
|
||||
Remove-PartitionAccessPath -InputObject $partition -AccessPath $driveAccessPath -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
$mountInfo.MountedPartition = $null
|
||||
$mountInfo.DriveLetter = $null
|
||||
}
|
||||
throw "Aucun fichier Sources\install.wim ou Sources\install.esd trouvé dans l'ISO. Chemins vérifiés : $($checkedPaths -join ', ')."
|
||||
} catch {
|
||||
try { [void](Dismount-DeployCenterInstallImage -Image $mountInfo -ErrorAction SilentlyContinue) } catch { }
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
throw "Type de source non pris en charge pour l'analyse : $SelectedPath"
|
||||
}
|
||||
|
||||
function Dismount-DeployCenterInstallImage {
|
||||
param([AllowNull()][object]$Image)
|
||||
|
||||
if ($null -eq $Image -or $null -eq $Image.MountedImage) { return }
|
||||
try {
|
||||
if ($Image.MountedPartition -and $Image.DriveLetter) {
|
||||
$driveAccessPath = '{0}:\' -f [string]$Image.DriveLetter
|
||||
Remove-PartitionAccessPath -InputObject $Image.MountedPartition -AccessPath $driveAccessPath -ErrorAction Stop | Out-Null
|
||||
}
|
||||
} finally {
|
||||
Dismount-DiskImage -InputObject $Image.MountedImage -ErrorAction Stop | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Find-WindowsSources, Get-WindowsImageEditions, Resolve-InstallImagePath, Dismount-DeployCenterInstallImage
|
||||
@@ -26,7 +26,7 @@
|
||||
FileSystem = 'NTFS'
|
||||
GptTypeId = 'DE94BBA4-06D1-4D40-A16A-BFD50179D6AC'
|
||||
GptAttributes = '0x8000000000000001'
|
||||
Size = @{ Policy = 'ImageSizePlusFreeSpace'; FreeSpaceMB = 250; PreviewMB = 1024 }
|
||||
Size = @{ Policy = 'ConfiguredMB'; ConfigKey = 'WinRESizeMB'; Value = 1024; PreviewMB = 1024 }
|
||||
}
|
||||
@{
|
||||
Name = 'Windows'
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
FileSystem = 'NTFS'
|
||||
GptTypeId = 'DE94BBA4-06D1-4D40-A16A-BFD50179D6AC'
|
||||
GptAttributes = '0x8000000000000001'
|
||||
Size = @{ Policy = 'ImageSizePlusFreeSpace'; FreeSpaceMB = 250; PreviewMB = 1024 }
|
||||
Size = @{ Policy = 'ConfiguredMB'; ConfigKey = 'WinRESizeMB'; Value = 1024; PreviewMB = 1024 }
|
||||
}
|
||||
@{
|
||||
Name = 'Windows'
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# DeployCenter — première version de la réécriture
|
||||
# DeployCenter — première version de la réécriture
|
||||
|
||||
## Contenu
|
||||
|
||||
- `DeployCenter.ps1` : point d'entrée et logique applicative existante.
|
||||
- `Modules/ThemeManager.psm1` : découverte, chargement et validation des thèmes PSD1, avec prise en charge des rôles de boutons.
|
||||
- `Modules/UI.psm1` : style centralisé des boutons (normal, survol, appui et désactivé).
|
||||
- `Modules/UI.psm1` : contrôles et fenêtres d’interface, construits selon le thème actif.
|
||||
- `Modules/Deployment.psm1` : partitionnement UEFI/GPT et installation de l’édition Windows sélectionnée.
|
||||
- `Themes/Light.psd1` et `Themes/Dark.psd1` : palettes et styles des boutons.
|
||||
|
||||
## Lancement
|
||||
@@ -27,11 +28,11 @@ Conserver à côté du script les ressources habituelles du projet (`Assets`, `u
|
||||
|
||||
Le redimensionnement de la fenêtre recalcule désormais les largeurs des panneaux, la taille du tableau et du journal, ainsi que la position des boutons. Les marges et hauteurs des contrôles restent volontairement fixes pour préserver la lisibilité sous WinPE.
|
||||
|
||||
## Limites connues de cette première étape
|
||||
## Architecture et validation
|
||||
|
||||
Cette version centralise le chargement des thèmes et le style des boutons, et introduit une mise en page adaptative. La logique de déploiement, la détection des sources, l'inventaire et UltraVNC restent pour l'instant dans le script principal : leur extraction en modules indépendants doit être faite progressivement afin de ne pas casser les dépendances PowerShell ni le fonctionnement WinPE.
|
||||
`DeployCenter.ps1` orchestre les modules : `UI.psm1` porte les workflows et contrôles d’interface dans le scope de session partagé, `ThemeManager.psm1` charge les palettes, `HardwareInventory.psm1` collecte et exporte les informations matérielles, `Partitioning.psm1` valide les profils et calcule les capacités, `WindowsSources.psm1` analyse les images Windows, et `Deployment.psm1` applique le profil puis déploie l’image.
|
||||
|
||||
Le script n'a pas pu être exécuté dans cet environnement, faute de moteur Windows PowerShell/WinForms. Il doit être validé sur la machine WinPE cible avant remplacement de la version actuelle.
|
||||
La syntaxe PowerShell a été vérifiée statiquement. Le déroulement DISM/DiskPart/reagentc et le démarrage UEFI restent à valider sur la machine WinPE cible avant le premier déploiement réel.
|
||||
|
||||
|
||||
## Mot de passe UltraVNC de test
|
||||
@@ -45,6 +46,24 @@ Password=MonMotDePasseDeTest
|
||||
|
||||
Le mot de passe est stocké en clair dans `DeployCenter.ini` : utilisez cette option uniquement dans un environnement de test protégé.
|
||||
|
||||
## Taille de la partition WinRE
|
||||
|
||||
La taille configurée par défaut est de 1 Go (1024 Mo). Elle peut être remplacée dans `DeployCenter.ini`, sous `[Partitioning]`, par `WinRESizeMB=1536` par exemple. Les valeurs doivent être des entiers d’au moins 990 Mo. Après l’application de Windows, DeployCenter vérifie la taille réelle de `winre.wim` et estime l’espace restant après sa copie ; l’installation est interrompue si la partition ne conserve pas au moins 250 Mo libres ou si elle est plus petite que 990 Mo.
|
||||
|
||||
## Historique récent
|
||||
|
||||
- Correction du retour du plan de partitionnement : conversion explicite de la liste générique en tableau pour éviter une erreur de types sous Windows PowerShell 5.1 avant l’exécution de DiskPart.
|
||||
- Sous certains WinPE, le code de sortie de DiskPart peut être absent malgré une exécution réussie. DeployCenter confirme désormais le partitionnement et le retrait des lettres en relisant l’état des partitions.
|
||||
- Remplacement de l’apostrophe typographique de « d’hôte » par une apostrophe ASCII simple dans le message de validation NetBIOS de `Deployment.psm1`.
|
||||
- Correction de la chaîne du journal DiskPart : les apostrophes typographiques dans une chaîne PowerShell délimitée par apostrophes simples provoquaient une erreur de syntaxe signalée plus loin dans le fichier.
|
||||
- Vérification des lettres de partition par requête directe `Get-Partition -DriveLetter`, puis contrôle du numéro de disque, afin de ne pas dépendre des lettres présentes dans l’instantané retourné par `Get-Partition -DiskNumber` sous WinPE.
|
||||
- La partition Recovery, marquée cachée par ses attributs GPT, peut ne pas être exposée par `Get-Partition -DriveLetter`. DeployCenter l’associe alors à sa position dans les partitions fraîchement créées par DiskPart.
|
||||
- Les partitions EFI et Recovery pouvant être absentes des requêtes `Get-Partition -DriveLetter` dans WinPE, l’association des rôles suit désormais l’ordre des partitions créées et vérifie l’accès effectif à chaque lettre temporaire.
|
||||
- Retrait de l’ajout automatique de `/ScratchDir` aux commandes DISM afin de tester le comportement sans cette redirection dans WinPE ; le journal DISM dédié est conservé.
|
||||
- La progression de l’application d’image lit maintenant les pourcentages émis par DISM pendant son exécution et les transmet à la barre WinForms. Si le code de sortie DISM est absent, le message de réussite explicite est aussi pris en compte.
|
||||
- Le pourcentage DISM est aussi affiché dans le libellé de l’étape de progression, sans ajouter de ligne de journal à chaque mise à jour.
|
||||
- Vérification de l’espace WinRE via `System.IO.DriveInfo` : `Get-Volume -DriveLetter` ne trouve pas toujours les volumes de récupération cachés dans WinPE, même lorsqu’ils sont montés et accessibles.
|
||||
|
||||
|
||||
### Correctifs v3
|
||||
- En-têtes du tableau des éditions Windows avec une couleur dédiée définie par le thème (`TableHeader`).
|
||||
@@ -92,15 +111,48 @@ La gestion d'UltraVNC est isolée dans `Modules/VNC.psm1`. Le fichier est charg
|
||||
|
||||
- `Modules/Network.psm1` : détection de l’adresse IPv4 de déploiement, utilisée par le pied de page et le nom du journal.
|
||||
- `Modules/Logging.psm1` : initialisation et écriture des journaux. L’adresse IPv4 détectée lui est transmise par le script principal.
|
||||
- `Modules/Partitioning.psm1` : découverte et validation des profils de partitionnement déclarés dans `Partitioning/Profiles/*.psd1`.
|
||||
- `Modules/Configuration.psm1` : lecture de `DeployCenter.ini` en lecture seule.
|
||||
- `Modules/ThemeManager.psm1` : chargement et validation des palettes déclarées dans `Themes/*.psd1`.
|
||||
- `Modules/UI.psm1` : contrôles, boîtes de dialogue, fenêtres Sources Windows et workflows d’interface.
|
||||
- `Modules/HardwareInventory.psm1` : collecte matérielle, sélection d’un disque et export consolidé de l’inventaire.
|
||||
- `Modules/Partitioning.psm1` : découverte et validation des profils PSD1, calcul de capacité et construction de l’état de dimensionnement.
|
||||
- `Modules/Deployment.psm1` : orchestration du partitionnement et de l’installation Windows pour les profils UEFI/GPT.
|
||||
- `Modules/WindowsSources.psm1` : recherche de supports Windows et lecture des éditions WIM/ESD.
|
||||
|
||||
## Profils de partitionnement
|
||||
|
||||
Les fichiers PSD1 de `Partitioning/Profiles` décrivent les schémas de partitionnement comme des données, sans exécuter de code contenu dans les profils. Le profil par défaut « UEFI / GPT — WinRE avant C: » crée EFI (300 Mo), MSR (16 Mo), WinRE (taille de `winre.wim` plus 250 Mo libres), puis Windows sur l’espace restant. Le profil « UEFI / GPT — WinRE, C: et D: » demande la taille de C: et attribue le reste à D:, dans l’ordre physique C: puis D:. Les propriétés de taille déclarées dans le profil alimentent un formulaire générique. Le profil fournit une estimation de la capacité disponible avec une réserve WinRE de 1 Go ; le calcul effectif de WinRE et l’exécution DiskPart ne sont pas encore implémentés.
|
||||
Les fichiers PSD1 de `Partitioning/Profiles` décrivent les schémas de partitionnement comme des données, sans exécuter de code contenu dans les profils. Le profil par défaut « UEFI / GPT — WinRE avant C: » crée EFI (300 Mo), MSR (16 Mo), WinRE (taille définie dans `DeployCenter.ini`, 1 Go par défaut), puis Windows sur l’espace restant. Le profil « UEFI / GPT — WinRE, C: et D: » demande la taille de C: et attribue le reste à D:, dans l’ordre physique C: puis D:. Les propriétés de taille déclarées dans le profil alimentent un formulaire générique.
|
||||
|
||||
Le bouton « Installer » demande une confirmation explicite avant d’effacer le disque sélectionné. Le flux vérifie le firmware, crée les partitions définies par le profil, applique l’image avec DISM, vérifie ensuite que WinRE tient dans la partition configurée avec au moins 250 Mo libres, configure le nom d’hôte, crée les fichiers de démarrage UEFI et active WinRE. Les opérations longues affichent une fenêtre de progression et sont consignées dans le journal. Le moteur actuel prend en charge les profils UEFI/GPT et requiert une image contenant `Windows\System32\Recovery\winre.wim`.
|
||||
|
||||
|
||||
## Historique des changements
|
||||
|
||||
### 2026-10-17
|
||||
|
||||
- Correction de l’échappement des arguments des commandes natives Windows. Les chemins terminés par une barre oblique inverse, comme `W:\`, ne ferment plus prématurément leur guillemet ; cela permet à `mountvol` de recevoir correctement sa lettre et le chemin de volume.
|
||||
- L’affectation temporaire d’une lettre source vérifie l’accès effectif au fichier WIM après `mountvol`, afin de gérer les environnements WinPE qui ne remontent pas correctement son code de sortie.
|
||||
- Le point de montage DISM temporaire est créé à la racine du lecteur système WinPE plutôt que sous `%TEMP%\DeployCenter`, après l’échec d’accès DISM dans ce dernier emplacement ; la libération de la lettre source est confirmée par l’inaccessibilité du WIM après `mountvol /D`.
|
||||
- La taille de WinRE est lue en montant l’édition sélectionnée avec DISM en lecture seule, pour les sources `.wim` comme `.esd`, puis en démontant l’image sans modification. Cette méthode remplace l’extraction directe de `winre.wim`, qui remplissait le disque RAM WinPE (erreur WIMGAPI `112`).
|
||||
- Remplacement de cette lecture préalable par une taille WinRE configurable dans `[Partitioning] WinRESizeMB` (1024 Mo par défaut). Le profil réserve cette taille sans monter ni extraire `install.wim` ; après l’application de Windows, DeployCenter mesure `winre.wim` et arrête la configuration WinRE si la partition est inférieure à 990 Mo ou ne conserverait pas 250 Mo libres après copie.
|
||||
- Correction de la fermeture du callback de progression : le callback de journalisation est maintenant capturé explicitement avant `GetNewClosure()`, afin qu’il reste accessible depuis le scope de l’action de progression.
|
||||
- Pour la préparation DISM, une source ISO exposée sous un chemin de volume GUID reçoit temporairement une lettre libre autre que C: ou D:. DISM reçoit aussi son répertoire temporaire et son propre journal pour faciliter le diagnostic des erreurs de montage.
|
||||
- Resserrement de l’espacement dans la confirmation d’effacement pour rapprocher l’avertissement du nom d’hôte.
|
||||
- Ajout du flux intégré « Installer » : confirmation avant effacement, tailles des partitions calculées depuis le profil, application DISM, configuration du nom d’hôte, fichiers de démarrage UEFI et activation de WinRE.
|
||||
- Création de `Modules/Deployment.psm1` pour isoler l’exécution du déploiement ; ajout dans `UI.psm1` des fenêtres thémées de confirmation et de progression. Le nouveau bouton d’installation suit le thème actif.
|
||||
- Suppression des objets de résultat de démontage ISO à la fois dans `WindowsSources.psm1` et aux appels UI, afin qu’ils ne s’affichent pas dans la console PowerShell.
|
||||
- Prise en charge des volumes ISO exposés par WinPE sans objet partition : recherche via le chemin de volume (GUID), puis libération de D: avec `mountvol` si cette lettre est attribuée à l’ISO.
|
||||
- Recherche de `install.wim`/`install.esd` à partir des volumes associés explicitement à l’ISO, puis association de chaque volume à sa partition.
|
||||
- Le montage ISO réutilise une lettre déjà attribuée si elle n’est ni C: ni D: ; si WinPE a attribué l’une de ces lettres, elle est retirée avant l’affectation temporaire.
|
||||
- Correction des appels `Add-PartitionAccessPath` et `Remove-PartitionAccessPath` : avec `-InputObject`, la lettre du lecteur doit être fournie comme `-AccessPath` pour respecter leurs jeux de paramètres.
|
||||
- Résolution du disque ISO par `Get-Disk -Number` à partir de `MSFT_DiskImage.Number`, sans pipeline ambigu dans certaines versions de WinPE.
|
||||
- Correction de l’association entre un ISO monté et son disque dans WinPE : résolution via `Get-DiskImage | Get-Disk` plutôt que par le chemin `\\.\CDROMn`.
|
||||
- Le montage ISO n’attribue plus de lettre automatiquement : une lettre temporaire disponible est choisie en excluant C: et D:. Une barre d’activité apparaît pendant l’analyse des éditions.
|
||||
- Renommage de la commande de nettoyage du montage ISO avec le verbe PowerShell approuvé `Dismount`, pour supprimer l’avertissement à l’import du module WindowsSources.
|
||||
- Le montage d’un ISO reste actif tant qu’il est la source Windows sélectionnée ; il est libéré après analyse annulée, remplacement par une autre source, erreur ou fermeture de DeployCenter.
|
||||
- Le callback de journalisation utilise `INFO` lorsque le niveau est omis ou vide, afin d’éviter une erreur de validation `ValidateSet` au démarrage.
|
||||
- Correction du callback de journalisation au démarrage : il conserve désormais le scope du script principal pour résoudre `Write-Log` depuis les événements WinForms. Les derniers appels directs de `DeployCenter.ps1` passent aussi par ce callback.
|
||||
|
||||
### 2026-10-15
|
||||
|
||||
- Les anciens exports de `HardwareInfo.ps1` ont d’abord été déplacés sous `%TEMP%\DeployCenter`, puis retirés au profit du seul inventaire consolidé décrit ci-dessous.
|
||||
@@ -131,3 +183,19 @@ Les fichiers PSD1 de `Partitioning/Profiles` décrivent les schémas de partitio
|
||||
- Le champ du nom d’hôte est aligné à droite du panneau avec un espacement réservé après son libellé afin d’éviter tout chevauchement.
|
||||
- Dans Sources Windows, le double-clic sur une ligne sélectionnée lance aussi l’affichage des éditions, comme le bouton dédié.
|
||||
- Après validation d’une édition Windows, la fenêtre Sources Windows se ferme pour revenir directement à l’écran principal.
|
||||
|
||||
- Extraction de la recherche, de la lecture des éditions et de la résolution des images Windows vers Modules/WindowsSources.psm1. DeployCenter.ps1 charge ce module après l’initialisation du journal.
|
||||
- Le module Sources Windows reçoit explicitement un callback de journalisation fourni par Logging.psm1, sans dépendre implicitement de la portée du script principal.
|
||||
|
||||
- Déplacement dans UI.psm1 des constructeurs génériques de labels, groupes et boutons, du pied de page de session et du réglage de fenêtre. Les fonctions reçoivent explicitement thème, police et données de session.
|
||||
- Extraction dans UI.psm1 de la sélection du logo selon le thème et de son remplacement ; chemins et PictureBox sont transmis en paramètres.
|
||||
- Création de Modules/Configuration.psm1 : la lecture de DeployCenter.ini est isolée et retourne un objet de préférences sans modifier le fichier.
|
||||
- Répartition des fonctions restantes : workflows de fenêtre et de thème dans UI.psm1, préférences dans Configuration.psm1, collecte/export/lecture disque dans HardwareInventory.psm1. Les profils et calculs de capacité restent dans Partitioning.psm1.
|
||||
- Configuration.psm1 renvoie désormais un objet de configuration avec des valeurs par défaut et ne dépend plus de variables script externes.
|
||||
- HardwareInventory.psm1 exporte la collecte et l’export avec des paramètres explicites ; HardwareInfo.ps1 reste un point d’entrée de compatibilité.
|
||||
- Nettoyage des espaces en fin de ligne dans les modules UI et Partitioning après leur déplacement.
|
||||
- Partitioning.psm1 construit désormais l’état des tailles choisi à partir du profil, du disque et du résultat du dialogue UI.
|
||||
- Ajout du BOM UTF-8 aux nouveaux fichiers PowerShell pour Windows PowerShell 5.1 et tolérance de l’absence temporaire d’inventaire lors de la résolution d’un disque.
|
||||
- Mise à jour de la description d’architecture et de la liste des modules après l’extraction des fonctions restantes.
|
||||
- Correction des appels d’export d’inventaire depuis le module VNC : les chemins requis et les données de session sont transmis explicitement, sans invite interactive au lancement.
|
||||
- Les workflows UI et VNC transmettent désormais leurs messages au callback de Logging.psm1, sans appeler Write-Log directement depuis leurs scopes.
|
||||
Reference in new issue
Block a user