Ajout de la partie application des changements sur le disque
Ajout de l'installation de Windows Correction du chargement des images iso. Correction de la définition de la taille de WinRE.
This commit is contained in:
1 parent
1c67cb2630
commit
8d5014b4aa
13 files changed
+2524
-1445
No files matched your search
@@ -0,0 +1,42 @@
|
||||
# DeployCenter - Lecture des préférences utilisateur
|
||||
# Lecture seule : ce module ne modifie jamais DeployCenter.ini.
|
||||
|
||||
function Read-DeployCenterConfig {
|
||||
[CmdletBinding()]
|
||||
param([Parameter(Mandatory=$true)][string]$ConfigPath)
|
||||
|
||||
$config = @{
|
||||
Theme = 'Light'
|
||||
SourcePaths = '%DEPLOY_VOLUME%'
|
||||
VncPassword = ''
|
||||
WinRESizeMB = 1024
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) { return $config }
|
||||
|
||||
$section = ''
|
||||
foreach ($line in (Get-Content -LiteralPath $ConfigPath -ErrorAction SilentlyContinue)) {
|
||||
$trimmed = $line.Trim()
|
||||
if (-not $trimmed -or $trimmed.StartsWith(';') -or $trimmed.StartsWith('#')) { continue }
|
||||
if ($trimmed -match '^\[(.+)\]$') { $section = $matches[1].Trim(); continue }
|
||||
if ($trimmed -notmatch '^([^=]+)=(.*)$') { continue }
|
||||
|
||||
$key = $matches[1].Trim()
|
||||
$value = $matches[2].Trim()
|
||||
if ($section -ieq 'General' -and $key -ieq 'Theme' -and $value -match '^[A-Za-z0-9_-]+$') {
|
||||
$config.Theme = $value
|
||||
} elseif ($section -ieq 'Sources' -and $key -ieq 'Paths') {
|
||||
$config.SourcePaths = $value
|
||||
} elseif ($section -ieq 'UltraVNC' -and $key -ieq 'Password') {
|
||||
$config.VncPassword = $value
|
||||
} elseif ($section -ieq 'Partitioning' -and $key -ieq 'WinRESizeMB') {
|
||||
$sizeMB = 0
|
||||
if (-not [int]::TryParse($value, [ref]$sizeMB) -or $sizeMB -lt 990) {
|
||||
throw "Le paramètre [Partitioning] WinRESizeMB doit être un entier supérieur ou égal à 990. Valeur reçue : '$value'."
|
||||
}
|
||||
$config.WinRESizeMB = $sizeMB
|
||||
}
|
||||
}
|
||||
return $config
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Read-DeployCenterConfig
|
||||
@@ -0,0 +1,577 @@
|
||||
# DeployCenter - Partitionnement et installation d'une edition Windows
|
||||
# Compatible Windows PowerShell 5.1 / WinPE.
|
||||
|
||||
function Write-DeployCenterDeploymentLog {
|
||||
param([AllowNull()][scriptblock]$LogAction, [string]$Message, [string]$Level = 'INFO')
|
||||
if ($LogAction) { & $LogAction $Message $Level }
|
||||
}
|
||||
|
||||
function ConvertTo-DeployCenterNativeArgument {
|
||||
param([AllowNull()][string]$Value)
|
||||
|
||||
if ($null -eq $Value) { $Value = '' }
|
||||
if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') { return $Value }
|
||||
|
||||
$builder = New-Object System.Text.StringBuilder
|
||||
[void]$builder.Append('"')
|
||||
$backslashCount = 0
|
||||
foreach ($character in $Value.ToCharArray()) {
|
||||
if ($character -eq [char]92) {
|
||||
$backslashCount++
|
||||
continue
|
||||
}
|
||||
if ($character -eq [char]34) {
|
||||
if ($backslashCount -gt 0) { [void]$builder.Append(('\' * (2 * $backslashCount))) }
|
||||
[void]$builder.Append('\"')
|
||||
$backslashCount = 0
|
||||
continue
|
||||
}
|
||||
if ($backslashCount -gt 0) { [void]$builder.Append(('\' * $backslashCount)) }
|
||||
[void]$builder.Append($character)
|
||||
$backslashCount = 0
|
||||
}
|
||||
if ($backslashCount -gt 0) { [void]$builder.Append(('\' * (2 * $backslashCount))) }
|
||||
[void]$builder.Append('"')
|
||||
return $builder.ToString()
|
||||
}
|
||||
|
||||
function New-DeployCenterDismSourceAccess {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ImagePath,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
|
||||
$lease = [pscustomobject]@{ ImagePath = $ImagePath; DriveLetter = $null; VolumeRoot = $null }
|
||||
if (-not $ImagePath.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase)) { return $lease }
|
||||
$volumeEnd = $ImagePath.IndexOf('}\', [StringComparison]::OrdinalIgnoreCase)
|
||||
if ($volumeEnd -lt 0) { throw "Chemin de volume GUID invalide pour l’image : $ImagePath" }
|
||||
$volumeRoot = $ImagePath.Substring(0, $volumeEnd + 2)
|
||||
$relativePath = $ImagePath.Substring($volumeEnd + 2)
|
||||
|
||||
$usedLetters = @{}
|
||||
foreach ($volume in @(Get-Volume -ErrorAction SilentlyContinue)) {
|
||||
if ($volume.DriveLetter) { $usedLetters[([string]$volume.DriveLetter).ToUpperInvariant()] = $true }
|
||||
}
|
||||
foreach ($drive in @(Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue)) {
|
||||
$usedLetters[([string]$drive.Name).ToUpperInvariant()] = $true
|
||||
}
|
||||
$letter = $null
|
||||
foreach ($candidate in @('Z','Y','X','W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E')) {
|
||||
if ($candidate -notin @('C','D') -and -not $usedLetters.ContainsKey($candidate)) {
|
||||
$letter = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $letter) { throw 'Aucune lettre temporaire disponible pour accéder à la source Windows.' }
|
||||
|
||||
$mountVolPath = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolPath -PathType Leaf)) { $mountVolPath = 'mountvol.exe' }
|
||||
$letterPath = ("{0}:\{1}" -f $letter, $relativePath)
|
||||
$mountError = $null
|
||||
try {
|
||||
Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $letter), $volumeRoot) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
} catch {
|
||||
$mountError = $_.Exception
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $letterPath -PathType Leaf)) {
|
||||
try {
|
||||
Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $letter), '/D') `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
} catch { }
|
||||
throw "La source Windows n’est pas accessible après l’affectation temporaire de $letter`:."
|
||||
}
|
||||
if ($mountError) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("mountvol n’a pas fourni de code de sortie exploitable, mais la source est accessible via {0}:. L’affectation est confirmée." -f $letter) `
|
||||
-Level 'WARNING'
|
||||
}
|
||||
$lease.ImagePath = $letterPath
|
||||
$lease.DriveLetter = $letter
|
||||
$lease.VolumeRoot = $volumeRoot
|
||||
return $lease
|
||||
}
|
||||
|
||||
function Remove-DeployCenterDismSourceAccess {
|
||||
param(
|
||||
[AllowNull()]$Lease,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
if ($null -eq $Lease -or -not $Lease.DriveLetter) { return }
|
||||
$mountVolPath = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolPath -PathType Leaf)) { $mountVolPath = 'mountvol.exe' }
|
||||
$releaseError = $null
|
||||
try {
|
||||
Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $Lease.DriveLetter), '/D') `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
} catch {
|
||||
$releaseError = $_.Exception
|
||||
}
|
||||
if (Test-Path -LiteralPath ([string]$Lease.ImagePath) -PathType Leaf) {
|
||||
$details = if ($releaseError) { $releaseError.Message } else { 'Le fichier source reste accessible.' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message "Impossible de confirmer la libération de la lettre temporaire $($Lease.DriveLetter): $details" -Level 'WARNING'
|
||||
} elseif ($releaseError) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("mountvol n’a pas fourni de code de sortie exploitable, mais la source n’est plus accessible via {0}:. La libération est confirmée." -f $Lease.DriveLetter) `
|
||||
-Level 'WARNING'
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DeployCenterNativeProgressPercent {
|
||||
param([Parameter(Mandatory=$true)][string]$OutputPath)
|
||||
|
||||
$stream = $null
|
||||
$reader = $null
|
||||
try {
|
||||
$stream = [System.IO.File]::Open($OutputPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
||||
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::Default, $true)
|
||||
$content = $reader.ReadToEnd()
|
||||
} catch {
|
||||
return $null
|
||||
} finally {
|
||||
if ($reader) { $reader.Dispose() }
|
||||
elseif ($stream) { $stream.Dispose() }
|
||||
}
|
||||
|
||||
$progressMatches = [regex]::Matches($content, '(?<![\d.])(?<percent>\d{1,3}(?:\.\d+)?)\s*%')
|
||||
if ($progressMatches.Count -eq 0) { return $null }
|
||||
$value = [double]$progressMatches[$progressMatches.Count - 1].Groups['percent'].Value
|
||||
return [int][math]::Max(0, [math]::Min(100, [math]::Floor($value)))
|
||||
}
|
||||
|
||||
function Invoke-DeployCenterNativeCommand {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$FilePath,
|
||||
[Parameter(Mandatory=$true)][string[]]$Arguments,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null,
|
||||
[AllowNull()][scriptblock]$ProgressAction = $null
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $TemporaryRoot -PathType Container)) {
|
||||
New-Item -Path $TemporaryRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
$id = [guid]::NewGuid().ToString('N')
|
||||
$stdoutPath = Join-Path $TemporaryRoot ("native_{0}.out" -f $id)
|
||||
$stderrPath = Join-Path $TemporaryRoot ("native_{0}.err" -f $id)
|
||||
$dismLogPath = Join-Path $TemporaryRoot ("dism_{0}.log" -f $id)
|
||||
$isDism = [IO.Path]::GetFileName($FilePath) -ieq 'dism.exe'
|
||||
if ($isDism) {
|
||||
$Arguments += "/LogPath:$dismLogPath"
|
||||
}
|
||||
$argumentLine = ($Arguments | ForEach-Object {
|
||||
ConvertTo-DeployCenterNativeArgument -Value ([string]$_)
|
||||
}) -join ' '
|
||||
|
||||
try {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Commande native : {0} {1}" -f $FilePath, $argumentLine)
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $argumentLine `
|
||||
-PassThru -WindowStyle Hidden -RedirectStandardOutput $stdoutPath `
|
||||
-RedirectStandardError $stderrPath -ErrorAction Stop
|
||||
while (-not $process.WaitForExit(250)) {
|
||||
if ($ProgressAction) {
|
||||
[System.Windows.Forms.Application]::DoEvents()
|
||||
if ($isDism -and (Test-Path -LiteralPath $stdoutPath -PathType Leaf)) {
|
||||
$nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
|
||||
if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent }
|
||||
}
|
||||
}
|
||||
}
|
||||
$process.WaitForExit()
|
||||
$process.Refresh()
|
||||
$exitCode = $process.ExitCode
|
||||
$nativeOutputLines = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
if (Test-Path -LiteralPath $outputPath -PathType Leaf) {
|
||||
foreach ($line in [System.IO.File]::ReadAllLines($outputPath)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($line)) {
|
||||
[void]$nativeOutputLines.Add([string]$line)
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($ProgressAction -and $isDism) {
|
||||
$nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
|
||||
if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent }
|
||||
}
|
||||
if ($null -eq $exitCode -and $isDism -and @($nativeOutputLines | Where-Object { $_ -match '(?i)completed successfully|a r.ussi' }).Count -gt 0) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message 'DISM a confirmé la réussite de l’opération, mais WinPE n’a pas fourni de code de sortie exploitable.' `
|
||||
-Level 'WARNING'
|
||||
return
|
||||
}
|
||||
if ($null -eq $exitCode -and [IO.Path]::GetFileName($FilePath) -ieq 'diskpart.exe') {
|
||||
# Dans certains WinPE, Start-Process ne renseigne pas ExitCode même
|
||||
# lorsque DiskPart a terminé. L’appelant vérifie ensuite l’état réel
|
||||
# des partitions et des lettres avant de poursuivre.
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message "DiskPart n'a pas fourni de code de sortie exploitable ; le résultat sera confirmé par vérification du disque." `
|
||||
-Level 'WARNING'
|
||||
return
|
||||
}
|
||||
if ($null -eq $exitCode -or [int]$exitCode -ne 0) {
|
||||
$diagnostic = @()
|
||||
if ($isDism -and (Test-Path -LiteralPath $dismLogPath -PathType Leaf)) {
|
||||
$diagnostic = @(Get-Content -LiteralPath $dismLogPath -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object { [string]$_ })
|
||||
foreach ($line in $diagnostic) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("DISM : {0}" -f $line) -Level 'ERROR'
|
||||
}
|
||||
}
|
||||
throw "La commande '$FilePath' s'est terminée avec le code '$exitCode'. $(($diagnostic -join ' | '))"
|
||||
}
|
||||
} finally {
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
if (Test-Path -LiteralPath $outputPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $outputPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
if (Test-Path -LiteralPath $dismLogPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $dismLogPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DeployCenterTemporaryDriveLetters {
|
||||
param([Parameter(Mandatory=$true)][int]$DiskNumber, [Parameter(Mandatory=$true)]$Profile)
|
||||
|
||||
$targetLetters = @()
|
||||
try {
|
||||
$targetLetters = @(Get-Partition -DiskNumber $DiskNumber -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.DriveLetter } | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() })
|
||||
} catch { }
|
||||
$occupied = @{}
|
||||
foreach ($volume in @(Get-Volume -ErrorAction SilentlyContinue)) {
|
||||
if (-not $volume.DriveLetter) { continue }
|
||||
$letter = ([string]$volume.DriveLetter).ToUpperInvariant()
|
||||
if ($targetLetters -contains $letter) { continue }
|
||||
$occupied[$letter] = $true
|
||||
}
|
||||
|
||||
$lettersByRole = @{}
|
||||
foreach ($partition in $Profile.Partitions) {
|
||||
if ([string]$partition.Role -eq 'Reserved') { continue }
|
||||
$preferred = [string]$partition.DriveLetter
|
||||
if ([string]$partition.Role -eq 'System') { $preferred = 'S' }
|
||||
if ([string]$partition.Role -eq 'Recovery') { $preferred = 'R' }
|
||||
if ([string]::IsNullOrWhiteSpace($preferred)) { $preferred = 'W' }
|
||||
$preferred = $preferred.ToUpperInvariant()
|
||||
if (-not $occupied.ContainsKey($preferred)) {
|
||||
$selected = $preferred
|
||||
} else {
|
||||
$selected = $null
|
||||
foreach ($candidate in @('W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E','D','C')) {
|
||||
if (-not $occupied.ContainsKey($candidate) -and $candidate -notin $lettersByRole.Values) {
|
||||
$selected = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $selected) { throw "Aucune lettre de lecteur libre pour la partition '$($partition.Name)'." }
|
||||
}
|
||||
if ($selected -in $lettersByRole.Values) { throw "La lettre $selected`: est affectée à plusieurs partitions du profil." }
|
||||
$lettersByRole[[string]$partition.Role] = $selected
|
||||
$occupied[$selected] = $true
|
||||
}
|
||||
return $lettersByRole
|
||||
}
|
||||
|
||||
function New-DeployCenterPartitionPlan {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]$DiskInfo,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||||
[AllowNull()]$SizingState
|
||||
)
|
||||
|
||||
if ([string]$Profile.PartitionStyle -ne 'GPT' -or [string]$Profile.Firmware -ne 'UEFI') {
|
||||
throw "Le profil '$($Profile.Name)' n’est pas pris en charge par le moteur actuel (UEFI/GPT requis)."
|
||||
}
|
||||
$totalMB = [long][math]::Floor(([double]$DiskInfo.SizeBytes / 1MB))
|
||||
$availableMB = $totalMB - 2
|
||||
$usedMB = 0L
|
||||
$plan = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($partition in $Profile.Partitions) {
|
||||
$policy = [string]$partition.Size.Policy
|
||||
$sizeMB = $null
|
||||
switch ($policy) {
|
||||
'FixedMB' { $sizeMB = [long]$partition.Size.Value }
|
||||
'ConfiguredMB' { $sizeMB = [long]$partition.Size.Value }
|
||||
'UserInputGB' {
|
||||
if ($null -eq $SizingState -or [string]$SizingState.ProfileId -ne [string]$Profile.Id) {
|
||||
throw "Le profil '$($Profile.Name)' nécessite une taille utilisateur, qui n’a pas été validée."
|
||||
}
|
||||
$role = [string]$partition.Role
|
||||
if (-not $SizingState.Inputs.Contains($role)) { throw "La taille de la partition '$role' est absente." }
|
||||
$sizeMB = [long][math]::Round(([double]$SizingState.Inputs[$role] * 1024), 0)
|
||||
if ($sizeMB -lt ([long]$partition.Size.MinimumGB * 1024)) { throw "La partition '$role' est sous sa taille minimale." }
|
||||
}
|
||||
{ $_ -in @('Remaining','RemainderAfterInputs') } {
|
||||
$sizeMB = $availableMB - $usedMB
|
||||
if ($sizeMB -lt 1) { throw "Le disque ne dispose plus d’espace pour la partition '$($partition.Name)'." }
|
||||
if ($policy -eq 'RemainderAfterInputs' -and $sizeMB -lt ([long]$partition.Size.MinimumGB * 1024)) {
|
||||
throw "L’espace restant pour '$($partition.Name)' est inférieur au minimum du profil."
|
||||
}
|
||||
}
|
||||
default { throw "Politique de taille non prise en charge lors du partitionnement : $policy" }
|
||||
}
|
||||
if ($null -ne $sizeMB) { $usedMB += [long]$sizeMB }
|
||||
$plan.Add([pscustomobject]@{ Definition = $partition; SizeMB = $sizeMB })
|
||||
}
|
||||
if ($usedMB -gt $availableMB) { throw "La taille demandée dépasse la capacité du disque sélectionné." }
|
||||
return $plan.ToArray()
|
||||
}
|
||||
|
||||
function ConvertTo-DeployCenterUnattendXml {
|
||||
param([Parameter(Mandatory=$true)][string]$HostName, [Parameter(Mandatory=$true)][string]$Architecture)
|
||||
$processorArchitecture = switch -Regex ($Architecture.ToLowerInvariant()) {
|
||||
'arm64' { 'arm64'; break }
|
||||
'x86|32' { 'x86'; break }
|
||||
default { 'amd64' }
|
||||
}
|
||||
return @"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||
<settings pass="specialize">
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="$processorArchitecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<ComputerName>$HostName</ComputerName>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
"@
|
||||
}
|
||||
|
||||
function Invoke-DeployCenterWindowsDeployment {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]$DiskInfo,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||||
[AllowNull()]$SizingState,
|
||||
[Parameter(Mandatory=$true)]$Edition,
|
||||
[Parameter(Mandatory=$true)][string]$HostName,
|
||||
[Parameter(Mandatory=$true)][string]$Firmware,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null,
|
||||
[AllowNull()][scriptblock]$ProgressAction = $null
|
||||
)
|
||||
|
||||
if ($HostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') {
|
||||
throw "Le nom d'hôte doit respecter les règles NetBIOS et ne pas dépasser 15 caractères."
|
||||
}
|
||||
if ([string]$Firmware -ne [string]$Profile.Firmware) {
|
||||
throw "Le profil '$($Profile.Name)' exige le firmware $($Profile.Firmware), détecté : $Firmware."
|
||||
}
|
||||
foreach ($requiredRole in @('System','Reserved','Recovery','Windows')) {
|
||||
$roleCount = @($Profile.Partitions | Where-Object { [string]$_.Role -eq $requiredRole }).Count
|
||||
if ($roleCount -ne 1) { throw "Le profil doit définir exactement une partition de rôle '$requiredRole'. Le disque n’a pas été modifié." }
|
||||
}
|
||||
$recoveryDefinitions = @($Profile.Partitions | Where-Object { [string]$_.Role -eq 'Recovery' })
|
||||
if ($recoveryDefinitions.Count -ne 1 -or [string]$recoveryDefinitions[0].Size.Policy -ne 'ConfiguredMB') {
|
||||
throw "Le profil doit définir une taille configurée pour la partition Recovery. Le disque n’a pas été modifié."
|
||||
}
|
||||
$winrePartitionSizeMB = [long]$recoveryDefinitions[0].Size.Value
|
||||
if ($winrePartitionSizeMB -lt 990) {
|
||||
throw "La partition WinRE doit être configurée à au moins 990 Mo. Le disque n’a pas été modifié."
|
||||
}
|
||||
if (-not $Edition.ImagePath -or -not (Test-Path -LiteralPath ([string]$Edition.ImagePath) -PathType Leaf)) {
|
||||
throw "Le chemin de l’image Windows sélectionnée est introuvable."
|
||||
}
|
||||
if (-not $Edition.Index -or [int]$Edition.Index -lt 1) { throw "L’index de l’édition Windows est invalide." }
|
||||
|
||||
$disk = Get-Disk -Number ([int]$DiskInfo.Index) -ErrorAction Stop
|
||||
if ($disk.IsBoot -or $disk.IsSystem) { throw 'Le disque sélectionné héberge le système WinPE et ne peut pas être effacé.' }
|
||||
if ($disk.IsOffline) { [void](Set-Disk -Number $disk.Number -IsOffline $false -ErrorAction Stop) }
|
||||
if ($disk.IsReadOnly) { [void](Set-Disk -Number $disk.Number -IsReadOnly $false -ErrorAction Stop) }
|
||||
|
||||
if (-not (Test-Path -LiteralPath $TemporaryRoot -PathType Container)) {
|
||||
New-Item -Path $TemporaryRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
$sourceLease = New-DeployCenterDismSourceAccess -ImagePath ([string]$Edition.ImagePath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
try {
|
||||
$deploymentImagePath = [string]$sourceLease.ImagePath
|
||||
$partitionSizingDisk = [pscustomobject]@{ Index = [int]$disk.Number; SizeBytes = [long]$disk.Size }
|
||||
$partitionPlan = @(New-DeployCenterPartitionPlan -DiskInfo $partitionSizingDisk -Profile $Profile -SizingState $SizingState)
|
||||
$driveLetters = Get-DeployCenterTemporaryDriveLetters -DiskNumber ([int]$disk.Number) -Profile $Profile
|
||||
|
||||
$dismPath = Join-Path $env:WINDIR 'System32\dism.exe'
|
||||
if (-not (Test-Path -LiteralPath $dismPath -PathType Leaf)) { $dismPath = 'dism.exe' }
|
||||
$bcdbootPath = Join-Path $env:WINDIR 'System32\bcdboot.exe'
|
||||
if (-not (Test-Path -LiteralPath $bcdbootPath -PathType Leaf)) { $bcdbootPath = 'bcdboot.exe' }
|
||||
$reagentcPath = Join-Path $env:WINDIR 'System32\reagentc.exe'
|
||||
if (-not (Test-Path -LiteralPath $reagentcPath -PathType Leaf)) { $reagentcPath = 'reagentc.exe' }
|
||||
|
||||
$scriptLines = New-Object System.Collections.Generic.List[string]
|
||||
$scriptLines.Add("select disk $($disk.Number)")
|
||||
$scriptLines.Add('clean')
|
||||
$scriptLines.Add('convert gpt')
|
||||
foreach ($item in $partitionPlan) {
|
||||
$definition = $item.Definition
|
||||
$role = [string]$definition.Role
|
||||
switch ($role) {
|
||||
'System' { $scriptLines.Add("create partition efi size=$($item.SizeMB)") }
|
||||
'Reserved' { $scriptLines.Add("create partition msr size=$($item.SizeMB)") }
|
||||
default {
|
||||
if ($null -eq $item.SizeMB -or [long]$item.SizeMB -ge ([long][math]::Floor(([double]$disk.Size / 1MB)) - 2)) {
|
||||
$scriptLines.Add('create partition primary')
|
||||
} else {
|
||||
$scriptLines.Add("create partition primary size=$($item.SizeMB)")
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($role -eq 'Reserved') { continue }
|
||||
$label = [string]$definition.Name
|
||||
if ($label -notmatch '^[A-Za-z0-9_-]{1,32}$') { throw "Nom de partition invalide dans le profil : $label" }
|
||||
if ([string]$definition.FileSystem) {
|
||||
$scriptLines.Add("format fs=$($definition.FileSystem) quick label=$label")
|
||||
}
|
||||
if ($definition.GptTypeId) { $scriptLines.Add("set id=$($definition.GptTypeId)") }
|
||||
if ($definition.GptAttributes) { $scriptLines.Add("gpt attributes=$($definition.GptAttributes)") }
|
||||
if ($driveLetters.ContainsKey($role)) { $scriptLines.Add("assign letter=$($driveLetters[$role])") }
|
||||
}
|
||||
|
||||
$diskpartPath = Join-Path $TemporaryRoot ("partition_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
|
||||
$unattendPath = Join-Path $TemporaryRoot ("unattend_{0}.xml" -f ([guid]::NewGuid().ToString('N')))
|
||||
$partitionCreated = $false
|
||||
$lettersReleased = $false
|
||||
try {
|
||||
[System.IO.File]::WriteAllLines($diskpartPath, $scriptLines.ToArray(), [System.Text.Encoding]::ASCII)
|
||||
if ($ProgressAction) { & $ProgressAction 'Effacement et partitionnement du disque…' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Partitionnement du disque {0} avec le profil '{1}' ; partition WinRE configurée : {2} Mo." -f $disk.Number, $Profile.Name, $winrePartitionSizeMB)
|
||||
$partitionCreated = $true
|
||||
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $diskpartPath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
$createdPartitions = @(Get-Partition -DiskNumber ([int]$disk.Number) -ErrorAction Stop)
|
||||
if ($createdPartitions.Count -ne $partitionPlan.Count) {
|
||||
throw "DiskPart a créé $($createdPartitions.Count) partition(s), alors que le profil en prévoit $($partitionPlan.Count). L’installation est interrompue."
|
||||
}
|
||||
$orderedCreatedPartitions = @($createdPartitions | Sort-Object -Property PartitionNumber)
|
||||
$partitionsByLetter = @{}
|
||||
for ($index = 0; $index -lt $partitionPlan.Count; $index++) {
|
||||
$role = [string]$partitionPlan[$index].Definition.Role
|
||||
if ($role -eq 'Reserved') { continue }
|
||||
$letter = [string]$driveLetters[$role]
|
||||
$driveRoot = '{0}:\' -f $letter
|
||||
if (-not (Test-Path -LiteralPath $driveRoot -PathType Container)) {
|
||||
throw "La lettre temporaire $letter`: n’est pas accessible après le partitionnement. L’installation est interrompue."
|
||||
}
|
||||
# WinPE peut ne pas exposer les partitions EFI/Recovery dans les
|
||||
# requêtes Get-Partition par lettre. Leur ordre sur le disque suit
|
||||
# directement celui des définitions exécutées par DiskPart.
|
||||
$partitionsByLetter[$letter] = $orderedCreatedPartitions[$index]
|
||||
}
|
||||
|
||||
$windowsLetter = [string]$driveLetters['Windows']
|
||||
$windowsPath = "${windowsLetter}:\"
|
||||
$windowsRoot = Join-Path $windowsPath 'Windows'
|
||||
$recoveryLetter = [string]$driveLetters['Recovery']
|
||||
$recoveryPath = "${recoveryLetter}:\Recovery\WindowsRE"
|
||||
$efiLetter = [string]$driveLetters['System']
|
||||
if (-not $windowsLetter -or -not $recoveryLetter -or -not $efiLetter) {
|
||||
throw "Le profil doit définir les rôles System, Windows et Recovery pour permettre l’installation UEFI avec WinRE."
|
||||
}
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction "Application de Windows sur $windowsLetter`:…" }
|
||||
Invoke-DeployCenterNativeCommand -FilePath $dismPath -Arguments @(
|
||||
'/Apply-Image', "/ImageFile:$deploymentImagePath", "/Index:$([int]$Edition.Index)", "/ApplyDir:$windowsPath", '/CheckIntegrity'
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Vérification de la capacité de la partition WinRE…' }
|
||||
$sourceWinre = Join-Path $windowsRoot 'System32\Recovery\winre.wim'
|
||||
if (-not (Test-Path -LiteralPath $sourceWinre -PathType Leaf)) {
|
||||
throw "winre.wim est absent de l’image Windows appliquée ; impossible de configurer WinRE."
|
||||
}
|
||||
$winreFile = Get-Item -LiteralPath $sourceWinre -ErrorAction Stop
|
||||
$recoveryPartition = $partitionsByLetter[$recoveryLetter]
|
||||
$recoveryDriveInfo = New-Object System.IO.DriveInfo -ArgumentList "${recoveryLetter}:\"
|
||||
if (-not $recoveryDriveInfo.IsReady) {
|
||||
throw "Le volume de récupération $recoveryLetter`: n’est pas prêt pour vérifier l’espace WinRE."
|
||||
}
|
||||
$minimumPartitionBytes = [long](990 * 1MB)
|
||||
$minimumFreeBytes = [long]($winreFile.Length + (250 * 1MB))
|
||||
$recoveryFreeBytes = [long]$recoveryDriveInfo.AvailableFreeSpace
|
||||
$expectedFreeBytes = $recoveryFreeBytes - [long]$winreFile.Length
|
||||
if ([long]$recoveryPartition.Size -lt $minimumPartitionBytes -or
|
||||
$recoveryFreeBytes -lt $minimumFreeBytes) {
|
||||
$winreSizeForLog = [math]::Ceiling(([double]$winreFile.Length / 1MB))
|
||||
$availableAfterCopyMB = [math]::Floor(([double]$expectedFreeBytes / 1MB))
|
||||
$message = "La partition WinRE configurée à $winrePartitionSizeMB Mo ne respecte pas les prérequis Microsoft : taille de winre.wim $winreSizeForLog Mo, espace libre requis après copie 250 Mo, estimé $availableAfterCopyMB Mo. L’installation est interrompue avant l’activation de WinRE."
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message $message -Level 'ERROR'
|
||||
throw $message
|
||||
}
|
||||
$winreSizeForLog = [math]::Ceiling(([double]$winreFile.Length / 1MB))
|
||||
$availableAfterCopyMB = [math]::Floor(([double]$expectedFreeBytes / 1MB))
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Vérification WinRE réussie : partition {0} Mo, winre.wim {1} Mo, espace restant estimé après copie {2} Mo." -f $winrePartitionSizeMB, $winreSizeForLog, $availableAfterCopyMB) -Level 'SUCCESS'
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction "Configuration du nom d’hôte…" }
|
||||
$editionArchitecture = if ($Edition.Edition -and $Edition.Edition.Architecture) { [string]$Edition.Edition.Architecture } else { [string]$Edition.Architecture }
|
||||
$unattendXml = ConvertTo-DeployCenterUnattendXml -HostName $HostName -Architecture $editionArchitecture
|
||||
[System.IO.File]::WriteAllText($unattendPath, $unattendXml, (New-Object System.Text.UTF8Encoding($false)))
|
||||
Invoke-DeployCenterNativeCommand -FilePath $dismPath -Arguments @(
|
||||
"/Image:$windowsPath", "/Apply-Unattend:$unattendPath"
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Configuration du démarrage UEFI…' }
|
||||
Invoke-DeployCenterNativeCommand -FilePath $bcdbootPath -Arguments @(
|
||||
("{0}:\Windows" -f $windowsLetter), '/s', ("{0}:" -f $efiLetter), '/f', 'UEFI'
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Configuration de la partition de récupération WinRE…' }
|
||||
New-Item -Path $recoveryPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
Copy-Item -LiteralPath $sourceWinre -Destination (Join-Path $recoveryPath 'winre.wim') -Force -ErrorAction Stop
|
||||
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
|
||||
'/SetReImage', '/Path', $recoveryPath, '/Target', $windowsRoot
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
|
||||
'/Enable', '/Target', $windowsRoot
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Finalisation des partitions…' }
|
||||
$removeLines = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($letter in @($driveLetters.Values | Select-Object -Unique)) {
|
||||
$removeLines.Add("select volume $letter")
|
||||
$removeLines.Add("remove letter=$letter")
|
||||
}
|
||||
$removePath = Join-Path $TemporaryRoot ("cleanup_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
|
||||
try {
|
||||
[System.IO.File]::WriteAllLines($removePath, $removeLines.ToArray(), [System.Text.Encoding]::ASCII)
|
||||
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $removePath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
$remainingLetters = @(
|
||||
Get-Partition -DiskNumber ([int]$disk.Number) -ErrorAction Stop |
|
||||
Where-Object { $_.DriveLetter -and ([string]$_.DriveLetter).ToUpperInvariant() -in @($driveLetters.Values | ForEach-Object { ([string]$_).ToUpperInvariant() }) }
|
||||
)
|
||||
if ($remainingLetters.Count -gt 0) {
|
||||
$remaining = @($remainingLetters | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() } | Select-Object -Unique) -join ', '
|
||||
throw "DiskPart n’a pas libéré toutes les lettres temporaires du disque : $remaining."
|
||||
}
|
||||
$lettersReleased = $true
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $removePath -PathType Leaf) { Remove-Item -LiteralPath $removePath -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Déploiement terminé : Windows {0}, disque {1}, hôte {2}." -f $Edition.Name, $disk.Number, $HostName) -Level 'SUCCESS'
|
||||
} finally {
|
||||
if ($partitionCreated -and -not $lettersReleased) {
|
||||
# En cas d’échec après création, libérer les lettres temporaires même si
|
||||
# l’image n’a pas pu être entièrement déployée.
|
||||
foreach ($letter in @($driveLetters.Values | Select-Object -Unique)) {
|
||||
try {
|
||||
$cleanup = Join-Path $TemporaryRoot ("release_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
|
||||
[System.IO.File]::WriteAllLines($cleanup, @("select volume $letter", "remove letter=$letter"), [System.Text.Encoding]::ASCII)
|
||||
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $cleanup) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
Remove-Item -LiteralPath $cleanup -Force -ErrorAction SilentlyContinue
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
foreach ($temporaryFile in @($diskpartPath, $unattendPath)) {
|
||||
if (Test-Path -LiteralPath $temporaryFile -PathType Leaf) { Remove-Item -LiteralPath $temporaryFile -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Remove-DeployCenterDismSourceAccess -Lease $sourceLease -TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-DeployCenterWindowsDeployment
|
||||
@@ -0,0 +1,151 @@
|
||||
|
||||
|
||||
function Get-HardwareInfo {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$info = [ordered]@{
|
||||
Manufacturer = "Inconnu"
|
||||
Model = "Inconnu"
|
||||
SerialNumber = "Inconnu"
|
||||
Firmware = "Inconnu"
|
||||
Architecture = $env:PROCESSOR_ARCHITECTURE
|
||||
WinPE = $false
|
||||
Disks = @()
|
||||
Errors = @()
|
||||
}
|
||||
|
||||
# Fabricant et modele du PC
|
||||
try {
|
||||
$computer = Get-CimInstance `
|
||||
-ClassName Win32_ComputerSystem `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($computer.Manufacturer) {
|
||||
$info.Manufacturer = $computer.Manufacturer.Trim()
|
||||
}
|
||||
|
||||
if ($computer.Model) {
|
||||
$info.Model = $computer.Model.Trim()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Modele du PC : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Numero de serie
|
||||
try {
|
||||
$bios = Get-CimInstance `
|
||||
-ClassName Win32_BIOS `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($bios.SerialNumber) {
|
||||
$info.SerialNumber = $bios.SerialNumber.Trim()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Numero de serie : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Detection du firmware
|
||||
try {
|
||||
$firmwareKey = Get-ItemProperty `
|
||||
-Path "HKLM:\SYSTEM\CurrentControlSet\Control" `
|
||||
-Name PEFirmwareType `
|
||||
-ErrorAction Stop
|
||||
|
||||
switch ([int]$firmwareKey.PEFirmwareType) {
|
||||
1 { $info.Firmware = "BIOS (Legacy)" }
|
||||
2 { $info.Firmware = "UEFI" }
|
||||
default { $info.Firmware = "Indetermine" }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Mode firmware indisponible"
|
||||
}
|
||||
|
||||
# Detection de WinPE
|
||||
$info.WinPE = Test-Path `
|
||||
"HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT"
|
||||
|
||||
# Inventaire des disques physiques
|
||||
try {
|
||||
$disks = @(Get-Disk -ErrorAction Stop)
|
||||
|
||||
foreach ($disk in $disks) {
|
||||
$operationalStatus = @($disk.OperationalStatus | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
$status = if ($operationalStatus.Count -gt 0) { $operationalStatus -join ', ' } else { 'Inconnu' }
|
||||
|
||||
$info.Disks += [PSCustomObject]@{
|
||||
Index = [int]$disk.Number
|
||||
Model = [string]$disk.FriendlyName
|
||||
SizeBytes = [long]$disk.Size
|
||||
SizeGB = [math]::Round(
|
||||
([double]$disk.Size / 1GB), 1
|
||||
)
|
||||
BusType = [string]$disk.BusType
|
||||
Status = $status
|
||||
HealthStatus = [string]$disk.HealthStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Inventaire des disques : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Retourner un objet exploitable par les autres modules
|
||||
[PSCustomObject]$info
|
||||
}
|
||||
|
||||
function Write-DeployInventory {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$InventoryDir,
|
||||
[Parameter(Mandatory=$true)][string]$InventoryFile,
|
||||
[AllowNull()]$HardwareInfo,
|
||||
[AllowNull()][string]$DeploymentIPv4,
|
||||
[AllowNull()][string]$VncPassword,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $InventoryDir -PathType Container)) {
|
||||
New-Item -Path $InventoryDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$inventory = [ordered]@{
|
||||
GeneratedAt = (Get-Date).ToString('o')
|
||||
DeploymentIPv4 = $DeploymentIPv4
|
||||
VncPassword = $VncPassword
|
||||
Hardware = $HardwareInfo
|
||||
}
|
||||
$json = $inventory | ConvertTo-Json -Depth 8
|
||||
Set-Content -LiteralPath $InventoryFile -Value $json -Encoding UTF8
|
||||
|
||||
# Nettoyer les anciens exports séparés après l'écriture de l'inventaire consolidé.
|
||||
$legacyInventoryFiles = @(
|
||||
(Join-Path $env:TEMP 'HardwareInfo.json'),
|
||||
(Join-Path $InventoryDir 'hardware.json')
|
||||
)
|
||||
foreach ($legacyFile in $legacyInventoryFiles) {
|
||||
if (Test-Path -LiteralPath $legacyFile -PathType Leaf) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $legacyFile -Force -ErrorAction Stop
|
||||
} catch {
|
||||
if ($LogAction) { & $LogAction "Impossible de supprimer l'ancien export d'inventaire '$legacyFile' : $($_.Exception.Message)" 'WARNING' }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if ($LogAction) { & $LogAction "Impossible d'écrire l'inventaire temporaire : $($_.Exception.Message)" 'WARNING' }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SelectedDeployDiskInfo {
|
||||
param(
|
||||
[AllowNull()]$HardwareInfo,
|
||||
[Parameter(Mandatory=$true)][int]$DiskIndex
|
||||
)
|
||||
if ($null -eq $HardwareInfo) { return $null }
|
||||
$matches = @($HardwareInfo.Disks | Where-Object { [int]$_.Index -eq $DiskIndex } | Select-Object -First 1)
|
||||
if ($matches.Count -gt 0) { return $matches[0] }
|
||||
return $null
|
||||
}
|
||||
Export-ModuleMember -Function Get-HardwareInfo, Write-DeployInventory, Get-SelectedDeployDiskInfo
|
||||
@@ -56,6 +56,12 @@ function Import-DeployCenterPartitionProfile {
|
||||
if ([int]$partition.Size.FreeSpaceMB -lt 250) { throw "La marge libre WinRE du profil '$ProfileName' doit être d'au moins 250 Mo." }
|
||||
if ([int]$partition.Size.PreviewMB -le 0) { throw "Le profil '$ProfileName' doit définir une réserve PreviewMB pour estimer la capacité disponible." }
|
||||
}
|
||||
'ConfiguredMB' {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$partition.Size.ConfigKey) -or
|
||||
[int]$partition.Size.Value -lt 990 -or [int]$partition.Size.PreviewMB -lt 990) {
|
||||
throw "La taille configurée de la partition '$($partition.Name)' du profil '$ProfileName' doit définir ConfigKey et une valeur d’au moins 990 Mo."
|
||||
}
|
||||
}
|
||||
'UserInputGB' {
|
||||
if ([int]$partition.Size.MinimumGB -le 0 -or [int]$partition.Size.DefaultPercent -le 0 -or [int]$partition.Size.DefaultPercent -ge 100) {
|
||||
throw "La saisie de taille utilisateur du profil '$ProfileName' doit définir MinimumGB et un DefaultPercent entre 1 et 99."
|
||||
@@ -85,7 +91,7 @@ function Get-DeployCenterPartitionSizingCapacity {
|
||||
$reservedMB = 0.0
|
||||
foreach ($partition in $Profile.Partitions) {
|
||||
switch ([string]$partition.Size.Policy) {
|
||||
'FixedMB' { $reservedMB += [double]$partition.Size.Value }
|
||||
{ $_ -in @('FixedMB','ConfiguredMB') } { $reservedMB += [double]$partition.Size.Value }
|
||||
default {
|
||||
if ($partition.Size.ContainsKey('PreviewMB')) { $reservedMB += [double]$partition.Size.PreviewMB }
|
||||
}
|
||||
@@ -101,4 +107,21 @@ function Get-DeployCenterPartitionSizingCapacity {
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-DeployCenterPartitionProfileNames, Import-DeployCenterPartitionProfile, Get-DeployCenterPartitionSizingCapacity
|
||||
function New-DeployCenterPartitionSizingState {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||||
[Parameter(Mandatory=$true)]$DiskInfo,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Selection
|
||||
)
|
||||
return [pscustomobject]@{
|
||||
ProfileId = [string]$Profile.Id
|
||||
DiskIndex = [int]$DiskInfo.Index
|
||||
Inputs = $Selection.Inputs
|
||||
RemainderRole = [string]$Selection.RemainderRole
|
||||
RemainderGB = [double]$Selection.RemainderGB
|
||||
AvailableGB = [double]$Selection.AvailableGB
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-DeployCenterPartitionProfileNames, Import-DeployCenterPartitionProfile, Get-DeployCenterPartitionSizingCapacity, New-DeployCenterPartitionSizingState
|
||||
+1067
-2
File diff suppressed because it is too large.
Load diff
+13
-13
@@ -9,22 +9,22 @@ function Start-DeployCenterVnc {
|
||||
$sourceIni = Join-Path $sourceVnc 'ultravnc.ini'
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($script:DeployIPv4)) {
|
||||
Write-Log 'Aucune adresse IPv4 de déploiement détectée.' 'WARNING'
|
||||
& $script:SourceLogAction 'Aucune adresse IPv4 de déploiement détectée.' 'WARNING'
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $sourceExe -PathType Leaf)) {
|
||||
Write-Log "UltraVNC non démarré : exécutable absent ($sourceExe)." 'WARNING'
|
||||
Write-DeployInventory
|
||||
& $script:SourceLogAction "UltraVNC non démarré : exécutable absent ($sourceExe)." 'WARNING'
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $passwordTool -PathType Leaf)) {
|
||||
Write-Log "UltraVNC non démarré : createpassword64.exe absent ($passwordTool)." 'WARNING'
|
||||
Write-DeployInventory
|
||||
& $script:SourceLogAction "UltraVNC non démarré : createpassword64.exe absent ($passwordTool)." 'WARNING'
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $sourceIni -PathType Leaf)) {
|
||||
Write-Log "UltraVNC non démarré : fichier absent ($sourceIni)." 'WARNING'
|
||||
Write-DeployInventory
|
||||
& $script:SourceLogAction "UltraVNC non démarré : fichier absent ($sourceIni)." 'WARNING'
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,13 +48,13 @@ function Start-DeployCenterVnc {
|
||||
foreach ($outputLine in $wpeutilOutput) {
|
||||
$outputText = [string]$outputLine
|
||||
if (-not [string]::IsNullOrWhiteSpace($outputText)) {
|
||||
Write-Log "wpeutil DisableFirewall : $outputText" 'INFO'
|
||||
& $script:SourceLogAction "wpeutil DisableFirewall : $outputText" 'INFO'
|
||||
}
|
||||
}
|
||||
if ($wpeutilExitCode -ne 0) {
|
||||
throw "wpeutil DisableFirewall a renvoyé le code $wpeutilExitCode."
|
||||
}
|
||||
Write-Log "Pare-feu WinPE désactivé à la demande de -Vnc (code wpeutil : $wpeutilExitCode)." 'WARNING'
|
||||
& $script:SourceLogAction "Pare-feu WinPE désactivé à la demande de -Vnc (code wpeutil : $wpeutilExitCode)." 'WARNING'
|
||||
|
||||
# Force le mode portable : l'INI actif est celui placé à côté de winvnc.exe.
|
||||
$vncStage = 'activation du mode portable UltraVNC'
|
||||
@@ -97,13 +97,13 @@ function Start-DeployCenterVnc {
|
||||
throw "winvnc.exe s'est arrêté immédiatement (code $($script:VncProcess.ExitCode))."
|
||||
}
|
||||
$script:VncStarted = $true
|
||||
Write-Log "UltraVNC démarré. IP : $($script:DeployIPv4) ; mot de passe $passwordSource." 'SUCCESS'
|
||||
& $script:SourceLogAction "UltraVNC démarré. IP : $($script:DeployIPv4) ; mot de passe $passwordSource." 'SUCCESS'
|
||||
} catch {
|
||||
Write-Log "Échec du démarrage UltraVNC à l'étape '$vncStage' : $($_.Exception.Message)" 'ERROR'
|
||||
& $script:SourceLogAction "Échec du démarrage UltraVNC à l'étape '$vncStage' : $($_.Exception.Message)" 'ERROR'
|
||||
$script:VncStarted = $false
|
||||
$script:VncPassword = $null
|
||||
}
|
||||
Write-DeployInventory
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
}
|
||||
|
||||
function Stop-DeployCenterVnc {
|
||||
@@ -120,7 +120,7 @@ function Stop-DeployCenterVnc {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Arrêt UltraVNC : $($_.Exception.Message)" 'WARNING'
|
||||
& $script:SourceLogAction "Arrêt UltraVNC : $($_.Exception.Message)" 'WARNING'
|
||||
}
|
||||
# Le processus winvnc peut conserver brièvement un handle sur son dossier.
|
||||
# Le dossier VNC est propre à ce processus PowerShell (uvnc_<PID>) : on ne
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
# DeployCenter - Recherche et lecture des sources Windows
|
||||
# Compatible Windows PowerShell 5.1
|
||||
|
||||
function Find-WindowsSources {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$RootPath,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
|
||||
$results = New-Object System.Collections.Generic.List[object]
|
||||
$roots = @($RootPath -split ';' | ForEach-Object {
|
||||
$candidate = $_.Trim().Trim('"')
|
||||
if ($candidate -notmatch '^[A-Za-z]:\\$') { $candidate = $candidate.TrimEnd('\') }
|
||||
$candidate
|
||||
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
|
||||
if ($roots.Count -eq 0) {
|
||||
throw 'Indique au moins un chemin local et/ou un partage SMB, separes par un point-virgule.'
|
||||
}
|
||||
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
|
||||
if ($LogAction) { & $LogAction "Source inaccessible, ignoree : $root" 'WARNING' }
|
||||
continue
|
||||
}
|
||||
|
||||
if ($LogAction) { & $LogAction "Recherche des sources Windows dans : $root" 'INFO' }
|
||||
# Parcours en lecture seule. Les erreurs d'acces aux sous-dossiers sont ignorees.
|
||||
# Afficher les ISO et les fichiers WIM/ESD autonomes.
|
||||
# Exception : masquer boot.wim uniquement lorsqu'il fait partie d'un
|
||||
# support Windows extrait dont le même dossier Sources contient install.wim/esd.
|
||||
$allFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue)
|
||||
$files = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($file in $allFiles) {
|
||||
if ($file.Extension -ieq '.iso') {
|
||||
[void]$files.Add($file)
|
||||
continue
|
||||
}
|
||||
|
||||
if ($file.Extension -ine '.wim' -and $file.Extension -ine '.esd') {
|
||||
continue
|
||||
}
|
||||
|
||||
$isBootWimInExtractedMedia = $false
|
||||
if ($file.Name -ieq 'boot.wim' -and $file.Directory.Name -ieq 'Sources') {
|
||||
$installWim = Join-Path $file.Directory.FullName 'install.wim'
|
||||
$installEsd = Join-Path $file.Directory.FullName 'install.esd'
|
||||
if ((Test-Path -LiteralPath $installWim -PathType Leaf) -or
|
||||
(Test-Path -LiteralPath $installEsd -PathType Leaf)) {
|
||||
$isBootWimInExtractedMedia = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $isBootWimInExtractedMedia) {
|
||||
[void]$files.Add($file)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($file in $files) {
|
||||
$kind = switch ($file.Extension.ToLowerInvariant()) {
|
||||
'.iso' { 'Image ISO' }
|
||||
'.wim' { 'Image WIM Windows' }
|
||||
'.esd' { 'Image ESD Windows' }
|
||||
}
|
||||
$detail = if ($file.Name -ieq 'install.wim' -or $file.Name -ieq 'install.esd') {
|
||||
"Image d’installation dans un support extrait"
|
||||
} elseif ($file.Extension -ieq '.iso') {
|
||||
"Support d’installation ISO"
|
||||
} else {
|
||||
"Image WIM/ESD autonome"
|
||||
}
|
||||
$results.Add([pscustomobject]@{
|
||||
Type = $kind; Nom = $file.Name; Chemin = $file.FullName
|
||||
Taille = ('{0:N2} Go' -f ($file.Length / 1GB)); Detail = $detail
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (-not ($roots | Where-Object { Test-Path -LiteralPath $_ -PathType Container })) {
|
||||
throw "Aucun des chemins n'est accessible : $($roots -join '; ')`nVerifie que le lecteur local et le partage SMB sont accessibles."
|
||||
}
|
||||
if ($LogAction) { & $LogAction ("Detection terminee : {0} element(s) trouve(s)." -f $results.Count) 'SUCCESS' }
|
||||
return @($results | Sort-Object Type, Nom, Chemin -Unique)
|
||||
}
|
||||
|
||||
function Get-WindowsImageEditions {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ImagePath,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) {
|
||||
throw "Fichier image introuvable : $ImagePath"
|
||||
}
|
||||
|
||||
$normalized = @()
|
||||
$getWindowsImage = Get-Command Get-WindowsImage -ErrorAction SilentlyContinue
|
||||
if ($getWindowsImage) {
|
||||
try {
|
||||
$images = @(Get-WindowsImage -ImagePath $ImagePath -ErrorAction Stop)
|
||||
foreach ($image in $images) {
|
||||
$indexValue = $null
|
||||
foreach ($propertyName in @('ImageIndex', 'Index')) {
|
||||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||||
$indexValue = [int]$image.$propertyName
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($null -eq $indexValue) { continue }
|
||||
|
||||
$nameValue = $null
|
||||
foreach ($propertyName in @('ImageName', 'Name', 'ImageDescription')) {
|
||||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||||
$nameValue = [string]$image.$propertyName
|
||||
break
|
||||
}
|
||||
}
|
||||
$archValue = $null
|
||||
foreach ($propertyName in @('Architecture', 'ImageArchitecture')) {
|
||||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||||
$archValue = $image.$propertyName
|
||||
break
|
||||
}
|
||||
}
|
||||
$versionValue = $null
|
||||
foreach ($propertyName in @('Version', 'ImageVersion')) {
|
||||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||||
$versionValue = [string]$image.$propertyName
|
||||
break
|
||||
}
|
||||
}
|
||||
$normalized += [pscustomobject]@{
|
||||
Index = $indexValue
|
||||
Name = $nameValue
|
||||
Description = if ($image.PSObject.Properties['ImageDescription']) { $image.ImageDescription } else { $null }
|
||||
Architecture = $archValue
|
||||
Version = $versionValue
|
||||
Edition = if ($image.PSObject.Properties['EditionId']) { $image.EditionId } else { $null }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if ($LogAction) { & $LogAction "Get-WindowsImage indisponible pour $ImagePath : $($_.Exception.Message)" 'WARNING' }
|
||||
}
|
||||
}
|
||||
|
||||
$dism = Join-Path $env:WINDIR 'System32\dism.exe'
|
||||
if (-not (Test-Path -LiteralPath $dism)) { $dism = 'dism.exe' }
|
||||
|
||||
# Si le module PowerShell n'a pas renvoyé les index/noms, les récupérer avec DISM.
|
||||
if ($normalized.Count -eq 0) {
|
||||
$output = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "DISM n'a pas pu lire l'image : $ImagePath`n$($output -join "`n")"
|
||||
}
|
||||
|
||||
$current = $null
|
||||
foreach ($line in $output) {
|
||||
$lineText = [string]$line
|
||||
if ($lineText -match '^\s*Index\s*:\s*(\d+)\s*$') {
|
||||
if ($null -ne $current) { $normalized += [pscustomobject]$current }
|
||||
$current = @{ Index = [int]$Matches[1]; Name = ''; Description = ''; Architecture = $null; Version = $null; Edition = $null }
|
||||
} elseif ($null -ne $current -and $lineText -match '^\s*Name\s*:\s*(.*?)\s*$') {
|
||||
$current.Name = $Matches[1]
|
||||
} elseif ($null -ne $current -and $lineText -match '^\s*Description\s*:\s*(.*?)\s*$') {
|
||||
$current.Description = $Matches[1]
|
||||
}
|
||||
}
|
||||
if ($null -ne $current) { $normalized += [pscustomobject]$current }
|
||||
}
|
||||
|
||||
if ($normalized.Count -eq 0) {
|
||||
throw "DISM n’a retourné aucune édition exploitable pour : $ImagePath"
|
||||
}
|
||||
|
||||
# Interroger DISM pour chaque index même si Get-WindowsImage a fourni les noms :
|
||||
# selon la version du module, les propriétés Architecture/Version peuvent être absentes.
|
||||
foreach ($edition in $normalized) {
|
||||
if ($edition.Architecture -and $edition.Version) { continue }
|
||||
|
||||
$detailOutput = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" "/Index:$($edition.Index)" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
if ($LogAction) { & $LogAction "DISM n'a pas pu lire les metadonnees de l'index $($edition.Index) : $($detailOutput -join ' ')" 'WARNING' }
|
||||
continue
|
||||
}
|
||||
|
||||
$insideImageDetails = $false
|
||||
$imageVersion = $null
|
||||
$servicePackBuild = $null
|
||||
foreach ($detailLine in $detailOutput) {
|
||||
$detailText = [string]$detailLine
|
||||
if ($detailText -match '^\s*Details for image\s*:') {
|
||||
$insideImageDetails = $true
|
||||
continue
|
||||
}
|
||||
# Ignorer la version de DISM affichée au début de sa sortie.
|
||||
if (-not $insideImageDetails) { continue }
|
||||
|
||||
if (-not $edition.Architecture -and $detailText -match '^\s*Architecture\s*:\s*(.*?)\s*$') {
|
||||
$edition.Architecture = $Matches[1]
|
||||
} elseif ($detailText -match '^\s*Version\s*:\s*(.*?)\s*$') {
|
||||
$imageVersion = $Matches[1]
|
||||
} elseif ($detailText -match '^\s*ServicePack Build\s*:\s*(.*?)\s*$') {
|
||||
$servicePackBuild = $Matches[1]
|
||||
} elseif (-not $edition.Name -and $detailText -match '^\s*Name\s*:\s*(.*?)\s*$') {
|
||||
$edition.Name = $Matches[1]
|
||||
} elseif (-not $edition.Edition -and $detailText -match '^\s*Edition\s*:\s*(.*?)\s*$') {
|
||||
$edition.Edition = $Matches[1]
|
||||
}
|
||||
}
|
||||
if ($imageVersion) {
|
||||
$edition.Version = if ($servicePackBuild -and $servicePackBuild -ne '0') {
|
||||
"$imageVersion.$servicePackBuild"
|
||||
} else {
|
||||
$imageVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Version globale de l'image : ne l'afficher que si les métadonnées des index
|
||||
# donnent une seule version cohérente. Ne pas répéter cette valeur sur chaque ligne.
|
||||
$versionValues = @($normalized | ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$_.Version)) { [string]$_.Version }
|
||||
} | Select-Object -Unique)
|
||||
$globalVersion = $null
|
||||
if ($versionValues.Count -eq 1) {
|
||||
$globalVersion = $versionValues[0]
|
||||
}
|
||||
|
||||
foreach ($edition in $normalized) {
|
||||
if ($edition.Architecture -is [int] -or [string]$edition.Architecture -match '^\d+$') {
|
||||
switch ([string]$edition.Architecture) {
|
||||
'0' { $edition.Architecture = 'x86' }
|
||||
'5' { $edition.Architecture = 'ARM' }
|
||||
'9' { $edition.Architecture = 'x64' }
|
||||
'12' { $edition.Architecture = 'ARM64' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Editions = @($normalized)
|
||||
GlobalVersion = $globalVersion
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-InstallImagePath {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SelectedPath,
|
||||
[AllowNull()][object]$ExistingImage = $null
|
||||
)
|
||||
|
||||
if ([IO.Path]::GetExtension($SelectedPath) -in @('.wim', '.esd')) {
|
||||
return [pscustomobject]@{ SourcePath = $SelectedPath; ImagePath = $SelectedPath; MountedImage = $null }
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $SelectedPath -PathType Container) {
|
||||
foreach ($relative in @('sources\install.wim', 'sources\install.esd', 'Sources\install.wim', 'Sources\install.esd')) {
|
||||
$candidate = Join-Path $SelectedPath $relative
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
||||
return [pscustomobject]@{ SourcePath = $SelectedPath; ImagePath = $candidate; MountedImage = $null }
|
||||
}
|
||||
}
|
||||
throw "Aucun fichier Sources\install.wim ou Sources\install.esd dans : $SelectedPath"
|
||||
}
|
||||
|
||||
if ([IO.Path]::GetExtension($SelectedPath) -ieq '.iso') {
|
||||
if ($ExistingImage -and $ExistingImage.SourcePath -and
|
||||
[string]::Equals([IO.Path]::GetFullPath([string]$ExistingImage.SourcePath), [IO.Path]::GetFullPath($SelectedPath), [StringComparison]::OrdinalIgnoreCase) -and
|
||||
$ExistingImage.MountedImage -and $ExistingImage.ImagePath) {
|
||||
return $ExistingImage
|
||||
}
|
||||
if (-not (Get-Command Mount-DiskImage -ErrorAction SilentlyContinue)) {
|
||||
throw "Le montage ISO n'est pas disponible dans cet environnement (commande Mount-DiskImage absente). Pour analyser cette source, utilise le dossier extrait ou un fichier install.wim/esd."
|
||||
}
|
||||
# Ne pas laisser Windows attribuer une lettre arbitraire (souvent D:) à l'ISO.
|
||||
$diskImage = Mount-DiskImage -ImagePath $SelectedPath -Access ReadOnly -NoDriveLetter -PassThru -ErrorAction Stop
|
||||
$mountInfo = [pscustomobject]@{
|
||||
SourcePath = $SelectedPath
|
||||
ImagePath = $null
|
||||
MountedImage = $diskImage
|
||||
MountedPartition = $null
|
||||
DriveLetter = $null
|
||||
}
|
||||
try {
|
||||
# Partir des volumes explicitement associés à cette image évite de
|
||||
# confondre le lecteur CDROM virtuel et un disque physique dans WinPE.
|
||||
$volumes = @(Get-Volume -DiskImage $diskImage -ErrorAction Stop)
|
||||
if ($volumes.Count -eq 0) { throw 'Aucun volume ISO expose par Windows.' }
|
||||
$usedLetters = @(
|
||||
Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() }
|
||||
Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue | ForEach-Object { ([string]$_.Name).ToUpperInvariant() }
|
||||
)
|
||||
$availableLetter = $null
|
||||
foreach ($candidateLetter in @('Z','Y','X','W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E')) {
|
||||
if ($candidateLetter -notin @('C','D') -and $candidateLetter -notin $usedLetters) {
|
||||
$availableLetter = $candidateLetter
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $availableLetter) { throw 'Aucune lettre de lecteur temporaire disponible pour le montage ISO.' }
|
||||
|
||||
$checkedPaths = @()
|
||||
foreach ($volume in $volumes) {
|
||||
$volumeRoot = [string]$volume.Path
|
||||
if (-not $volumeRoot.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase) -and $volume.DriveLetter) {
|
||||
$mountVolCommand = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolCommand -PathType Leaf)) { $mountVolCommand = 'mountvol.exe' }
|
||||
$volumeMountPath = '{0}:\' -f [string]$volume.DriveLetter
|
||||
$volumeNameOutput = & $mountVolCommand $volumeMountPath '/L' 2>&1
|
||||
$volumeGuidPath = @($volumeNameOutput | ForEach-Object { [string]$_ } | Where-Object { $_.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase) }) | Select-Object -First 1
|
||||
if ($volumeGuidPath) { $volumeRoot = [string]$volumeGuidPath }
|
||||
}
|
||||
if ($volumeRoot -and $volumeRoot.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
if (-not $volumeRoot.EndsWith('\')) { $volumeRoot += '\' }
|
||||
foreach ($relative in @('sources\install.wim', 'sources\install.esd')) {
|
||||
$candidate = $volumeRoot + $relative
|
||||
$checkedPaths += $candidate
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
||||
if ([string]$volume.DriveLetter -eq 'D') {
|
||||
$mountVolCommand = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolCommand -PathType Leaf)) { $mountVolCommand = 'mountvol.exe' }
|
||||
$removeOutput = & $mountVolCommand 'D:\' '/D' 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "Impossible de libérer D: pour le volume ISO : $($removeOutput -join ' ')" }
|
||||
}
|
||||
$mountInfo.ImagePath = $candidate
|
||||
return $mountInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$partition = Get-Partition -Volume $volume -ErrorAction Stop
|
||||
if ($null -eq $partition) { continue }
|
||||
$existingDrivePaths = @($partition.AccessPaths | Where-Object { [string]$_ -match '^[A-Za-z]:\\$' })
|
||||
$reusableDrivePath = $existingDrivePaths | Where-Object { [string]$_ -notmatch '^[CDcd]:\\$' } | Select-Object -First 1
|
||||
if ($reusableDrivePath) {
|
||||
$driveAccessPath = [string]$reusableDrivePath
|
||||
$driveLetter = $driveAccessPath.Substring(0, 1).ToUpperInvariant()
|
||||
} else {
|
||||
foreach ($existingDrivePath in $existingDrivePaths) {
|
||||
Remove-PartitionAccessPath -InputObject $partition -AccessPath ([string]$existingDrivePath) -ErrorAction Stop | Out-Null
|
||||
}
|
||||
$driveLetter = $availableLetter
|
||||
$driveAccessPath = '{0}:\' -f $driveLetter
|
||||
Add-PartitionAccessPath -InputObject $partition -AccessPath $driveAccessPath -ErrorAction Stop
|
||||
}
|
||||
$mountInfo.MountedPartition = $partition
|
||||
$mountInfo.DriveLetter = $driveLetter
|
||||
foreach ($relative in @('sources\install.wim', 'sources\install.esd')) {
|
||||
$candidate = "${driveLetter}:\$relative"
|
||||
$checkedPaths += $candidate
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
||||
$mountInfo.ImagePath = $candidate
|
||||
return $mountInfo
|
||||
}
|
||||
}
|
||||
if ($driveLetter -eq $availableLetter) {
|
||||
Remove-PartitionAccessPath -InputObject $partition -AccessPath $driveAccessPath -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
$mountInfo.MountedPartition = $null
|
||||
$mountInfo.DriveLetter = $null
|
||||
}
|
||||
throw "Aucun fichier Sources\install.wim ou Sources\install.esd trouvé dans l'ISO. Chemins vérifiés : $($checkedPaths -join ', ')."
|
||||
} catch {
|
||||
try { [void](Dismount-DeployCenterInstallImage -Image $mountInfo -ErrorAction SilentlyContinue) } catch { }
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
throw "Type de source non pris en charge pour l'analyse : $SelectedPath"
|
||||
}
|
||||
|
||||
function Dismount-DeployCenterInstallImage {
|
||||
param([AllowNull()][object]$Image)
|
||||
|
||||
if ($null -eq $Image -or $null -eq $Image.MountedImage) { return }
|
||||
try {
|
||||
if ($Image.MountedPartition -and $Image.DriveLetter) {
|
||||
$driveAccessPath = '{0}:\' -f [string]$Image.DriveLetter
|
||||
Remove-PartitionAccessPath -InputObject $Image.MountedPartition -AccessPath $driveAccessPath -ErrorAction Stop | Out-Null
|
||||
}
|
||||
} finally {
|
||||
Dismount-DiskImage -InputObject $Image.MountedImage -ErrorAction Stop | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Find-WindowsSources, Get-WindowsImageEditions, Resolve-InstallImagePath, Dismount-DeployCenterInstallImage
|
||||
Reference in new issue
Block a user