From e388cdca45b7be696f40524a864bdad61b4067e7 Mon Sep 17 00:00:00 2001 From: Benoit LORAND Date: Thu, 15 Oct 2026 23:05:30 +0200 Subject: [PATCH] Ajout de la configuration de paritionnement des disques --- .gitignore | 4 +- DeployCenter.ps1 | 106 ++++++++++--- HardwareInfo.ps1 | 96 ++---------- Modules/Network.psm1 | 31 +++- Modules/Partitioning.psm1 | 104 +++++++++++++ Modules/UI.psm1 | 143 +++++++++++++++++- Modules/VNC.psm1 | 24 ++- .../Profiles/UEFI-GPT-WinRE-before-C.psd1 | 40 +++++ README.md | 25 +++ 9 files changed, 450 insertions(+), 123 deletions(-) create mode 100644 Modules/Partitioning.psm1 create mode 100644 Partitioning/Profiles/UEFI-GPT-WinRE-before-C.psd1 diff --git a/.gitignore b/.gitignore index aac2254..49c5897 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -Logs/* \ No newline at end of file +Logs/* +Archives v1/* +Archives v2/* \ No newline at end of file diff --git a/DeployCenter.ps1 b/DeployCenter.ps1 index 7bc77b6..8a7947d 100644 --- a/DeployCenter.ps1 +++ b/DeployCenter.ps1 @@ -96,7 +96,33 @@ if (-not (Test-Path -LiteralPath $loggingModulePath -PathType Leaf)) { } . ([scriptblock]::Create([System.IO.File]::ReadAllText($loggingModulePath))) Initialize-DeployCenterLogging -LogRoot $script:LogRoot -IPv4 $script:DeployIPv4 -$script:HardwareJson = Join-Path $env:TEMP "HardwareInfo.json" + +# Les profils de partitionnement sont des fichiers de données PSD1, validés par leur module. +$partitioningModulePath = Join-Path $script:DeployRoot 'Modules\Partitioning.psm1' +if (-not (Test-Path -LiteralPath $partitioningModulePath -PathType Leaf)) { + throw "Module de partitionnement introuvable : $partitioningModulePath" +} +Import-Module $partitioningModulePath -Force +$script:PartitionProfilesRoot = Join-Path $script:DeployRoot 'Partitioning\Profiles' +$script:PartitionProfiles = @() +foreach ($profileName in (Get-DeployCenterPartitionProfileNames -ProfilesRoot $script:PartitionProfilesRoot)) { + try { + $script:PartitionProfiles += Import-DeployCenterPartitionProfile -ProfilesRoot $script:PartitionProfilesRoot -ProfileName $profileName + } catch { + Write-Log "Profil de partitionnement '$profileName' ignoré : $($_.Exception.Message)" 'WARNING' + } +} +if ($script:PartitionProfiles.Count -eq 0) { + throw "Aucun profil de partitionnement valide trouvé dans $script:PartitionProfilesRoot" +} +$script:SelectedPartitionProfileIndex = 0 +for ($profileIndex = 0; $profileIndex -lt $script:PartitionProfiles.Count; $profileIndex++) { + if ($script:PartitionProfiles[$profileIndex].Default -eq $true) { + $script:SelectedPartitionProfileIndex = $profileIndex + break + } +} +$script:SelectedPartitionProfile = $script:PartitionProfiles[$script:SelectedPartitionProfileIndex] # Inventaire de session et UltraVNC portable : tout reste dans le dossier temporaire. $script:InventoryDir = Join-Path $env:TEMP 'DeployCenter' @@ -262,6 +288,21 @@ function Write-DeployInventory { } $json = $inventory | ConvertTo-Json -Depth 8 Set-Content -LiteralPath $script: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 $script:InventoryDir 'hardware.json') + ) + foreach ($legacyFile in $legacyInventoryFiles) { + if (Test-Path -LiteralPath $legacyFile -PathType Leaf) { + try { + Remove-Item -LiteralPath $legacyFile -Force -ErrorAction Stop + } catch { + Write-Log "Impossible de supprimer l'ancien export d'inventaire '$legacyFile' : $($_.Exception.Message)" 'WARNING' + } + } + } } catch { Write-Log "Impossible d'écrire l'inventaire temporaire : $($_.Exception.Message)" 'WARNING' } @@ -611,13 +652,13 @@ $script:form.Controls.Add($script:groupPC) # DISQUE PHYSIQUE - SELECTEUR COMPACT # ============================================================ -$script:groupDisks = New-GroupBox -Text "Disque physique" ` - -X $margin -Y 220 -Width $usableWidth -Height 78 +$script:groupDisks = New-GroupBox -Text "Disque et partitionnement" ` + -X $margin -Y 220 -Width $usableWidth -Height 116 $script:cmbDisk = New-Object System.Windows.Forms.ComboBox $script:cmbDisk.Name = "cmbDisk" -$script:cmbDisk.Location = New-Object System.Drawing.Point(12, 30) -$script:cmbDisk.Size = New-Object System.Drawing.Size(($usableWidth - 24), 28) +$script:cmbDisk.Location = New-Object System.Drawing.Point(12, 38) +$script:cmbDisk.Size = New-Object System.Drawing.Size(($usableWidth - 24), 26) $script:cmbDisk.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right $script:cmbDisk.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList $script:cmbDisk.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat @@ -626,13 +667,29 @@ $script:cmbDisk.ForeColor = $script:Theme.Text $script:cmbDisk.Font = $script:FontSmall $script:cmbDisk.IntegralHeight = $false $script:cmbDisk.MaxDropDownItems = 12 + +$partitionProfileSelector = New-DeployCenterPartitionProfileSelector ` + -Parent $script:groupDisks ` + -ProfileNames @($script:PartitionProfiles | ForEach-Object { [string]$_.Name }) ` + -Theme $script:Theme ` + -Font $script:FontSmall +$script:lblPartitionProfile = $partitionProfileSelector.Label +$script:cmbPartitionProfile = $partitionProfileSelector.ComboBox +$script:cmbPartitionProfile.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right +$script:cmbPartitionProfile.SelectedIndex = $script:SelectedPartitionProfileIndex + $script:groupDisks.Controls.Add($script:cmbDisk) $script:form.Controls.Add($script:groupDisks) function Set-DiskSelectorWidth { if ($null -eq $script:cmbDisk -or $null -eq $script:groupDisks) { return } - $script:cmbDisk.Location = New-Object System.Drawing.Point(12, 30) - $script:cmbDisk.Size = New-Object System.Drawing.Size([Math]::Max(250, $script:groupDisks.ClientSize.Width - 24), 28) + $selectorWidth = [Math]::Max(250, $script:groupDisks.ClientSize.Width - 24) + $script:cmbDisk.Location = New-Object System.Drawing.Point(12, 38) + $script:cmbDisk.Size = New-Object System.Drawing.Size($selectorWidth, 26) + Set-DeployCenterPartitionProfileSelectorLayout ` + -Label $script:lblPartitionProfile ` + -ComboBox $script:cmbPartitionProfile ` + -Width $selectorWidth } # ============================================================ @@ -730,6 +787,9 @@ function Set-ControlTheme { $Control.BackColor = $script:Theme.PanelAlt $Control.ForeColor = $script:Theme.Text } + elseif ($Control -is [System.Windows.Forms.ComboBox]) { + Set-DeployCenterComboBoxTheme -ComboBox $Control -Theme $script:Theme + } elseif ($Control -is [System.Windows.Forms.TextBox]) { $Control.BackColor = $script:Theme.PanelAlt $Control.ForeColor = $script:Theme.Text @@ -1328,8 +1388,8 @@ function Show-WindowsSourcesDialog { $list.Location = New-Object System.Drawing.Point(16, 112) $list.Size = New-Object System.Drawing.Size([Math]::Max(300, $clientWidth - 32), [Math]::Max(120, $clientHeight - 205)) $list.Anchor = 'Top,Bottom,Left,Right' - $lblStatus.Location = New-Object System.Drawing.Point(16, ($clientHeight - 40)) - $lblStatus.Size = New-Object System.Drawing.Size([Math]::Max(300, $clientWidth - 32), 32) + $lblStatus.Location = New-Object System.Drawing.Point(16, ($clientHeight - 52)) + $lblStatus.Size = New-Object System.Drawing.Size([Math]::Max(300, $clientWidth - 32), 24) $lblStatus.Anchor = 'Bottom,Left,Right' } $dlg.Add_Shown({ @@ -1342,7 +1402,7 @@ function Show-WindowsSourcesDialog { $btnEditions.Add_Click({ if ($list.SelectedItems.Count -eq 0) { - [System.Windows.Forms.MessageBox]::Show('Sélectionne d''abord une source dans la liste.', 'Éditions Windows', 'OK', 'Information') | Out-Null + Show-DeployCenterMessage -Message 'Sélectionne d''abord une source dans la liste.' -Title 'Éditions Windows' -Icon Information -Owner $dlg -Theme $script:Theme -Font $script:FontRegular return } @@ -1567,7 +1627,7 @@ function Show-WindowsSourcesDialog { } catch { $lblStatus.Text = "Echec de l'analyse des editions de $selectedName." Write-Log "Erreur analyse editions $selectedPath : $($_.Exception.Message)" 'ERROR' - [System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Analyse des editions', 'OK', 'Warning') | Out-Null + Show-DeployCenterMessage -Message $_.Exception.Message -Title 'Analyse des éditions' -Icon Warning -Owner $dlg -Theme $script:Theme -Font $script:FontRegular } finally { $dlg.Cursor = [System.Windows.Forms.Cursors]::Default $btnEditions.Enabled = $true @@ -1592,7 +1652,7 @@ function Show-WindowsSourcesDialog { } catch { $lblStatus.Text = 'Echec de la recherche. Consulte le message.' Write-Log "Erreur detection sources : $($_.Exception.Message)" 'ERROR' - [System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Detection des sources', 'OK', 'Warning') | Out-Null + Show-DeployCenterMessage -Message $_.Exception.Message -Title 'Détection des sources' -Icon Warning -Owner $dlg -Theme $script:Theme -Font $script:FontRegular } finally { $dlg.Cursor = [System.Windows.Forms.Cursors]::Default $btnScan.Enabled = $true @@ -1643,7 +1703,7 @@ function Update-HardwareView { try { $script:cmbDisk.Items.Clear() foreach ($disk in $info.Disks) { - $diskLabel = "Disque {0} - {1} - {2:N1} Go - {3} - {4}" -f $disk.Index, $disk.Model, $disk.SizeGB, $disk.Interface, $disk.Status + $diskLabel = "Disque {0} - {1} - {2:N1} Go - {3} - {4}" -f $disk.Index, $disk.Model, $disk.SizeGB, $disk.BusType, $disk.Status $diskChoice = [pscustomobject]@{ Text = $diskLabel; Tag = [string]$disk.Index } [void]$script:cmbDisk.Items.Add($diskChoice) } @@ -1663,8 +1723,6 @@ function Update-HardwareView { $script:cmbDisk.EndUpdate() } - $json = $info | ConvertTo-Json -Depth 6 - Set-Content -LiteralPath $script:HardwareJson -Value $json -Encoding UTF8 Write-DeployInventory -HardwareInfo $info Write-Log "Inventaire materiel actualise." "SUCCESS" @@ -1680,12 +1738,7 @@ function Update-HardwareView { catch { Write-Log "Erreur inventaire : $($_.Exception.Message)" "ERROR" - [System.Windows.Forms.MessageBox]::Show( - $_.Exception.Message, - "Erreur DeployCenter", - [System.Windows.Forms.MessageBoxButtons]::OK, - [System.Windows.Forms.MessageBoxIcon]::Error - ) | Out-Null + Show-DeployCenterMessage -Message $_.Exception.Message -Title 'Erreur DeployCenter' -Icon Error -Owner $script:form -Theme $script:Theme -Font $script:FontRegular } finally { $script:btnRefresh.Enabled = $true @@ -1714,7 +1767,7 @@ function Update-DeployCenterLayout { $script:groupPC.Location = New-Object System.Drawing.Point($m, 100) $script:groupPC.Size = New-Object System.Drawing.Size($contentWidth, 105) $script:groupDisks.Location = New-Object System.Drawing.Point($m, 220) - $script:groupDisks.Size = New-Object System.Drawing.Size($contentWidth, 78) + $script:groupDisks.Size = New-Object System.Drawing.Size($contentWidth, 116) Set-DiskSelectorWidth # Le journal absorbe l'espace vertical restant au-dessus des actions et du pied de page. @@ -1765,6 +1818,15 @@ $script:cmbDisk.Add_SizeChanged({ Set-DiskSelectorWidth }) +$script:cmbPartitionProfile.Add_SelectedIndexChanged({ + $selectedIndex = $script:cmbPartitionProfile.SelectedIndex + if ($selectedIndex -ge 0 -and $selectedIndex -lt $script:PartitionProfiles.Count) { + $script:SelectedPartitionProfileIndex = $selectedIndex + $script:SelectedPartitionProfile = $script:PartitionProfiles[$selectedIndex] + Write-Log "Profil de partitionnement sélectionné : $($script:SelectedPartitionProfile.Name)" 'INFO' + } +}) + $script:form.Add_SizeChanged({ Update-DeployCenterLayout }) diff --git a/HardwareInfo.ps1 b/HardwareInfo.ps1 index ebeafaf..48e79eb 100644 --- a/HardwareInfo.ps1 +++ b/HardwareInfo.ps1 @@ -73,20 +73,22 @@ function Get-HardwareInfo { # Inventaire des disques physiques try { - $disks = Get-CimInstance ` - -ClassName Win32_DiskDrive ` - -ErrorAction Stop + $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.Index - Model = $disk.Model + Index = [int]$disk.Number + Model = [string]$disk.FriendlyName SizeBytes = [long]$disk.Size SizeGB = [math]::Round( ([double]$disk.Size / 1GB), 1 ) - InterfaceType = $disk.InterfaceType - Status = $disk.Status + BusType = [string]$disk.BusType + Status = $status + HealthStatus = [string]$disk.HealthStatus } } } @@ -97,83 +99,3 @@ function Get-HardwareInfo { # Retourner un objet exploitable par les autres modules [PSCustomObject]$info } - - -# ============================================================ -# Execution du test -# ============================================================ - -$LogDirectory = "X:\DeployLogs" -$JsonPath = Join-Path $LogDirectory "hardware.json" -$TextPath = Join-Path $LogDirectory "hardware.log" - -try { - New-Item -ItemType Directory ` - -Path $LogDirectory ` - -Force ` - -ErrorAction Stop | Out-Null -} -catch { - throw "Impossible de creer le repertoire de logs : $($_.Exception.Message)" -} - -$result = Get-HardwareInfo - -# Export JSON en UTF-8 avec BOM sous Windows PowerShell 5.1 -try { - $json = $result | ConvertTo-Json -Depth 5 - - Set-Content ` - -Path $JsonPath ` - -Value $json ` - -Encoding UTF8 ` - -ErrorAction Stop -} -catch { - throw "Impossible d'enregistrer le JSON : $($_.Exception.Message)" -} - -# Construire un journal texte sans utiliser Write-Host -$lines = @( - "=== DeployCenter - Inventaire materiel ===" - "Date : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" - "Fabricant : $($result.Manufacturer)" - "Modele : $($result.Model)" - "Numero serie : $($result.SerialNumber)" - "Firmware : $($result.Firmware)" - "Architecture : $($result.Architecture)" - "WinPE : $($result.WinPE)" - "" - "=== Disques detectes ===" -) - -foreach ($disk in $result.Disks) { - $lines += ( - "Disque {0} : {1} - {2} Go - {3} - {4}" -f - $disk.Index, - $disk.Model, - $disk.SizeGB, - $disk.InterfaceType, - $disk.Status - ) -} - -if ($result.Errors.Count -gt 0) { - $lines += "" - $lines += "=== Erreurs ===" - - foreach ($message in $result.Errors) { - $lines += $message - } -} - -$utf8 = New-Object System.Text.UTF8Encoding($false) - -[System.IO.File]::WriteAllLines( - $TextPath, - [string[]]$lines, - $utf8 -) - -# Renvoyer l'objet comme sortie principale du script -$result diff --git a/Modules/Network.psm1 b/Modules/Network.psm1 index df418bc..23c0e83 100644 --- a/Modules/Network.psm1 +++ b/Modules/Network.psm1 @@ -9,17 +9,36 @@ function Get-DeployNetworkIPv4 { # APIPA et adresses IPv4 non routables couramment inutiles en WinPE. $candidates = @() try { - foreach ($cfg in (Get-CimInstance Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=True' -ErrorAction Stop)) { - $hasGateway = @($cfg.DefaultIPGateway | Where-Object { $_ -match '^\d{1,3}(\.\d{1,3}){3}$' }).Count -gt 0 - foreach ($address in @($cfg.IPAddress | Where-Object { $_ -match '^\d{1,3}(\.\d{1,3}){3}$' })) { - if ($address -eq '127.0.0.1' -or $address -like '169.254.*' -or $address -eq '0.0.0.0') { continue } - $candidates += [pscustomobject]@{ Address = $address; HasGateway = $hasGateway; Index = [int]$cfg.Index } + foreach ($interface in [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()) { + if ($interface.NetworkInterfaceType -eq [System.Net.NetworkInformation.NetworkInterfaceType]::Loopback) { continue } + + $properties = $interface.GetIPProperties() + $ipv4Properties = $properties.GetIPv4Properties() + if ($null -eq $ipv4Properties) { continue } + + $hasGateway = @( + $properties.GatewayAddresses | Where-Object { + $_.Address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and + $_.Address.ToString() -ne '0.0.0.0' + } + ).Count -gt 0 + + foreach ($unicast in $properties.UnicastAddresses) { + $address = $unicast.Address + if ($address.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { continue } + $addressText = $address.ToString() + if ($addressText -eq '127.0.0.1' -or $addressText -like '169.254.*' -or $addressText -eq '0.0.0.0') { continue } + $candidates += [pscustomobject]@{ + Address = $addressText + HasGateway = $hasGateway + Index = [int]$ipv4Properties.Index + } } } } catch { # La détection réseau ne doit pas empêcher le démarrage de DeployCenter. if (Get-Command Write-Log -ErrorAction SilentlyContinue) { - Write-Log "Détection réseau WMI : $($_.Exception.Message)" 'WARNING' + Write-Log "Détection réseau : $($_.Exception.Message)" 'WARNING' } } diff --git a/Modules/Partitioning.psm1 b/Modules/Partitioning.psm1 new file mode 100644 index 0000000..bbe5cb1 --- /dev/null +++ b/Modules/Partitioning.psm1 @@ -0,0 +1,104 @@ +# DeployCenter - Chargement et validation des profils de partitionnement +# Les profils PSD1 sont des fichiers de données ; ils ne contiennent aucun code exécuté. + +function Get-DeployCenterPartitionProfileNames { + [CmdletBinding()] + param([Parameter(Mandatory=$true)][string]$ProfilesRoot) + + if (-not (Test-Path -LiteralPath $ProfilesRoot -PathType Container)) { return @() } + $names = @() + foreach ($file in (Get-ChildItem -LiteralPath $ProfilesRoot -Filter '*.psd1' -File -ErrorAction SilentlyContinue)) { + if ($file.BaseName -match '^[A-Za-z0-9_-]+$') { $names += $file.BaseName } + } + return @($names | Sort-Object -Unique) +} + +function Import-DeployCenterPartitionProfile { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$ProfilesRoot, + [Parameter(Mandatory=$true)][string]$ProfileName + ) + + if ($ProfileName -notmatch '^[A-Za-z0-9_-]+$') { throw "Nom de profil invalide : $ProfileName" } + $path = Join-Path $ProfilesRoot ($ProfileName + '.psd1') + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Profil introuvable : $ProfileName" } + + $profile = Import-PowerShellDataFile -LiteralPath $path + foreach ($key in @('Id','Name','Description','Firmware','PartitionStyle','Partitions')) { + if (-not $profile.ContainsKey($key)) { throw "Le profil '$ProfileName' ne contient pas la clé '$key'." } + } + if ([string]$profile.Id -ne $ProfileName) { throw "L'identifiant du profil '$ProfileName' ne correspond pas au nom du fichier." } + if (@($profile.Partitions).Count -lt 2) { throw "Le profil '$ProfileName' doit définir au moins deux partitions." } + if (@($profile.Partitions | Where-Object { [string]$_.Role -eq 'Windows' }).Count -ne 1) { + throw "Le profil '$ProfileName' doit définir exactement une partition Windows." + } + $userInputPartitions = @($profile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'UserInputGB' }) + $remainderPartitions = @($profile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'RemainderAfterInputs' }) + $remainingPartitions = @($profile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'Remaining' }) + if ($userInputPartitions.Count -gt 0) { + if ($remainderPartitions.Count -ne 1 -or $remainingPartitions.Count -gt 0) { + throw "Le profil '$ProfileName' doit associer les saisies utilisateur à une partition de reste unique." + } + } elseif ($remainingPartitions.Count -ne 1 -or [string]$profile.Partitions[-1].Size.Policy -ne 'Remaining') { + throw "Le profil '$ProfileName' doit placer une unique partition utilisant l'espace restant en dernière position." + } + foreach ($partition in $profile.Partitions) { + foreach ($key in @('Name','Role','FileSystem','Size')) { + if (-not $partition.ContainsKey($key)) { throw "Une partition du profil '$ProfileName' ne contient pas la clé '$key'." } + } + if (-not $partition.Size.ContainsKey('Policy')) { throw "La taille d'une partition du profil '$ProfileName' ne définit pas Policy." } + switch ([string]$partition.Size.Policy) { + 'FixedMB' { + if ([int]$partition.Size.Value -le 0) { throw "Une taille fixe du profil '$ProfileName' doit être supérieure à zéro." } + } + 'ImageSizePlusFreeSpace' { + 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." } + } + '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." + } + if ([string]::IsNullOrWhiteSpace([string]$partition.Size.Label)) { throw "La saisie de taille utilisateur du profil '$ProfileName' doit définir Label." } + } + 'RemainderAfterInputs' { + if ([double]$partition.Size.MinimumGB -lt 0 -or [string]::IsNullOrWhiteSpace([string]$partition.Size.Label)) { + throw "La partition de reste du profil '$ProfileName' doit définir un MinimumGB non négatif et un Label." + } + } + 'Remaining' { } + default { throw "Politique de taille inconnue dans le profil '$ProfileName' : $($partition.Size.Policy)" } + } + } + + return $profile +} + +function Get-DeployCenterPartitionSizingCapacity { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile, + [Parameter(Mandatory=$true)][long]$DiskSizeBytes + ) + + $reservedMB = 0.0 + foreach ($partition in $Profile.Partitions) { + switch ([string]$partition.Size.Policy) { + 'FixedMB' { $reservedMB += [double]$partition.Size.Value } + default { + if ($partition.Size.ContainsKey('PreviewMB')) { $reservedMB += [double]$partition.Size.PreviewMB } + } + } + } + + $capacityBytes = $DiskSizeBytes - [long][math]::Ceiling($reservedMB * 1MB) + if ($capacityBytes -le 0) { throw "Le disque est trop petit pour les réserves du profil '$($Profile.Name)'." } + return [pscustomobject]@{ + UsableBytes = [long]$capacityBytes + UsableGB = [math]::Round(($capacityBytes / 1GB), 1) + ReservedMB = [math]::Ceiling($reservedMB) + } +} + +Export-ModuleMember -Function Get-DeployCenterPartitionProfileNames, Import-DeployCenterPartitionProfile, Get-DeployCenterPartitionSizingCapacity diff --git a/Modules/UI.psm1 b/Modules/UI.psm1 index c375b4e..463dea5 100644 --- a/Modules/UI.psm1 +++ b/Modules/UI.psm1 @@ -19,4 +19,145 @@ } } -Export-ModuleMember -Function Set-DeployCenterButtonStyle +function New-DeployCenterComboBox { + param( + [Parameter(Mandatory=$true)][string]$Name, + [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Theme, + [Parameter(Mandatory=$true)][System.Drawing.Font]$Font, + [Parameter(Mandatory=$true)][string[]]$Items + ) + + $comboBox = New-Object System.Windows.Forms.ComboBox + $comboBox.Name = $Name + $comboBox.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList + $comboBox.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat + $comboBox.BackColor = $Theme.PanelAlt + $comboBox.ForeColor = $Theme.Text + $comboBox.Font = $Font + $comboBox.IntegralHeight = $false + $comboBox.MaxDropDownItems = 12 + foreach ($item in $Items) { [void]$comboBox.Items.Add($item) } + return $comboBox +} + +function Set-DeployCenterComboBoxTheme { + param( + [Parameter(Mandatory=$true)][System.Windows.Forms.ComboBox]$ComboBox, + [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Theme + ) + $ComboBox.BackColor = $Theme.PanelAlt + $ComboBox.ForeColor = $Theme.Text +} + +function New-DeployCenterPartitionProfileSelector { + param( + [Parameter(Mandatory=$true)][System.Windows.Forms.GroupBox]$Parent, + [Parameter(Mandatory=$true)][string[]]$ProfileNames, + [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Theme, + [Parameter(Mandatory=$true)][System.Drawing.Font]$Font + ) + + $label = New-Object System.Windows.Forms.Label + $label.Text = 'Profil de partitionnement' + $label.Size = New-Object System.Drawing.Size(240, 17) + $label.ForeColor = $Theme.Text + $label.BackColor = [System.Drawing.Color]::Transparent + $label.Font = $Font + + $comboBox = New-DeployCenterComboBox -Name 'cmbPartitionProfile' -Theme $Theme -Font $Font -Items $ProfileNames + $Parent.Controls.Add($label) + $Parent.Controls.Add($comboBox) + Set-DeployCenterPartitionProfileSelectorLayout -Label $label -ComboBox $comboBox -Width ($Parent.ClientSize.Width - 24) + + return [pscustomobject]@{ Label = $label; ComboBox = $comboBox } +} + +function Set-DeployCenterPartitionProfileSelectorLayout { + param( + [Parameter(Mandatory=$true)][System.Windows.Forms.Label]$Label, + [Parameter(Mandatory=$true)][System.Windows.Forms.ComboBox]$ComboBox, + [Parameter(Mandatory=$true)][int]$Width + ) + $Label.Location = New-Object System.Drawing.Point(12, 66) + $ComboBox.Location = New-Object System.Drawing.Point(12, 84) + $ComboBox.Size = New-Object System.Drawing.Size([Math]::Max(250, $Width), 26) +} + +function Show-DeployCenterMessage { + param( + [Parameter(Mandatory=$true)][string]$Message, + [Parameter(Mandatory=$true)][string]$Title, + [ValidateSet('Information','Warning','Error')][string]$Icon = 'Information', + [System.Windows.Forms.Form]$Owner = $null, + [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Theme, + [Parameter(Mandatory=$true)][System.Drawing.Font]$Font + ) + + $messageForm = New-Object System.Windows.Forms.Form + $messageForm.Text = $Title + $messageForm.StartPosition = if ($Owner) { 'CenterParent' } else { 'CenterScreen' } + $messageForm.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog + $messageForm.ShowInTaskbar = $false + $messageForm.MinimizeBox = $false + $messageForm.MaximizeBox = $false + $messageForm.BackColor = $Theme.Background + $messageForm.ForeColor = $Theme.Text + $messageForm.Font = $Font + + $messageWidth = 390 + $textSize = [System.Windows.Forms.TextRenderer]::MeasureText( + $Message, + $Font, + (New-Object System.Drawing.Size($messageWidth, 0)), + [System.Windows.Forms.TextFormatFlags]::WordBreak + ) + $messageHeight = [Math]::Max(42, $textSize.Height + 8) + $messageForm.ClientSize = New-Object System.Drawing.Size(470, ($messageHeight + 94)) + + $iconLabel = New-Object System.Windows.Forms.Label + $iconLabel.Location = New-Object System.Drawing.Point(18, 24) + $iconLabel.Size = New-Object System.Drawing.Size(34, 34) + $iconLabel.Text = if ($Icon -eq 'Information') { 'i' } else { '!' } + $iconLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter + $iconLabel.Font = New-Object System.Drawing.Font('Segoe UI', [single]16, [System.Drawing.FontStyle]::Bold) + $iconLabel.ForeColor = if ($Icon -eq 'Error') { $Theme.Danger } else { $Theme.Accent } + $iconLabel.BackColor = [System.Drawing.Color]::Transparent + $messageForm.Controls.Add($iconLabel) + + $messageLabel = New-Object System.Windows.Forms.Label + $messageLabel.Location = New-Object System.Drawing.Point(64, 20) + $messageLabel.Size = New-Object System.Drawing.Size($messageWidth, $messageHeight) + $messageLabel.Text = $Message + $messageLabel.ForeColor = $Theme.Text + $messageLabel.BackColor = [System.Drawing.Color]::Transparent + $messageLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft + $messageLabel.Font = $Font + $messageForm.Controls.Add($messageLabel) + + $buttonPanel = New-Object System.Windows.Forms.Panel + $buttonPanel.Dock = [System.Windows.Forms.DockStyle]::Bottom + $buttonPanel.Height = 58 + $buttonPanel.BackColor = $Theme.PanelAlt + $messageForm.Controls.Add($buttonPanel) + + $okButton = New-Object System.Windows.Forms.Button + $okButton.Text = 'OK' + $okButton.Location = New-Object System.Drawing.Point(362, 10) + $okButton.Size = New-Object System.Drawing.Size(92, 36) + $okButton.Font = $Font + $okButton.Cursor = [System.Windows.Forms.Cursors]::Hand + $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK + Set-DeployCenterButtonStyle -Button $okButton -Theme $Theme -Role 'PrimaryButton' + $buttonPanel.Controls.Add($okButton) + $messageForm.AcceptButton = $okButton + $messageForm.CancelButton = $okButton + + if ($Owner) { + [void]$messageForm.ShowDialog($Owner) + } else { + [void]$messageForm.ShowDialog() + } + $messageForm.Dispose() +} + +Export-ModuleMember -Function Set-DeployCenterButtonStyle, New-DeployCenterComboBox, Set-DeployCenterComboBoxTheme, New-DeployCenterPartitionProfileSelector, Set-DeployCenterPartitionProfileSelectorLayout, Show-DeployCenterMessage diff --git a/Modules/VNC.psm1 b/Modules/VNC.psm1 index d4ddc24..c54052e 100644 --- a/Modules/VNC.psm1 +++ b/Modules/VNC.psm1 @@ -8,7 +8,6 @@ function Start-DeployCenterVnc { $passwordTool = Join-Path $sourceVnc 'createpassword64.exe' $sourceIni = Join-Path $sourceVnc 'ultravnc.ini' - $script:DeployIPv4 = Get-DeployNetworkIPv4 if ([string]::IsNullOrWhiteSpace($script:DeployIPv4)) { Write-Log 'Aucune adresse IPv4 de déploiement détectée.' 'WARNING' } @@ -29,25 +28,36 @@ function Start-DeployCenterVnc { return } + $vncStage = 'préparation du dossier temporaire' try { if (Test-Path -LiteralPath $script:VncDirectory) { Remove-Item -LiteralPath $script:VncDirectory -Recurse -Force -ErrorAction Stop } New-Item -Path $script:VncDirectory -ItemType Directory -Force | Out-Null + $vncStage = "copie des fichiers depuis $sourceVnc vers $script:VncDirectory" Copy-Item -Path (Join-Path $sourceVnc '*') -Destination $script:VncDirectory -Recurse -Force # WinPE ne permet pas toujours d'ajouter des règles de pare-feu. # La désactivation globale n'est effectuée que lorsque VNC est demandé. + $vncStage = 'désactivation du pare-feu WinPE' $wpeutil = Join-Path $env:WINDIR 'System32\wpeutil.exe' if (-not (Test-Path -LiteralPath $wpeutil -PathType Leaf)) { $wpeutil = 'wpeutil.exe' } - & $wpeutil DisableFirewall - if ($LASTEXITCODE -ne 0) { - throw "wpeutil DisableFirewall a renvoyé le code $LASTEXITCODE." + $wpeutilOutput = @(& $wpeutil DisableFirewall 2>&1) + $wpeutilExitCode = $LASTEXITCODE + foreach ($outputLine in $wpeutilOutput) { + $outputText = [string]$outputLine + if (-not [string]::IsNullOrWhiteSpace($outputText)) { + Write-Log "wpeutil DisableFirewall : $outputText" 'INFO' + } } - Write-Log 'Pare-feu WinPE désactivé à la demande de -Vnc.' 'WARNING' + 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' # Force le mode portable : l'INI actif est celui placé à côté de winvnc.exe. + $vncStage = 'activation du mode portable UltraVNC' $portableFlag = Join-Path $script:VncDirectory 'ultravnc.portable' Set-Content -LiteralPath $portableFlag -Value '' -Encoding ASCII @@ -64,6 +74,7 @@ function Start-DeployCenterVnc { } # createpassword64 encode le mot de passe choisi dans le champ passwd d'ultravnc.ini. + $vncStage = 'génération du mot de passe UltraVNC' $tool = Join-Path $script:VncDirectory 'createpassword64.exe' $script:VncProcess = Start-Process -FilePath $tool -ArgumentList @($script:VncPassword) ` -WorkingDirectory $script:VncDirectory -Wait -PassThru -WindowStyle Hidden @@ -78,6 +89,7 @@ function Start-DeployCenterVnc { } $server = Join-Path $script:VncDirectory 'winvnc.exe' + $vncStage = 'démarrage du serveur UltraVNC' $script:VncProcess = Start-Process -FilePath $server -ArgumentList @('-run') ` -WorkingDirectory $script:VncDirectory -PassThru -WindowStyle Hidden Start-Sleep -Milliseconds 800 @@ -87,7 +99,7 @@ function Start-DeployCenterVnc { $script:VncStarted = $true Write-Log "UltraVNC démarré. IP : $($script:DeployIPv4) ; mot de passe $passwordSource." 'SUCCESS' } catch { - Write-Log "Échec du démarrage UltraVNC : $($_.Exception.Message)" 'ERROR' + Write-Log "Échec du démarrage UltraVNC à l'étape '$vncStage' : $($_.Exception.Message)" 'ERROR' $script:VncStarted = $false $script:VncPassword = $null } diff --git a/Partitioning/Profiles/UEFI-GPT-WinRE-before-C.psd1 b/Partitioning/Profiles/UEFI-GPT-WinRE-before-C.psd1 new file mode 100644 index 0000000..c9fc6fb --- /dev/null +++ b/Partitioning/Profiles/UEFI-GPT-WinRE-before-C.psd1 @@ -0,0 +1,40 @@ +@{ + Id = 'UEFI-GPT-WinRE-before-C' + Name = 'UEFI / GPT — WinRE avant C:' + Description = "Crée EFI, MSR et WinRE avant la partition Windows qui occupe l’espace restant." + Default = $true + Firmware = 'UEFI' + PartitionStyle = 'GPT' + Partitions = @( + @{ + Name = 'EFI' + Role = 'System' + FileSystem = 'FAT32' + GptTypeId = 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B' + Size = @{ Policy = 'FixedMB'; Value = 300 } + } + @{ + Name = 'MSR' + Role = 'Reserved' + FileSystem = $null + GptTypeId = 'E3C9E316-0B5C-4DB8-817D-F92DF00215AE' + Size = @{ Policy = 'FixedMB'; Value = 16 } + } + @{ + Name = 'WinRE' + Role = 'Recovery' + FileSystem = 'NTFS' + GptTypeId = 'DE94BBA4-06D1-4D40-A16A-BFD50179D6AC' + GptAttributes = '0x8000000000000001' + Size = @{ Policy = 'ImageSizePlusFreeSpace'; FreeSpaceMB = 250; PreviewMB = 1024 } + } + @{ + Name = 'Windows' + Role = 'Windows' + FileSystem = 'NTFS' + GptTypeId = 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7' + DriveLetterAfterReboot = 'C' + Size = @{ Policy = 'Remaining' } + } + ) +} diff --git a/README.md b/README.md index e5f296f..cc455f4 100644 --- a/README.md +++ b/README.md @@ -92,3 +92,28 @@ 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`. + +## 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 actuellement disponible est « UEFI / GPT — WinRE avant C: » : EFI (300 Mo), MSR (16 Mo), WinRE (taille de `winre.wim` plus 250 Mo libres), puis Windows sur l’espace restant. Il est marqué comme profil par défaut. La sélection est visible dans l’interface ; le calcul effectif de la taille WinRE et l’exécution du partitionnement ne sont pas encore implémentés. + + +## Historique des changements + +### 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. +- L’affichage des disques a d’abord utilisé `InterfaceType`, puis a été corrigé pour afficher `BusType` obtenu par `Get-Disk`. +- Le module VNC réutilise l’adresse IPv4 détectée au démarrage ; la détection réseau n’est plus relancée lors du démarrage de VNC. +- Les messages d’information et d’erreur utilisent maintenant `Show-DeployCenterMessage` dans `Modules/UI.psm1`, avec la palette active, au lieu des boîtes de dialogue natives de Windows. +- Les erreurs de démarrage VNC indiquent maintenant l’étape concernée pour distinguer les échecs de nettoyage, de copie, de pare-feu et de lancement. +- La sortie de `wpeutil DisableFirewall` et son code de retour sont consignés dans le journal DeployCenter au lieu d’apparaître dans la console PowerShell. +- Le libellé de statut de la fenêtre Sources Windows est remonté et sa hauteur réduite pour mieux le séparer du pied de page. +- L’inventaire matériel, l’adresse IP et les informations VNC sont réunis dans `%TEMP%\DeployCenter\DeployCenter_Inventory.json`. Les exports distincts `HardwareInfo.json`, `hardware.json` et `hardware.log` ne sont plus générés. +- Les anciens fichiers temporaires peuvent rester jusqu’au redémarrage de WinPE, qui vide `%TEMP%`. +- La collecte complète des disques utilise maintenant `Get-Disk` : numéro, nom, taille, type de bus, état opérationnel et état de santé. `Win32_DiskDrive` n’est plus utilisé pour l’inventaire des disques. +- La détection IPv4 de `Modules/Network.psm1` utilise les API .NET `System.Net.NetworkInformation` au lieu de `Get-CimInstance Win32_NetworkAdapterConfiguration` ; la priorité aux interfaces avec passerelle et les exclusions d’adresses restent en place. +- Ajout du chargement et de la validation de profils de partitionnement PSD1, ainsi que d’un sélecteur dans l’interface. Le profil UEFI/GPT place EFI, MSR et WinRE avant Windows, qui reçoit l’espace restant ; aucun script DiskPart n’est exécuté à cette étape. +- Ajout du BOM UTF-8 aux nouveaux fichiers PowerShell et PSD1 pour préserver les caractères accentués lors de leur lecture par Windows PowerShell 5.1. +- Correction de l’apostrophe dans la description du profil UEFI/GPT, qui empêchait le chargement du fichier PSD1.