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
@@ -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
|
||||
Reference in new issue
Block a user