Ajout du support de unattend
Ajout du script cmd à utiliser pour lancer DeployCenter
This commit is contained in:
1 parent
2019c565f5
commit
e067721763
12 files changed
+663
-166
No files matched your search
+1
-3
@@ -1,3 +1 @@
|
||||
Logs/*
|
||||
Archives v1/*
|
||||
Archives v2/*
|
||||
DeployCenter.ini
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
Start powershell.exe -executionpolicy Unrestricted -WorkingDirectory y:\deploycenter -Command .\DeployCenter.ps1 -windowed -vnc
|
||||
@@ -13,4 +13,14 @@ Password=
|
||||
|
||||
[Partitioning]
|
||||
; Taille de la partition WinRE en Mo ; défaut : 1024 (1 Go), minimum : 990.
|
||||
; WinRESizeMB=1024
|
||||
; WinRESizeMB=1024
|
||||
|
||||
[Unattend]
|
||||
; Paramètres utilisés par les profils qui créent un compte administrateur local.
|
||||
AdminName=admin
|
||||
AdminDisplayName=Administrateur local
|
||||
; Utiliser les noms de groupes neutres en anglais, reconnus sur toutes les langues Windows.
|
||||
AdminGroup=Administrators
|
||||
; Base64 du mot de passe encodé en UTF-16LE (ce n'est pas un chiffrement).
|
||||
; Laisser vide empêche l'utilisation du profil jusqu'à la configuration d'un mot de passe.
|
||||
AdminPasswordBase64=
|
||||
+78
-14
@@ -124,6 +124,11 @@ if (-not (Test-Path -LiteralPath $deploymentModulePath -PathType Leaf)) {
|
||||
throw "Module de déploiement introuvable : $deploymentModulePath"
|
||||
}
|
||||
Import-Module $deploymentModulePath -Force
|
||||
$unattendModulePath = Join-Path $script:DeployRoot 'Modules\Unattend.psm1'
|
||||
if (-not (Test-Path -LiteralPath $unattendModulePath -PathType Leaf)) {
|
||||
throw "Module Unattend introuvable : $unattendModulePath"
|
||||
}
|
||||
Import-Module $unattendModulePath -Force
|
||||
$script:PartitionProfilesRoot = Join-Path $script:DeployRoot 'Partitioning\Profiles'
|
||||
$script:PartitionProfiles = @()
|
||||
foreach ($profileName in (Get-DeployCenterPartitionProfileNames -ProfilesRoot $script:PartitionProfilesRoot)) {
|
||||
@@ -147,6 +152,7 @@ $script:SelectedPartitionProfile = $script:PartitionProfiles[$script:SelectedPar
|
||||
$script:SelectedPartitionSizing = $null
|
||||
$script:SelectedDiskIndex = $null
|
||||
$script:SelectedWindowsEdition = $null
|
||||
$script:RestartAfterDeployment = $false
|
||||
$script:RefreshingDiskList = $false
|
||||
$script:ChangingPartitionProfileSelection = $false
|
||||
|
||||
@@ -242,6 +248,22 @@ foreach ($themeFileName in $defaultThemeFiles.Keys) {
|
||||
}
|
||||
|
||||
$script:Config = Read-DeployCenterConfig -ConfigPath $script:ConfigPath
|
||||
$script:UnattendProfilesRoot = Join-Path $script:DeployRoot 'Unattend\Profiles'
|
||||
$script:UnattendProfiles = @()
|
||||
foreach ($unattendProfileName in (Get-DeployCenterUnattendProfileNames -ProfilesRoot $script:UnattendProfilesRoot)) {
|
||||
try {
|
||||
$script:UnattendProfiles += Import-DeployCenterUnattendProfile `
|
||||
-ProfilesRoot $script:UnattendProfilesRoot -ProfileName $unattendProfileName
|
||||
} catch {
|
||||
& $script:SourceLogAction "Profil Unattend '$unattendProfileName' ignoré : $($_.Exception.Message)" 'WARNING'
|
||||
}
|
||||
}
|
||||
$duplicateUnattendNames = @($script:UnattendProfiles | Group-Object -Property Name | Where-Object Count -gt 1)
|
||||
foreach ($duplicateName in $duplicateUnattendNames) {
|
||||
& $script:SourceLogAction "Profils Unattend ignorés car leur nom affiché est en double : $($duplicateName.Name)." 'WARNING'
|
||||
$script:UnattendProfiles = @($script:UnattendProfiles | Where-Object { [string]$_.Name -ne [string]$duplicateName.Name })
|
||||
}
|
||||
$script:UnattendOobeChoice = 'Avec OOBE'
|
||||
foreach ($partitionProfile in $script:PartitionProfiles) {
|
||||
foreach ($partition in $partitionProfile.Partitions) {
|
||||
if ([string]$partition.Size.Policy -ne 'ConfiguredMB') { continue }
|
||||
@@ -437,7 +459,7 @@ $script:cmbTheme.BringToFront()
|
||||
# ============================================================
|
||||
|
||||
$script:groupPC = New-GroupBox -Text "Informations du PC" `
|
||||
-X $margin -Y 100 -Width $usableWidth -Height 105 -Theme $script:Theme -Font $script:FontGroup
|
||||
-X $margin -Y 100 -Width $usableWidth -Height 90 -Theme $script:Theme -Font $script:FontGroup
|
||||
|
||||
$script:lblManufacturer = New-Label -Text "Fabricant : -" `
|
||||
-X 18 -Y 25 -Width 330 -Height 24 -Theme $script:Theme -Font $script:FontRegular
|
||||
@@ -449,18 +471,12 @@ $script:lblFirmware = New-Label -Text "Firmware : -" `
|
||||
-X 360 -Y 52 -Width 220 -Height 24 -Theme $script:Theme -Font $script:FontRegular
|
||||
$script:lblArchitecture = New-Label -Text "Architecture : -" `
|
||||
-X 600 -Y 52 -Width 220 -Height 24 -Theme $script:Theme -Font $script:FontRegular
|
||||
$script:lblWinPE = New-Label -Text "Environnement : -" `
|
||||
-X 18 -Y 78 -Width 350 -Height 22 `
|
||||
-Theme $script:Theme -Font $script:FontSmall `
|
||||
-Color $script:Theme.Secondary
|
||||
|
||||
$script:groupPC.Controls.AddRange(@(
|
||||
$script:lblManufacturer,
|
||||
$script:lblModel,
|
||||
$script:lblSerial,
|
||||
$script:lblFirmware,
|
||||
$script:lblArchitecture,
|
||||
$script:lblWinPE
|
||||
$script:lblArchitecture
|
||||
))
|
||||
$script:form.Controls.Add($script:groupPC)
|
||||
|
||||
@@ -469,7 +485,7 @@ $script:form.Controls.Add($script:groupPC)
|
||||
# ============================================================
|
||||
|
||||
$script:groupDisks = New-GroupBox -Text "Préparation du déploiement" `
|
||||
-X $margin -Y 220 -Width $usableWidth -Height 226 -Theme $script:Theme -Font $script:FontGroup
|
||||
-X $margin -Y 205 -Width $usableWidth -Height 270 -Theme $script:Theme -Font $script:FontGroup
|
||||
|
||||
$deploymentControls = New-DeployCenterDeploymentControls `
|
||||
-Parent $script:groupDisks `
|
||||
@@ -481,6 +497,9 @@ $script:btnChooseWindows = $deploymentControls.ChooseSystemButton
|
||||
$script:lblSelectedWindows = $deploymentControls.SelectedSystemLabel
|
||||
$script:lblPartitionProfile = $deploymentControls.PartitionProfileLabel
|
||||
$script:cmbPartitionProfile = $deploymentControls.PartitionProfileComboBox
|
||||
$script:cmbUnattendProfile = $deploymentControls.UnattendProfileComboBox
|
||||
$script:DeploymentControls = $deploymentControls
|
||||
$script:lblHostName = $deploymentControls.HostNameLabel
|
||||
$script:txtHostName = $deploymentControls.HostNameTextBox
|
||||
$script:cmbPartitionProfile.SelectedIndex = $script:SelectedPartitionProfileIndex
|
||||
|
||||
@@ -585,9 +604,18 @@ $script:btnChooseWindows.Add_Click({
|
||||
$script:lblSelectedWindows.ForeColor = $script:Theme.Success
|
||||
$script:lblSelectedWindows.Name = 'lblSelectedWindows'
|
||||
$script:cmbPartitionProfile.Enabled = $true
|
||||
Set-DeployCenterUnattendProfileChoices -ComboBox $script:cmbUnattendProfile `
|
||||
-Profiles $script:UnattendProfiles -EditionName ([string]$edition.Name) `
|
||||
-OobeChoice $script:UnattendOobeChoice
|
||||
}
|
||||
})
|
||||
|
||||
$script:cmbUnattendProfile.Add_SelectedIndexChanged({
|
||||
$showHostName = $script:cmbUnattendProfile.SelectedItem -and
|
||||
[string]$script:cmbUnattendProfile.SelectedItem -ne $script:UnattendOobeChoice
|
||||
Set-DeployCenterUnattendHostNameVisibility -Controls $script:DeploymentControls -ShowHostName ([bool]$showHostName)
|
||||
})
|
||||
|
||||
$script:btnLog.Add_Click({
|
||||
if ($script:logForm.IsDisposed) { return }
|
||||
if (-not $script:logForm.Visible) { $script:logForm.Show($script:form) }
|
||||
@@ -602,7 +630,18 @@ $script:btnDeploy.Add_Click({
|
||||
$diskIndex = $null
|
||||
if ($null -ne $script:cmbDisk.SelectedItem) { $diskIndex = [int]$script:cmbDisk.SelectedItem.Tag }
|
||||
$diskInfo = if ($null -ne $diskIndex) { Get-SelectedDeployDiskInfo -HardwareInfo $script:HardwareInventory -DiskIndex $diskIndex } else { $null }
|
||||
$hostName = [string]$script:txtHostName.Text.Trim()
|
||||
$unattendChoice = [string]$script:cmbUnattendProfile.SelectedItem
|
||||
$unattendProfile = $null
|
||||
$hostName = $null
|
||||
if ($unattendChoice -and $unattendChoice -ne $script:UnattendOobeChoice) {
|
||||
$matchingProfiles = @($script:UnattendProfiles | Where-Object { [string]$_.Name -eq $unattendChoice })
|
||||
if ($matchingProfiles.Count -ne 1) {
|
||||
Show-DeployCenterMessage -Message "Le profil Unattend '$unattendChoice' est introuvable ou ambigu." -Title 'Profil Unattend' -Icon Error -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
|
||||
return
|
||||
}
|
||||
$unattendProfile = $matchingProfiles[0]
|
||||
$hostName = [string]$script:txtHostName.Text.Trim()
|
||||
}
|
||||
|
||||
if ($null -eq $diskInfo) {
|
||||
Show-DeployCenterMessage -Message 'Sélectionne le disque de destination.' -Title 'Déploiement Windows' -Icon Warning -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
|
||||
@@ -612,10 +651,19 @@ $script:btnDeploy.Add_Click({
|
||||
Show-DeployCenterMessage -Message 'Sélectionne une édition Windows à installer.' -Title 'Déploiement Windows' -Icon Warning -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
|
||||
return
|
||||
}
|
||||
if ($hostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') {
|
||||
if ($null -ne $unattendProfile -and $unattendProfile.ComputerName -and $hostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') {
|
||||
Show-DeployCenterMessage -Message "Le nom d’hôte doit contenir de 1 à 15 caractères, commencer et finir par une lettre ou un chiffre, et ne contenir que des lettres, chiffres ou tirets." -Title "Nom d’hôte invalide" -Icon Warning -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
|
||||
return
|
||||
}
|
||||
if ($null -ne $unattendProfile) {
|
||||
try {
|
||||
Test-DeployCenterUnattendConfiguration -Profile $unattendProfile `
|
||||
-Configuration $script:Config -HostName $hostName
|
||||
} catch {
|
||||
Show-DeployCenterMessage -Message $_.Exception.Message -Title 'Configuration Unattend invalide' -Icon Warning -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
|
||||
return
|
||||
}
|
||||
}
|
||||
if ($script:SelectedPartitionSizing -and [int]$script:SelectedPartitionSizing.DiskIndex -ne [int]$diskInfo.Index) {
|
||||
Show-DeployCenterMessage -Message 'La taille des partitions correspond à un autre disque. Sélectionne de nouveau le profil de partitionnement.' -Title 'Partitionnement' -Icon Warning -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
|
||||
return
|
||||
@@ -630,9 +678,10 @@ $script:btnDeploy.Add_Click({
|
||||
$sizeParts += ("espace restant : {0:N1} Go" -f $script:SelectedPartitionSizing.RemainderGB)
|
||||
$sizeDescription = $sizeParts -join ', '
|
||||
}
|
||||
$unattendDescription = if ($null -ne $unattendProfile) { "Profil Unattend : $($unattendProfile.Name)`r`nNom d’hôte : $hostName" } else { 'Configuration initiale : Avec OOBE' }
|
||||
$confirmation = Show-DeployCenterConfirmation `
|
||||
-Title "Confirmer l’effacement du disque" `
|
||||
-Message ("Disque {0} — {1} ({2:N1} Go)`r`nProfil : {3}`r`nÉdition : {4}`r`nTailles : {5}`r`nNom d’hôte : {6}`r`nTout le contenu du disque sera effacé. Cette opération est irréversible." -f $diskInfo.Index, $diskInfo.Model, $diskInfo.SizeGB, $script:SelectedPartitionProfile.Name, $script:SelectedWindowsEdition.Name, $sizeDescription, $hostName) `
|
||||
-Message ("Disque {0} — {1} ({2:N1} Go)`r`nProfil de partitionnement : {3}`r`nÉdition : {4}`r`nTailles : {5}`r`n{6}`r`nTout le contenu du disque sera effacé. Cette opération est irréversible." -f $diskInfo.Index, $diskInfo.Model, $diskInfo.SizeGB, $script:SelectedPartitionProfile.Name, $script:SelectedWindowsEdition.Name, $sizeDescription, $unattendDescription) `
|
||||
-Owner $script:form -Theme $script:Theme -Font $script:FontRegular
|
||||
if (-not $confirmation) { return }
|
||||
|
||||
@@ -672,6 +721,8 @@ $script:btnDeploy.Add_Click({
|
||||
-SizingState $script:SelectedPartitionSizing `
|
||||
-Edition $script:SelectedWindowsEdition `
|
||||
-HostName $hostName `
|
||||
-UnattendProfile $unattendProfile `
|
||||
-UnattendConfiguration $script:Config `
|
||||
-Firmware $script:HardwareInventory.Firmware `
|
||||
-TemporaryRoot $script:InventoryDir `
|
||||
-LogAction $script:SourceLogAction `
|
||||
@@ -679,7 +730,13 @@ $script:btnDeploy.Add_Click({
|
||||
|
||||
$progress.Form.Close()
|
||||
$script:form.Enabled = $true
|
||||
Show-DeployCenterMessage -Message 'Windows a été appliqué. Le déploiement est prêt à être finalisé par le redémarrage de WinPE.' -Title 'Déploiement terminé' -Icon Information -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
|
||||
$restartAfterDeployment = Show-DeployCenterDeploymentComplete `
|
||||
-Message 'Windows a été appliqué avec succès. Le déploiement est prêt.' `
|
||||
-Owner $script:form -Theme $script:Theme -Font $script:FontRegular
|
||||
if ($restartAfterDeployment) {
|
||||
$script:RestartAfterDeployment = $true
|
||||
$script:form.Close()
|
||||
}
|
||||
} catch {
|
||||
if (-not $progress.Form.IsDisposed) { $progress.Form.Close() }
|
||||
$script:form.Enabled = $true
|
||||
@@ -688,7 +745,7 @@ $script:btnDeploy.Add_Click({
|
||||
} finally {
|
||||
if (-not $progress.Form.IsDisposed) { $progress.Form.Dispose() }
|
||||
$script:form.Enabled = $true
|
||||
$script:form.Activate()
|
||||
if (-not $script:form.IsDisposed -and $script:form.Visible) { $script:form.Activate() }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -821,4 +878,11 @@ finally {
|
||||
Stop-DeployCenterVnc
|
||||
}
|
||||
$script:form.Dispose()
|
||||
if ($script:RestartAfterDeployment) {
|
||||
try {
|
||||
Restart-DeployCenterWinPE -LogAction $script:SourceLogAction
|
||||
} catch {
|
||||
& $script:SourceLogAction "Échec de la demande de redémarrage : $($_.Exception.Message)" 'ERROR'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Ignorer tous les fichiers de ce répertoire
|
||||
*
|
||||
|
||||
# Sauf celui-ci
|
||||
!.gitignore
|
||||
@@ -10,6 +10,10 @@ function Read-DeployCenterConfig {
|
||||
SourcePaths = '%DEPLOY_VOLUME%'
|
||||
VncPassword = ''
|
||||
WinRESizeMB = 1024
|
||||
UnattendAdminName = 'admin'
|
||||
UnattendAdminDisplayName = 'Administrateur local'
|
||||
UnattendAdminGroup = 'Administrators'
|
||||
UnattendAdminPasswordBase64 = ''
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) { return $config }
|
||||
|
||||
@@ -34,6 +38,14 @@ function Read-DeployCenterConfig {
|
||||
throw "Le paramètre [Partitioning] WinRESizeMB doit être un entier strictement positif. Valeur reçue : '$value'."
|
||||
}
|
||||
$config.WinRESizeMB = $sizeMB
|
||||
} elseif ($section -ieq 'Unattend' -and $key -ieq 'AdminName') {
|
||||
$config.UnattendAdminName = $value
|
||||
} elseif ($section -ieq 'Unattend' -and $key -ieq 'AdminDisplayName') {
|
||||
$config.UnattendAdminDisplayName = $value
|
||||
} elseif ($section -ieq 'Unattend' -and $key -ieq 'AdminGroup') {
|
||||
$config.UnattendAdminGroup = $value
|
||||
} elseif ($section -ieq 'Unattend' -and $key -ieq 'AdminPasswordBase64') {
|
||||
$config.UnattendAdminPasswordBase64 = $value
|
||||
}
|
||||
}
|
||||
return $config
|
||||
|
||||
+219
-125
@@ -1,11 +1,51 @@
|
||||
# DeployCenter - Partitionnement et installation d'une edition Windows
|
||||
# Compatible Windows PowerShell 5.1 / WinPE.
|
||||
|
||||
Import-Module (Join-Path $PSScriptRoot 'Unattend.psm1') -Force
|
||||
|
||||
function Write-DeployCenterDeploymentLog {
|
||||
param([AllowNull()][scriptblock]$LogAction, [string]$Message, [string]$Level = 'INFO')
|
||||
if ($LogAction) { & $LogAction $Message $Level }
|
||||
}
|
||||
|
||||
function Update-DeployCenterHostStorageCache {
|
||||
param([AllowNull()][scriptblock]$LogAction = $null)
|
||||
|
||||
try {
|
||||
Update-HostStorageCache -ErrorAction Stop
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message 'Cache de stockage WinPE actualisé.' -Level 'INFO'
|
||||
return $true
|
||||
} catch {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Actualisation du cache de stockage WinPE indisponible : {0}" -f $_.Exception.Message) `
|
||||
-Level 'WARNING'
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Restart-DeployCenterWinPE {
|
||||
[CmdletBinding()]
|
||||
param([AllowNull()][scriptblock]$LogAction = $null)
|
||||
|
||||
$wpeutilPath = Join-Path $env:WINDIR 'System32\wpeutil.exe'
|
||||
if (-not (Test-Path -LiteralPath $wpeutilPath -PathType Leaf)) {
|
||||
throw 'wpeutil.exe est introuvable ; le redémarrage WinPE ne peut pas être demandé.'
|
||||
}
|
||||
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message 'Redémarrage de WinPE demandé via wpeutil.' -Level 'INFO'
|
||||
$output = @(& $wpeutilPath Reboot 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
foreach ($line in $output) {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
}
|
||||
}
|
||||
if ($null -ne $exitCode -and $exitCode -ne 0) {
|
||||
throw "wpeutil Reboot a renvoyé le code $exitCode."
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertTo-DeployCenterNativeArgument {
|
||||
param([AllowNull()][string]$Value)
|
||||
|
||||
@@ -109,6 +149,7 @@ function Remove-DeployCenterDismSourceAccess {
|
||||
} catch {
|
||||
$releaseError = $_.Exception
|
||||
}
|
||||
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
|
||||
if (Test-Path -LiteralPath ([string]$Lease.ImagePath) -PathType Leaf) {
|
||||
$details = if ($releaseError) { $releaseError.Message } else { 'Le fichier source reste accessible.' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
@@ -159,6 +200,7 @@ function Invoke-DeployCenterNativeCommand {
|
||||
$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'
|
||||
$isImageApply = $isDism -and @($Arguments | Where-Object { [string]$_ -ieq '/Apply-Image' }).Count -gt 0
|
||||
if ($isDism) {
|
||||
$Arguments += "/LogPath:$dismLogPath"
|
||||
}
|
||||
@@ -168,69 +210,54 @@ function Invoke-DeployCenterNativeCommand {
|
||||
|
||||
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]
|
||||
$nativeOutputEncoding = [System.Console]::OutputEncoding
|
||||
if ($null -eq $nativeOutputEncoding) { $nativeOutputEncoding = [System.Text.Encoding]::Default }
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
if (Test-Path -LiteralPath $outputPath -PathType Leaf) {
|
||||
foreach ($line in [System.IO.File]::ReadAllLines($outputPath, $nativeOutputEncoding)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($line)) {
|
||||
[void]$nativeOutputLines.Add([string]$line)
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
if ($isImageApply) {
|
||||
$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 (Test-Path -LiteralPath $stdoutPath -PathType Leaf) {
|
||||
$nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
|
||||
if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 -and [IO.Path]::GetFileName($FilePath) -ieq 'bcdboot.exe') {
|
||||
$bootOutput = ($nativeOutputLines -join "`n").Normalize([System.Text.NormalizationForm]::FormD) -replace '\p{Mn}', ''
|
||||
$bootFilesCreated = $bootOutput -match '(?i)boot files successfully created|fichiers de d\s*marrage.*bien.*cr'
|
||||
$systemArgumentIndex = [array]::IndexOf($Arguments, '/s')
|
||||
$bcdStoreExists = $false
|
||||
if ($systemArgumentIndex -ge 0 -and $systemArgumentIndex + 1 -lt $Arguments.Count) {
|
||||
$systemRoot = ([string]$Arguments[$systemArgumentIndex + 1]).TrimEnd('\')
|
||||
$bcdStorePath = "${systemRoot}\EFI\Microsoft\Boot\BCD"
|
||||
$bcdStoreExists = Test-Path -LiteralPath $bcdStorePath -PathType Leaf
|
||||
$process.WaitForExit()
|
||||
$process.Refresh()
|
||||
$exitCode = $process.ExitCode
|
||||
$nativeOutputEncoding = [System.Console]::OutputEncoding
|
||||
if ($null -eq $nativeOutputEncoding) { $nativeOutputEncoding = [System.Text.Encoding]::Default }
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
if (Test-Path -LiteralPath $outputPath -PathType Leaf) {
|
||||
foreach ($line in [System.IO.File]::ReadAllLines($outputPath, $nativeOutputEncoding)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($line)) {
|
||||
[void]$nativeOutputLines.Add([string]$line)
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($bootFilesCreated -or $bcdStoreExists) {
|
||||
if ($ProgressAction -and (Test-Path -LiteralPath $stdoutPath -PathType Leaf)) {
|
||||
$nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
|
||||
if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent }
|
||||
}
|
||||
if ($null -eq $exitCode -and @($nativeOutputLines | Where-Object { $_ -match '(?i)completed successfully|a r.ussi' }).Count -gt 0) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message "bcdboot a créé les fichiers de démarrage, mais WinPE n’a pas fourni de code de sortie exploitable. La création est confirmée." `
|
||||
-Message 'DISM confirme que Windows a été appliqué, mais le code de sortie est absent dans WinPE.' `
|
||||
-Level 'WARNING'
|
||||
return
|
||||
}
|
||||
} else {
|
||||
$commandOutput = @(& $FilePath @Arguments 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
foreach ($line in $commandOutput) {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
|
||||
[void]$nativeOutputLines.Add([string]$line)
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($null -eq $exitCode -or [int]$exitCode -ne 0) {
|
||||
$diagnostic = @()
|
||||
@@ -255,6 +282,61 @@ function Invoke-DeployCenterNativeCommand {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DeployCenterOfflineWindowsBootGuid {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$BcdStorePath,
|
||||
[Parameter(Mandatory=$true)][string]$WindowsDriveLetter,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $BcdStorePath -PathType Leaf)) {
|
||||
throw "Magasin BCD introuvable après la création des fichiers de démarrage : $BcdStorePath"
|
||||
}
|
||||
$bcdeditPath = Join-Path $env:WINDIR 'System32\bcdedit.exe'
|
||||
if (-not (Test-Path -LiteralPath $bcdeditPath -PathType Leaf)) { $bcdeditPath = 'bcdedit.exe' }
|
||||
$output = @(& $bcdeditPath /store $BcdStorePath /enum osloader /v 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
foreach ($line in $output) {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
}
|
||||
}
|
||||
if ($null -eq $exitCode -or [int]$exitCode -ne 0) {
|
||||
throw "bcdedit n’a pas pu lire le magasin BCD $BcdStorePath (code $exitCode)."
|
||||
}
|
||||
|
||||
$targetLetter = $WindowsDriveLetter.TrimEnd(':').ToUpperInvariant()
|
||||
$entries = New-Object System.Collections.Generic.List[object]
|
||||
$currentEntry = $null
|
||||
foreach ($lineObject in $output) {
|
||||
$line = [string]$lineObject
|
||||
if ($line -match '^\s*(?:identifier|identificateur)\s+(?<guid>\{[0-9A-Fa-f-]{36}\})\s*$') {
|
||||
if ($null -ne $currentEntry) { $entries.Add($currentEntry) }
|
||||
$currentEntry = [pscustomobject]@{
|
||||
Guid = [string]$Matches['guid']
|
||||
Device = $null
|
||||
OsDevice = $null
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ($null -eq $currentEntry) { continue }
|
||||
if ($line -match '^\s*device\s+partition=(?<letter>[A-Za-z]):\\?\s*$') {
|
||||
$currentEntry.Device = $Matches['letter'].ToUpperInvariant()
|
||||
} elseif ($line -match '^\s*osdevice\s+partition=(?<letter>[A-Za-z]):\\?\s*$') {
|
||||
$currentEntry.OsDevice = $Matches['letter'].ToUpperInvariant()
|
||||
}
|
||||
}
|
||||
if ($null -ne $currentEntry) { $entries.Add($currentEntry) }
|
||||
|
||||
$matchingEntries = @($entries | Where-Object {
|
||||
$_.Device -eq $targetLetter -and $_.OsDevice -eq $targetLetter
|
||||
})
|
||||
if ($matchingEntries.Count -ne 1) {
|
||||
throw "Impossible d’identifier une entrée BCD unique pour Windows sur ${targetLetter}: (entrées correspondantes : $($matchingEntries.Count))."
|
||||
}
|
||||
return [string]$matchingEntries[0].Guid
|
||||
}
|
||||
|
||||
function Get-DeployCenterTemporaryDriveLetters {
|
||||
param([Parameter(Mandatory=$true)][int]$DiskNumber, [Parameter(Mandatory=$true)]$Profile)
|
||||
|
||||
@@ -270,7 +352,6 @@ function Get-DeployCenterTemporaryDriveLetters {
|
||||
if ($targetLetters -contains $letter) { continue }
|
||||
$occupied[$letter] = $true
|
||||
}
|
||||
|
||||
$lettersByRole = @{}
|
||||
foreach ($partition in $Profile.Partitions) {
|
||||
if ([string]$partition.Role -eq 'Reserved') { continue }
|
||||
@@ -343,25 +424,6 @@ function New-DeployCenterPartitionPlan {
|
||||
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(
|
||||
@@ -369,15 +431,18 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||||
[AllowNull()]$SizingState,
|
||||
[Parameter(Mandatory=$true)]$Edition,
|
||||
[Parameter(Mandatory=$true)][string]$HostName,
|
||||
[AllowNull()][string]$HostName = $null,
|
||||
[AllowNull()][System.Collections.IDictionary]$UnattendProfile = $null,
|
||||
[AllowNull()][System.Collections.IDictionary]$UnattendConfiguration = $null,
|
||||
[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 de machine doit respecter les règles NetBIOS et ne pas dépasser 15 caractères.'
|
||||
if ($null -ne $UnattendProfile) {
|
||||
if ($null -eq $UnattendConfiguration) { throw 'La configuration DeployCenter.ini est absente pour le profil Unattend sélectionné.' }
|
||||
Test-DeployCenterUnattendConfiguration -Profile $UnattendProfile -Configuration $UnattendConfiguration -HostName ([string]$HostName)
|
||||
}
|
||||
if ([string]$Firmware -ne [string]$Profile.Firmware) {
|
||||
throw ("Le profil {0} exige le firmware {1}, détecté : {2}." -f $Profile.Name, $Profile.Firmware, $Firmware)
|
||||
@@ -422,7 +487,6 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
|
||||
$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
|
||||
@@ -444,27 +508,51 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
if ([string]$definition.FileSystem) {
|
||||
$scriptLines.Add("format fs=$($definition.FileSystem) quick label=$label")
|
||||
}
|
||||
if ($role -eq 'Recovery' -and $driveLetters.ContainsKey($role)) {
|
||||
$scriptLines.Add("assign letter=$($driveLetters[$role])")
|
||||
}
|
||||
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])") }
|
||||
if ($driveLetters.ContainsKey($role) -and $role -ne 'Recovery') {
|
||||
$scriptLines.Add("assign letter=$($driveLetters[$role])")
|
||||
}
|
||||
}
|
||||
|
||||
$cleanLines = @("select disk $($disk.Number)", 'clean')
|
||||
$cleanPath = Join-Path $TemporaryRoot ("clean_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
|
||||
$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)
|
||||
[System.IO.File]::WriteAllLines($cleanPath, $cleanLines, [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', $cleanPath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
|
||||
|
||||
[System.IO.File]::WriteAllLines($diskpartPath, $scriptLines.ToArray(), [System.Text.Encoding]::ASCII)
|
||||
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $diskpartPath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
|
||||
$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)
|
||||
$recoveryPlanIndex = -1
|
||||
for ($index = 0; $index -lt $partitionPlan.Count; $index++) {
|
||||
if ([string]$partitionPlan[$index].Definition.Role -eq 'Recovery') {
|
||||
$recoveryPlanIndex = $index
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($recoveryPlanIndex -lt 0) { throw 'La partition Recovery est absente du plan exécuté.' }
|
||||
$recoveryPartition = $orderedCreatedPartitions[$recoveryPlanIndex]
|
||||
$recoveryLetter = [string]$driveLetters['Recovery']
|
||||
$recoveryDriveRoot = '{0}:\' -f $recoveryLetter
|
||||
if (-not (Test-Path -LiteralPath $recoveryDriveRoot -PathType Container)) {
|
||||
throw "La lettre temporaire ${recoveryLetter}: de WinRE n’est pas accessible après le partitionnement."
|
||||
}
|
||||
$partitionsByLetter = @{}
|
||||
for ($index = 0; $index -lt $partitionPlan.Count; $index++) {
|
||||
$role = [string]$partitionPlan[$index].Definition.Role
|
||||
@@ -483,7 +571,6 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
$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) {
|
||||
@@ -534,13 +621,18 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
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 ($null -ne $UnattendProfile) {
|
||||
if ($ProgressAction) { & $ProgressAction 'Préparation du fichier de réponses Windows…' }
|
||||
$editionArchitecture = if ($Edition.Edition -and $Edition.Edition.Architecture) { [string]$Edition.Edition.Architecture } else { [string]$Edition.Architecture }
|
||||
$unattendXml = New-DeployCenterUnattendXml -Profile $UnattendProfile `
|
||||
-Configuration $UnattendConfiguration -HostName ([string]$HostName) -Architecture $editionArchitecture
|
||||
[System.IO.File]::WriteAllText($unattendPath, $unattendXml, (New-Object System.Text.UTF8Encoding($false)))
|
||||
$pantherUnattendDirectory = Join-Path $windowsRoot 'Panther\Unattend'
|
||||
New-Item -Path $pantherUnattendDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
Copy-Item -LiteralPath $unattendPath -Destination (Join-Path $pantherUnattendDirectory 'Unattend.xml') -Force -ErrorAction Stop
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Fichier de réponses Windows préparé avec le profil '{0}' (nom d’hôte : {1})." -f $UnattendProfile.Name, $HostName) -Level 'INFO'
|
||||
}
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Configuration du démarrage UEFI…' }
|
||||
Invoke-DeployCenterNativeCommand -FilePath $bcdbootPath -Arguments @(
|
||||
@@ -557,52 +649,54 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
if (-not (Test-Path -LiteralPath $reagentcPath -PathType Leaf)) {
|
||||
throw 'REAgentC.exe est introuvable dans Windows appliqué et dans WinPE.'
|
||||
}
|
||||
$bcdStorePath = "${efiLetter}:\EFI\Microsoft\Boot\BCD"
|
||||
$windowsBootGuid = Get-DeployCenterOfflineWindowsBootGuid -BcdStorePath $bcdStorePath `
|
||||
-WindowsDriveLetter $windowsLetter -LogAction $LogAction
|
||||
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
|
||||
'/SetReImage', '/Path', $recoveryPath, '/Target', $windowsRoot
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
|
||||
'/Enable', '/Target', $windowsRoot
|
||||
'/Enable', '/OSGUID', $windowsBootGuid
|
||||
) -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")
|
||||
$lettersToRemove = @()
|
||||
foreach ($roleToRemove in @('System', 'Recovery')) {
|
||||
$letter = [string]$driveLetters[$roleToRemove]
|
||||
if (-not [string]::IsNullOrWhiteSpace($letter)) { $lettersToRemove += $letter }
|
||||
}
|
||||
$lettersToRemove = @($lettersToRemove | Select-Object -Unique)
|
||||
foreach ($letter in $lettersToRemove) {
|
||||
$partitionToRemove = $partitionsByLetter[$letter]
|
||||
$removeLines.Add("select disk $($disk.Number)")
|
||||
$removeLines.Add("select partition $([int]$partitionToRemove.PartitionNumber)")
|
||||
$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."
|
||||
if ($removeLines.Count -gt 0) {
|
||||
$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
|
||||
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
|
||||
# Get-Partition peut conserver la lettre Recovery dans ses propriétés
|
||||
# après que DiskPart l'a effectivement retirée sous WinPE.
|
||||
$remainingPaths = @($lettersToRemove | Where-Object {
|
||||
[System.IO.Directory]::Exists(('{0}:\' -f $_))
|
||||
})
|
||||
if ($remainingPaths.Count -gt 0) {
|
||||
$remaining = (@($remainingPaths | ForEach-Object { ([string]$_).ToUpperInvariant() } | Select-Object -Unique)) -join ', '
|
||||
throw "DiskPart n’a pas libéré toutes les lettres temporaires EFI/WinRE : $remaining."
|
||||
}
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $removePath -PathType Leaf) { Remove-Item -LiteralPath $removePath -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
$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'
|
||||
$completionMode = if ($null -ne $UnattendProfile) { "profil Unattend '$($UnattendProfile.Name)'" } else { 'Avec OOBE' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Déploiement terminé : Windows {0}, disque {1}, mode {2}." -f $Edition.Name, $disk.Number, $completionMode) -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)) {
|
||||
foreach ($temporaryFile in @($cleanPath, $diskpartPath, $unattendPath)) {
|
||||
if (Test-Path -LiteralPath $temporaryFile -PathType Leaf) { Remove-Item -LiteralPath $temporaryFile -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
@@ -611,4 +705,4 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-DeployCenterWindowsDeployment
|
||||
Export-ModuleMember -Function Invoke-DeployCenterWindowsDeployment, Restart-DeployCenterWinPE
|
||||
+148
-23
@@ -134,9 +134,26 @@ function New-DeployCenterDeploymentControls {
|
||||
$profileSelector = New-DeployCenterPartitionProfileSelector -Parent $Parent -ProfileNames $ProfileNames -Theme $Theme -Font $Font
|
||||
$profileSelector.ComboBox.Enabled = $false
|
||||
|
||||
$unattendLabel = New-Object System.Windows.Forms.Label
|
||||
$unattendLabel.Name = 'lblUnattendProfile'
|
||||
$unattendLabel.Text = 'Configuration de première ouverture'
|
||||
$unattendLabel.Location = New-Object System.Drawing.Point(12, 190)
|
||||
$unattendLabel.Size = New-Object System.Drawing.Size(320, 18)
|
||||
$unattendLabel.Font = $Font
|
||||
$unattendLabel.ForeColor = $Theme.Text
|
||||
$unattendLabel.BackColor = [System.Drawing.Color]::Transparent
|
||||
|
||||
$unattendCombo = New-DeployCenterComboBox -Name 'cmbUnattendProfile' -Theme $Theme -Font $Font `
|
||||
-Items @('Avec OOBE')
|
||||
$unattendCombo.Location = New-Object System.Drawing.Point(12, 207)
|
||||
$unattendCombo.Size = New-Object System.Drawing.Size([Math]::Max(250, $Parent.ClientSize.Width - 24), 26)
|
||||
$unattendCombo.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
|
||||
$unattendCombo.SelectedIndex = 0
|
||||
|
||||
$hostLabel = New-Object System.Windows.Forms.Label
|
||||
$hostLabel.Name = 'lblHostName'
|
||||
$hostLabel.Text = "Nom d’hôte"
|
||||
$hostLabel.Location = New-Object System.Drawing.Point(12, 190)
|
||||
$hostLabel.Location = New-Object System.Drawing.Point(12, 240)
|
||||
$hostLabel.Size = New-Object System.Drawing.Size(220, 24)
|
||||
$hostLabel.Font = $Font
|
||||
$hostLabel.ForeColor = $Theme.Text
|
||||
@@ -144,7 +161,7 @@ function New-DeployCenterDeploymentControls {
|
||||
|
||||
$hostTextBox = New-Object System.Windows.Forms.TextBox
|
||||
$hostTextBox.Name = 'txtHostName'
|
||||
$hostTextBox.Location = New-Object System.Drawing.Point([Math]::Max(244, $Parent.ClientSize.Width - 202), 187)
|
||||
$hostTextBox.Location = New-Object System.Drawing.Point([Math]::Max(244, $Parent.ClientSize.Width - 202), 237)
|
||||
$hostTextBox.Size = New-Object System.Drawing.Size(190, 26)
|
||||
$hostTextBox.MaxLength = 15
|
||||
$hostTextBox.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right
|
||||
@@ -152,17 +169,57 @@ function New-DeployCenterDeploymentControls {
|
||||
$hostTextBox.ForeColor = $Theme.Text
|
||||
$hostTextBox.Font = $Font
|
||||
|
||||
$Parent.Controls.AddRange(@($diskLabel, $diskCombo, $systemLabel, $chooseSystemButton, $selectedSystemLabel, $hostLabel, $hostTextBox))
|
||||
$hostLabel.Visible = $false
|
||||
$hostTextBox.Visible = $false
|
||||
$Parent.Controls.AddRange(@($diskLabel, $diskCombo, $systemLabel, $chooseSystemButton, $selectedSystemLabel, $unattendLabel, $unattendCombo, $hostLabel, $hostTextBox))
|
||||
return [pscustomobject]@{
|
||||
DiskComboBox = $diskCombo
|
||||
ChooseSystemButton = $chooseSystemButton
|
||||
SelectedSystemLabel = $selectedSystemLabel
|
||||
PartitionProfileLabel = $profileSelector.Label
|
||||
PartitionProfileComboBox = $profileSelector.ComboBox
|
||||
UnattendProfileLabel = $unattendLabel
|
||||
UnattendProfileComboBox = $unattendCombo
|
||||
HostNameLabel = $hostLabel
|
||||
HostNameTextBox = $hostTextBox
|
||||
}
|
||||
}
|
||||
|
||||
function Set-DeployCenterUnattendHostNameVisibility {
|
||||
param([Parameter(Mandatory=$true)]$Controls, [Parameter(Mandatory=$true)][bool]$ShowHostName)
|
||||
$Controls.HostNameLabel.Visible = $ShowHostName
|
||||
$Controls.HostNameTextBox.Visible = $ShowHostName
|
||||
}
|
||||
|
||||
function Set-DeployCenterUnattendProfileChoices {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Windows.Forms.ComboBox]$ComboBox,
|
||||
[Parameter(Mandatory=$true)][AllowEmptyCollection()][object[]]$Profiles,
|
||||
[AllowNull()][string]$EditionName,
|
||||
[Parameter(Mandatory=$true)][string]$OobeChoice
|
||||
)
|
||||
|
||||
$isServerEdition = -not [string]::IsNullOrWhiteSpace($EditionName) -and
|
||||
$EditionName.IndexOf('server', [StringComparison]::OrdinalIgnoreCase) -ge 0
|
||||
$target = if ($isServerEdition) { 'Server' } else { 'Workstation' }
|
||||
$choices = @($OobeChoice)
|
||||
if (-not [string]::IsNullOrWhiteSpace($EditionName)) {
|
||||
$choices += @($Profiles | Where-Object { [string]$_.Target -eq $target } | ForEach-Object { [string]$_.Name })
|
||||
}
|
||||
|
||||
$previousChoice = [string]$ComboBox.SelectedItem
|
||||
$ComboBox.BeginUpdate()
|
||||
try {
|
||||
$ComboBox.Items.Clear()
|
||||
foreach ($choice in $choices) { [void]$ComboBox.Items.Add($choice) }
|
||||
$selectionIndex = $ComboBox.Items.IndexOf($previousChoice)
|
||||
if ($selectionIndex -lt 0) { $selectionIndex = 0 }
|
||||
$ComboBox.SelectedIndex = $selectionIndex
|
||||
} finally {
|
||||
$ComboBox.EndUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
function Set-DeployCenterDeploymentControlsLayout {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Windows.Forms.GroupBox]$Parent,
|
||||
@@ -173,7 +230,8 @@ function Set-DeployCenterDeploymentControlsLayout {
|
||||
$Controls.ChooseSystemButton.Location = New-Object System.Drawing.Point([Math]::Max(12, $Parent.ClientSize.Width - 144), 67)
|
||||
$Controls.SelectedSystemLabel.Size = New-Object System.Drawing.Size($width, 22)
|
||||
Set-DeployCenterPartitionProfileSelectorLayout -Label $Controls.PartitionProfileLabel -ComboBox $Controls.PartitionProfileComboBox -Width $width
|
||||
$Controls.HostNameTextBox.Location = New-Object System.Drawing.Point([Math]::Max(244, $Parent.ClientSize.Width - 202), 187)
|
||||
$Controls.UnattendProfileComboBox.Size = New-Object System.Drawing.Size($width, 26)
|
||||
$Controls.HostNameTextBox.Location = New-Object System.Drawing.Point([Math]::Max(244, $Parent.ClientSize.Width - 202), 237)
|
||||
$Controls.HostNameTextBox.Size = New-Object System.Drawing.Size(190, 26)
|
||||
}
|
||||
|
||||
@@ -453,6 +511,89 @@ function Show-DeployCenterMessage {
|
||||
$messageForm.Dispose()
|
||||
}
|
||||
|
||||
function Show-DeployCenterDeploymentComplete {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Message,
|
||||
[Parameter(Mandatory=$true)][System.Windows.Forms.Form]$Owner,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Theme,
|
||||
[Parameter(Mandatory=$true)][System.Drawing.Font]$Font
|
||||
)
|
||||
|
||||
$dialog = New-Object System.Windows.Forms.Form
|
||||
$dialog.Text = 'Déploiement terminé'
|
||||
$dialog.StartPosition = 'CenterParent'
|
||||
$dialog.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
|
||||
$dialog.ShowInTaskbar = $false
|
||||
$dialog.MinimizeBox = $false
|
||||
$dialog.MaximizeBox = $false
|
||||
$dialog.BackColor = $Theme.Background
|
||||
$dialog.ForeColor = $Theme.Text
|
||||
$dialog.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)
|
||||
$dialog.ClientSize = New-Object System.Drawing.Size(470, ($messageHeight + 126))
|
||||
|
||||
$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 = 'i'
|
||||
$iconLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
|
||||
$iconLabel.Font = New-Object System.Drawing.Font('Segoe UI', [single]16, [System.Drawing.FontStyle]::Bold)
|
||||
$iconLabel.ForeColor = $Theme.Accent
|
||||
$iconLabel.BackColor = [System.Drawing.Color]::Transparent
|
||||
$dialog.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
|
||||
$dialog.Controls.Add($messageLabel)
|
||||
|
||||
$restartCheckBox = New-Object System.Windows.Forms.CheckBox
|
||||
$restartCheckBox.Text = 'Redémarrer cet ordinateur après fermeture'
|
||||
$restartCheckBox.Location = New-Object System.Drawing.Point(64, ($messageHeight + 34))
|
||||
$restartCheckBox.Size = New-Object System.Drawing.Size(380, 26)
|
||||
$restartCheckBox.Checked = $true
|
||||
$restartCheckBox.ForeColor = $Theme.Text
|
||||
$restartCheckBox.BackColor = [System.Drawing.Color]::Transparent
|
||||
$restartCheckBox.Font = $Font
|
||||
$dialog.Controls.Add($restartCheckBox)
|
||||
|
||||
$buttonPanel = New-Object System.Windows.Forms.Panel
|
||||
$buttonPanel.Dock = [System.Windows.Forms.DockStyle]::Bottom
|
||||
$buttonPanel.Height = 58
|
||||
$buttonPanel.BackColor = $Theme.PanelAlt
|
||||
$dialog.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)
|
||||
$dialog.AcceptButton = $okButton
|
||||
$dialog.CancelButton = $okButton
|
||||
|
||||
try {
|
||||
$result = $dialog.ShowDialog($Owner)
|
||||
return ($result -eq [System.Windows.Forms.DialogResult]::OK -and $restartCheckBox.Checked)
|
||||
} finally {
|
||||
$dialog.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Show-DeployCenterConfirmation {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Title,
|
||||
@@ -769,13 +910,6 @@ function Set-DeployCenterTheme {
|
||||
$script:lblTitle.ForeColor = $script:Theme.Text
|
||||
$script:lblSubtitle.ForeColor = $script:Theme.Secondary
|
||||
|
||||
if ($script:lblWinPE.Text -like "*WinPE*") {
|
||||
$script:lblWinPE.ForeColor = $script:Theme.Success
|
||||
}
|
||||
else {
|
||||
$script:lblWinPE.ForeColor = $script:Theme.Secondary
|
||||
}
|
||||
|
||||
Set-DeployCenterButtonStyle -Button $script:btnChooseWindows -Theme $script:Theme -Role 'Button'
|
||||
Set-DeployCenterButtonStyle -Button $script:btnLog -Theme $script:Theme -Role 'Button'
|
||||
Set-DeployCenterButtonStyle -Button $script:btnRefresh -Theme $script:Theme -Role 'PrimaryButton'
|
||||
@@ -1388,15 +1522,6 @@ function Update-HardwareView {
|
||||
$script:lblFirmware.Text = "Firmware : $($info.Firmware)"
|
||||
$script:lblArchitecture.Text = "Architecture : $($info.Architecture)"
|
||||
|
||||
if ($info.WinPE) {
|
||||
$script:lblWinPE.Text = "Environnement : WinPE"
|
||||
$script:lblWinPE.ForeColor = $script:Theme.Success
|
||||
}
|
||||
else {
|
||||
$script:lblWinPE.Text = "Environnement : Windows classique"
|
||||
$script:lblWinPE.ForeColor = $script:Theme.Secondary
|
||||
}
|
||||
|
||||
$script:HardwareInventory = $info
|
||||
$previousDiskIndex = $null
|
||||
if ($null -ne $script:cmbDisk.SelectedItem -and $null -ne $script:cmbDisk.SelectedItem.Tag) {
|
||||
@@ -1496,9 +1621,9 @@ function Update-DeployCenterLayout {
|
||||
|
||||
# Les deux panneaux superieurs conservent leur hauteur, mais s'etirent en largeur.
|
||||
$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, 226)
|
||||
$script:groupPC.Size = New-Object System.Drawing.Size($contentWidth, 90)
|
||||
$script:groupDisks.Location = New-Object System.Drawing.Point($m, 205)
|
||||
$script:groupDisks.Size = New-Object System.Drawing.Size($contentWidth, 270)
|
||||
Set-DiskSelectorWidth
|
||||
|
||||
$footerHeight = 26
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# DeployCenter - Découverte et génération des profils de réponses Windows.
|
||||
# Les profils PSD1 restent des fichiers de données ; les réponses sont construites comme XML.
|
||||
|
||||
function Get-DeployCenterUnattendProfileNames {
|
||||
[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-DeployCenterUnattendProfile {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ProfilesRoot,
|
||||
[Parameter(Mandatory=$true)][string]$ProfileName
|
||||
)
|
||||
|
||||
if ($ProfileName -notmatch '^[A-Za-z0-9_-]+$') { throw "Nom de profil Unattend invalide : $ProfileName" }
|
||||
$path = Join-Path $ProfilesRoot ($ProfileName + '.psd1')
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Profil Unattend introuvable : $ProfileName" }
|
||||
$profile = Import-PowerShellDataFile -LiteralPath $path
|
||||
foreach ($key in @('Id','Name','Description','Target','Template','ComputerName','LocalAccount')) {
|
||||
if (-not $profile.ContainsKey($key)) { throw ('Le profil Unattend {0} ne contient pas la cle {1}.' -f $ProfileName, $key) }
|
||||
}
|
||||
if ([string]$profile.Id -ne $ProfileName) { throw ('L identifiant du profil Unattend {0} ne correspond pas au nom du fichier.' -f $ProfileName) }
|
||||
if ([string]$profile.Target -notin @('Workstation', 'Server')) { throw ('La propriete Target du profil Unattend {0} doit etre Workstation ou Server.' -f $ProfileName) }
|
||||
if ([string]$profile.Template -notmatch '^[A-Za-z0-9_-]+\.xml$') { throw ('Nom de template XML invalide dans le profil {0}.' -f $ProfileName) }
|
||||
$templatesRoot = Join-Path (Split-Path -Parent $ProfilesRoot) 'Templates'
|
||||
$templatePath = Join-Path $templatesRoot ([string]$profile.Template)
|
||||
if (-not (Test-Path -LiteralPath $templatePath -PathType Leaf)) { throw "Template XML introuvable pour le profil '$ProfileName' : $templatePath" }
|
||||
if (-not [string]$profile.Name -or [string]::IsNullOrWhiteSpace([string]$profile.Name)) { throw ('Le profil Unattend {0} doit definir un nom affichable.' -f $ProfileName) }
|
||||
$profile.TemplatePath = $templatePath
|
||||
return $profile
|
||||
}
|
||||
|
||||
function Test-DeployCenterUnattendConfiguration {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Configuration,
|
||||
[Parameter(Mandatory=$true)][string]$HostName
|
||||
)
|
||||
|
||||
if ($Profile.ComputerName -and $HostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') {
|
||||
throw 'Le nom de machine doit respecter les règles NetBIOS et ne pas dépasser 15 caractères.'
|
||||
}
|
||||
if ($Profile.LocalAccount) {
|
||||
$accountName = [string]$Configuration.UnattendAdminName
|
||||
$displayName = [string]$Configuration.UnattendAdminDisplayName
|
||||
$groupName = [string]$Configuration.UnattendAdminGroup
|
||||
$passwordBase64 = [string]$Configuration.UnattendAdminPasswordBase64
|
||||
if ($accountName -notmatch '^[^"/\[\]:;|=,+*?<>\\]{1,20}$') { throw 'Le paramètre [Unattend] AdminName doit contenir de 1 à 20 caractères et éviter les caractères interdits par Windows.' }
|
||||
if ([string]::IsNullOrWhiteSpace($displayName)) { throw 'Le paramètre [Unattend] AdminDisplayName ne peut pas être vide.' }
|
||||
if ([string]::IsNullOrWhiteSpace($groupName)) { throw 'Le paramètre [Unattend] AdminGroup ne peut pas être vide.' }
|
||||
if ([string]::IsNullOrWhiteSpace($passwordBase64)) { throw 'Le paramètre [Unattend] AdminPasswordBase64 est requis pour ce profil.' }
|
||||
try {
|
||||
$passwordBytes = [Convert]::FromBase64String($passwordBase64)
|
||||
if ($passwordBytes.Length -eq 0 -or ($passwordBytes.Length % 2) -ne 0 -or
|
||||
[string]::IsNullOrEmpty([Text.Encoding]::Unicode.GetString($passwordBytes))) {
|
||||
throw 'Format UTF-16LE Base64 invalide.'
|
||||
}
|
||||
} catch {
|
||||
throw 'Le paramètre [Unattend] AdminPasswordBase64 doit être un mot de passe UTF-16LE encodé en Base64.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function New-DeployCenterUnattendXml {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Configuration,
|
||||
[Parameter(Mandatory=$true)][string]$HostName,
|
||||
[Parameter(Mandatory=$true)][string]$Architecture
|
||||
)
|
||||
|
||||
Test-DeployCenterUnattendConfiguration -Profile $Profile -Configuration $Configuration -HostName $HostName
|
||||
$document = New-Object System.Xml.XmlDocument
|
||||
$document.PreserveWhitespace = $true
|
||||
$document.Load([string]$Profile.TemplatePath)
|
||||
$namespaces = New-Object System.Xml.XmlNamespaceManager($document.NameTable)
|
||||
$namespaces.AddNamespace('u', 'urn:schemas-microsoft-com:unattend')
|
||||
$namespaces.AddNamespace('wcm', 'http://schemas.microsoft.com/WMIConfig/2002/State')
|
||||
|
||||
$processorArchitecture = switch -Regex ($Architecture.ToLowerInvariant()) {
|
||||
'arm64' { 'arm64'; break }
|
||||
'x86|32' { 'x86'; break }
|
||||
default { 'amd64' }
|
||||
}
|
||||
foreach ($component in $document.SelectNodes('//u:component', $namespaces)) {
|
||||
$component.SetAttribute('processorArchitecture', $processorArchitecture)
|
||||
}
|
||||
if ($Profile.ComputerName) {
|
||||
$node = $document.SelectSingleNode('/u:unattend/u:settings[@pass="specialize"]/u:component[@name="Microsoft-Windows-Shell-Setup"]/u:ComputerName', $namespaces)
|
||||
if ($null -eq $node) { throw ('Le template du profil {0} ne definit pas le noeud ComputerName dans specialize.' -f $Profile.Id) }
|
||||
$node.InnerText = $HostName
|
||||
}
|
||||
if ($Profile.LocalAccount) {
|
||||
$values = @{
|
||||
Name = [string]$Configuration.UnattendAdminName
|
||||
DisplayName = [string]$Configuration.UnattendAdminDisplayName
|
||||
Group = [string]$Configuration.UnattendAdminGroup
|
||||
Password = [string]$Configuration.UnattendAdminPasswordBase64
|
||||
}
|
||||
foreach ($name in $values.Keys) {
|
||||
$field = if ($name -eq 'Password') { 'Password/u:Value' } else { $name }
|
||||
$node = $document.SelectSingleNode("/u:unattend/u:settings[@pass='oobeSystem']/u:component[@name='Microsoft-Windows-Shell-Setup']/u:UserAccounts/u:LocalAccounts/u:LocalAccount/u:$field", $namespaces)
|
||||
if ($null -eq $node) { throw ('Le template du profil {0} ne definit pas le champ de compte {1}.' -f $Profile.Id, $name) }
|
||||
$node.InnerText = $values[$name]
|
||||
}
|
||||
}
|
||||
return $document.OuterXml
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-DeployCenterUnattendProfileNames, Import-DeployCenterUnattendProfile, Test-DeployCenterUnattendConfiguration, New-DeployCenterUnattendXml
|
||||
@@ -6,6 +6,7 @@
|
||||
- `Modules/ThemeManager.psm1` : découverte, chargement et validation des thèmes PSD1, avec prise en charge des rôles de boutons.
|
||||
- `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.
|
||||
- `Modules/Unattend.psm1` : découverte et validation des profils de réponse Windows, puis génération sécurisée du XML.
|
||||
- `Themes/Light.psd1` et `Themes/Dark.psd1` : palettes et styles des boutons.
|
||||
|
||||
## Lancement
|
||||
@@ -52,6 +53,23 @@ La taille configurée par défaut est de 1 Go (1024 Mo). Elle peut être remplac
|
||||
|
||||
## Historique récent
|
||||
|
||||
- Le `clean` et la création des partitions sont exécutés en deux étapes DiskPart ; `Update-HostStorageCache` actualise les objets Storage entre les deux, puis après le partitionnement, les retraits de lettres et le `mountvol /D` de la source, avant les contrôles concernés. Si le cmdlet est indisponible dans WinPE, le déploiement continue avec un avertissement.
|
||||
- Le choix « Avec OOBE » ne génère ni n’applique de fichier de réponses et masque la saisie du nom d’hôte. Les profils `Unattend/Profiles/*.psd1` alimentent le menu de la page principale ; le profil `LocalAdmin` configure le nom de machine et crée un compte local de la configuration `[Unattend]`, sans activer le compte intégré Administrator.
|
||||
- Chaque profil Unattend déclare `Target = 'Workstation'` ou `Target = 'Server'`. Le menu propose les profils correspondant à l’édition retenue (le nom contenant « Server » sélectionne la catégorie serveur) ; avant le choix d’une édition, seul « Avec OOBE » est disponible.
|
||||
- L’indication « Environnement : WinPE » a été retirée de l’écran principal. Le bloc d’informations PC est moins haut et le bloc de préparation est remonté/compacté afin de dégager les boutons d’action sur les petites résolutions.
|
||||
- Le template Workstation renseigne les paramètres régionaux français et répond aux pages OOBE de licence, inscription OEM, compte en ligne, réseau sans fil et paramètres express. Le compte local est fourni dans `UserAccounts`; les paramètres `SkipMachineOOBE` et `SkipUserOOBE` ne sont pas utilisés.
|
||||
- À la fin d’un déploiement réussi, la boîte de confirmation propose le redémarrage WinPE avec une case cochée par défaut. Si elle reste cochée, DeployCenter ferme ses ressources, arrête VNC, démonte l’image source puis demande le redémarrage via `wpeutil`.
|
||||
- Compatibilité Windows PowerShell 5.1 : les chaînes de diagnostic de `Unattend.psm1` évitent les apostrophes intégrées et le module est enregistré en UTF-8 avec BOM.
|
||||
- Le groupe local du compte est configuré avec le nom neutre `Administrators`, que Windows traduit selon la langue de l’image ; cela évite de dépendre du libellé français `Administrateurs`.
|
||||
- Les réponses `specialize` et `oobeSystem` sont placées dans `Windows\Panther\Unattend\Unattend.xml` pour être traitées par Windows Setup au premier démarrage. Le mot de passe du compte local provient de `AdminPasswordBase64` (Base64 UTF-16LE, réversible) et n’est jamais ajouté au journal.
|
||||
- La vérification du retrait des lettres EFI/WinRE s’appuie sur l’existence réelle des chemins de lecteur : dans WinPE, `Get-Partition` peut conserver une lettre Recovery dans ses métadonnées alors que `mountvol` et le système de fichiers confirment son retrait.
|
||||
- La lecture du magasin BCD accepte le libellé français `identificateur` de `bcdedit` en plus du libellé anglais `identifier`, pour retrouver l’entrée Windows hors ligne dans un WinPE francophone.
|
||||
- Correction du pipeline de diagnostic des lettres EFI/WinRE pour rester compatible avec le parseur Windows PowerShell 5.1.
|
||||
- DiskPart attribue la lettre temporaire de WinRE avant d’appliquer son type Recovery et ses attributs GPT, conformément à l’ordre des exemples Microsoft. En fin de déploiement réussi, la lettre est retirée en sélectionnant explicitement la partition ; `Get-Partition` peut ne pas exposer sa lettre dans WinPE.
|
||||
- Correction du texte de diagnostic WinRE pour éviter une apostrophe typographique dans une chaîne délimitée par apostrophes simples sous Windows PowerShell 5.1.
|
||||
- L’activation hors ligne de WinRE utilise désormais le GUID de l’entrée Windows relevé dans le magasin BCD créé par `bcdboot`, conformément aux paramètres REAgentC disponibles dans WinPE.
|
||||
- Le suivi de progression avec `Start-Process` est réservé à `dism.exe /Apply-Image`. Les autres commandes natives, y compris DISM hors application d’image, sont invoquées directement, leurs sorties sont journalisées et leur code est lu dans `$LASTEXITCODE`.
|
||||
- En fin de déploiement réussi, seules les lettres temporaires des partitions EFI et WinRE sont retirées. Les lettres Windows et Data définies par le profil restent attribuées ; en cas d’échec, le nettoyage ne retire aucune lettre pour préserver l’état utile au diagnostic.
|
||||
- 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`.
|
||||
@@ -124,6 +142,7 @@ La gestion d'UltraVNC est isolée dans `Modules/VNC.psm1`. Le fichier est charg
|
||||
- `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/Unattend.psm1` : découverte des profils de réponses Windows, validation de leur configuration et génération du XML utilisé au premier démarrage.
|
||||
- `Modules/WindowsSources.psm1` : recherche de supports Windows et lecture des éditions WIM/ESD.
|
||||
|
||||
## Profils de partitionnement
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@{
|
||||
Id = 'LocalAdmin'
|
||||
Name = 'Compte administrateur local'
|
||||
Description = 'Configure le nom de la machine et crée un compte local membre du groupe des administrateurs.'
|
||||
Target = 'Workstation'
|
||||
Template = 'LocalAdmin.xml'
|
||||
ComputerName = $true
|
||||
LocalAccount = $true
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<settings pass="specialize">
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<ComputerName></ComputerName>
|
||||
</component>
|
||||
</settings>
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<InputLocale>040c:0000040c</InputLocale>
|
||||
<SystemLocale>fr-FR</SystemLocale>
|
||||
<UILanguage>fr-FR</UILanguage>
|
||||
<UserLocale>fr-FR</UserLocale>
|
||||
</component>
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<OOBE>
|
||||
<HideEULAPage>true</HideEULAPage>
|
||||
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||
<ProtectYourPC>3</ProtectYourPC>
|
||||
</OOBE>
|
||||
<UserAccounts>
|
||||
<LocalAccounts>
|
||||
<LocalAccount wcm:action="add">
|
||||
<Password>
|
||||
<Value></Value>
|
||||
<PlainText>false</PlainText>
|
||||
</Password>
|
||||
<DisplayName></DisplayName>
|
||||
<Group></Group>
|
||||
<Name></Name>
|
||||
</LocalAccount>
|
||||
</LocalAccounts>
|
||||
</UserAccounts>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
Reference in new issue
Block a user