Auto-generate ComputerName in Unattend XML

Replaces the strict validation for an existing ComputerName element with dynamic XML initialization. Automatically creates the specialize settings block, the Microsoft-Windows-Shell-Setup component, and the ComputerName element if they are missing from the provided Unattend XML file. This improves script robustness and simplifies the requirements for custom unattended setup templates by patching in the necessary device naming structure on the fly.
This commit is contained in:
rbalsleyMSFT
2026-04-09 15:14:00 -07:00
parent 82bac17b38
commit f1f1957c43
+213 -59
View File
@@ -534,20 +534,79 @@ function Get-UnattendSourcePath {
return Join-Path $UnattendFolder "unattend_$archSuffix.xml" return Join-Path $UnattendFolder "unattend_$archSuffix.xml"
} }
function Test-UnattendHasComputerNameElement { function Initialize-UnattendComputerNamePath {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$Path [xml]$UnattendXml,
[Parameter(Mandatory = $true)]
[string]$WindowsArch
) )
[xml]$unattendXml = Get-Content -Path $Path $unattendRoot = $UnattendXml.DocumentElement
foreach ($component in $unattendXml.unattend.settings.component) { if (($null -eq $unattendRoot) -or ($unattendRoot.LocalName -ne 'unattend')) {
if ($component.ComputerName) { throw 'Unattend XML is missing the unattend root element.'
return $true
}
} }
return $false $unattendNamespace = $unattendRoot.NamespaceURI
if ([string]::IsNullOrWhiteSpace($unattendNamespace)) {
throw 'Unattend XML is missing the default unattend namespace.'
}
$namespaceManager = New-Object System.Xml.XmlNamespaceManager($UnattendXml.NameTable)
$namespaceManager.AddNamespace('un', $unattendNamespace)
$specializeSettings = $unattendRoot.SelectSingleNode("un:settings[@pass='specialize']", $namespaceManager)
$createdSpecializeSettings = $false
if ($null -eq $specializeSettings) {
$specializeSettings = $UnattendXml.CreateElement('settings', $unattendNamespace)
$null = $specializeSettings.SetAttribute('pass', 'specialize')
$firstSettingsNode = $unattendRoot.SelectSingleNode('un:settings', $namespaceManager)
if ($null -ne $firstSettingsNode) {
$null = $unattendRoot.InsertBefore($specializeSettings, $firstSettingsNode)
}
else {
$null = $unattendRoot.AppendChild($specializeSettings)
}
$createdSpecializeSettings = $true
}
$shellSetupComponent = $specializeSettings.SelectSingleNode("un:component[@name='Microsoft-Windows-Shell-Setup']", $namespaceManager)
$createdShellSetupComponent = $false
if ($null -eq $shellSetupComponent) {
$processorArchitecture = if ($WindowsArch -ieq 'arm64') { 'arm64' } else { 'amd64' }
$shellSetupComponent = $UnattendXml.CreateElement('component', $unattendNamespace)
$null = $shellSetupComponent.SetAttribute('name', 'Microsoft-Windows-Shell-Setup')
$null = $shellSetupComponent.SetAttribute('processorArchitecture', $processorArchitecture)
$null = $shellSetupComponent.SetAttribute('publicKeyToken', '31bf3856ad364e35')
$null = $shellSetupComponent.SetAttribute('language', 'neutral')
$null = $shellSetupComponent.SetAttribute('versionScope', 'nonSxS')
$null = $shellSetupComponent.SetAttribute('xmlns:wcm', 'http://www.w3.org/2000/xmlns/', 'http://schemas.microsoft.com/WMIConfig/2002/State')
$null = $shellSetupComponent.SetAttribute('xmlns:xsi', 'http://www.w3.org/2000/xmlns/', 'http://www.w3.org/2001/XMLSchema-instance')
$firstComponentNode = $specializeSettings.SelectSingleNode('un:component', $namespaceManager)
if ($null -ne $firstComponentNode) {
$null = $specializeSettings.InsertBefore($shellSetupComponent, $firstComponentNode)
}
else {
$null = $specializeSettings.AppendChild($shellSetupComponent)
}
$createdShellSetupComponent = $true
}
$computerNameElement = $shellSetupComponent.SelectSingleNode('un:ComputerName', $namespaceManager)
$createdComputerNameElement = $false
if ($null -eq $computerNameElement) {
$computerNameElement = $UnattendXml.CreateElement('ComputerName', $unattendNamespace)
$null = $shellSetupComponent.AppendChild($computerNameElement)
$createdComputerNameElement = $true
}
return [PSCustomObject]@{
ComputerNameElement = $computerNameElement
CreatedSpecializeSettings = $createdSpecializeSettings
CreatedShellSetupComponent = $createdShellSetupComponent
CreatedComputerNameElement = $createdComputerNameElement
}
} }
function Save-StagedUnattendFile { function Save-StagedUnattendFile {
@@ -559,40 +618,47 @@ function Save-StagedUnattendFile {
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[ValidateSet('Legacy', 'None', 'Prompt', 'Template', 'Prefixes')] [ValidateSet('Legacy', 'None', 'Prompt', 'Template', 'Prefixes')]
[string]$DeviceNamingMode, [string]$DeviceNamingMode,
[string]$DeviceNameTemplate [string]$DeviceNameTemplate,
[Parameter(Mandatory = $true)]
[string]$WindowsArch,
[bool]$LegacyPrefixesWillBeStaged = $false
) )
if ($DeviceNamingMode -in @('Legacy', 'Prefixes')) { if ($DeviceNamingMode -eq 'None') {
Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null
return return
} }
[xml]$unattendXml = Get-Content -Path $SourcePath [xml]$unattendXml = Get-Content -Path $SourcePath
$computerNameComponent = $null $computerNamePath = Initialize-UnattendComputerNamePath -UnattendXml $unattendXml -WindowsArch $WindowsArch
foreach ($component in $unattendXml.unattend.settings.component) {
if ($component.ComputerName) { if ($computerNamePath.CreatedSpecializeSettings -or $computerNamePath.CreatedShellSetupComponent -or $computerNamePath.CreatedComputerNameElement) {
$computerNameComponent = $component $createdParts = @()
break if ($computerNamePath.CreatedSpecializeSettings) {
$createdParts += 'specialize settings'
} }
} if ($computerNamePath.CreatedShellSetupComponent) {
$createdParts += 'Microsoft-Windows-Shell-Setup component'
if ($null -eq $computerNameComponent) {
if ($DeviceNamingMode -eq 'None') {
Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null
return
} }
if ($computerNamePath.CreatedComputerNameElement) {
throw "ComputerName element not found in unattend source file: $SourcePath" $createdParts += 'ComputerName element'
}
WriteLog "Created $($createdParts -join ', ') while staging unattend file $DestinationPath"
} }
if ($DeviceNamingMode -eq 'None') { if ($DeviceNamingMode -eq 'Prompt') {
$computerNameComponent.ComputerName = '*' $computerNamePath.ComputerNameElement.InnerText = 'MyComputer'
}
elseif ($DeviceNamingMode -eq 'Prompt') {
$computerNameComponent.ComputerName = 'MyComputer'
} }
elseif ($DeviceNamingMode -eq 'Template') { elseif ($DeviceNamingMode -eq 'Template') {
$computerNameComponent.ComputerName = $DeviceNameTemplate $computerNamePath.ComputerNameElement.InnerText = $DeviceNameTemplate
}
elseif ($DeviceNamingMode -eq 'Prefixes') {
if ($computerNamePath.CreatedComputerNameElement) {
$computerNamePath.ComputerNameElement.InnerText = '*'
}
}
elseif (($DeviceNamingMode -eq 'Legacy') -and $computerNamePath.CreatedComputerNameElement) {
$computerNamePath.ComputerNameElement.InnerText = if ($LegacyPrefixesWillBeStaged) { '*' } else { 'MyComputer' }
} }
$unattendXml.Save($DestinationPath) $unattendXml.Save($DestinationPath)
@@ -4338,45 +4404,124 @@ Function New-DeploymentUSB {
return Join-Path $UnattendFolder "unattend_$archSuffix.xml" return Join-Path $UnattendFolder "unattend_$archSuffix.xml"
} }
function Initialize-UnattendComputerNamePath {
param(
[xml]$UnattendXml,
[string]$WindowsArch
)
$unattendRoot = $UnattendXml.DocumentElement
if (($null -eq $unattendRoot) -or ($unattendRoot.LocalName -ne 'unattend')) {
throw 'Unattend XML is missing the unattend root element.'
}
$unattendNamespace = $unattendRoot.NamespaceURI
if ([string]::IsNullOrWhiteSpace($unattendNamespace)) {
throw 'Unattend XML is missing the default unattend namespace.'
}
$namespaceManager = New-Object System.Xml.XmlNamespaceManager($UnattendXml.NameTable)
$namespaceManager.AddNamespace('un', $unattendNamespace)
$specializeSettings = $unattendRoot.SelectSingleNode("un:settings[@pass='specialize']", $namespaceManager)
$createdSpecializeSettings = $false
if ($null -eq $specializeSettings) {
$specializeSettings = $UnattendXml.CreateElement('settings', $unattendNamespace)
$null = $specializeSettings.SetAttribute('pass', 'specialize')
$firstSettingsNode = $unattendRoot.SelectSingleNode('un:settings', $namespaceManager)
if ($null -ne $firstSettingsNode) {
$null = $unattendRoot.InsertBefore($specializeSettings, $firstSettingsNode)
}
else {
$null = $unattendRoot.AppendChild($specializeSettings)
}
$createdSpecializeSettings = $true
}
$shellSetupComponent = $specializeSettings.SelectSingleNode("un:component[@name='Microsoft-Windows-Shell-Setup']", $namespaceManager)
$createdShellSetupComponent = $false
if ($null -eq $shellSetupComponent) {
$processorArchitecture = if ($WindowsArch -ieq 'arm64') { 'arm64' } else { 'amd64' }
$shellSetupComponent = $UnattendXml.CreateElement('component', $unattendNamespace)
$null = $shellSetupComponent.SetAttribute('name', 'Microsoft-Windows-Shell-Setup')
$null = $shellSetupComponent.SetAttribute('processorArchitecture', $processorArchitecture)
$null = $shellSetupComponent.SetAttribute('publicKeyToken', '31bf3856ad364e35')
$null = $shellSetupComponent.SetAttribute('language', 'neutral')
$null = $shellSetupComponent.SetAttribute('versionScope', 'nonSxS')
$null = $shellSetupComponent.SetAttribute('xmlns:wcm', 'http://www.w3.org/2000/xmlns/', 'http://schemas.microsoft.com/WMIConfig/2002/State')
$null = $shellSetupComponent.SetAttribute('xmlns:xsi', 'http://www.w3.org/2000/xmlns/', 'http://www.w3.org/2001/XMLSchema-instance')
$firstComponentNode = $specializeSettings.SelectSingleNode('un:component', $namespaceManager)
if ($null -ne $firstComponentNode) {
$null = $specializeSettings.InsertBefore($shellSetupComponent, $firstComponentNode)
}
else {
$null = $specializeSettings.AppendChild($shellSetupComponent)
}
$createdShellSetupComponent = $true
}
$computerNameElement = $shellSetupComponent.SelectSingleNode('un:ComputerName', $namespaceManager)
$createdComputerNameElement = $false
if ($null -eq $computerNameElement) {
$computerNameElement = $UnattendXml.CreateElement('ComputerName', $unattendNamespace)
$null = $shellSetupComponent.AppendChild($computerNameElement)
$createdComputerNameElement = $true
}
return [PSCustomObject]@{
ComputerNameElement = $computerNameElement
CreatedSpecializeSettings = $createdSpecializeSettings
CreatedShellSetupComponent = $createdShellSetupComponent
CreatedComputerNameElement = $createdComputerNameElement
}
}
function Save-LocalStagedUnattendFile { function Save-LocalStagedUnattendFile {
param( param(
[string]$SourcePath, [string]$SourcePath,
[string]$DestinationPath, [string]$DestinationPath,
[string]$DeviceNamingMode, [string]$DeviceNamingMode,
[string]$DeviceNameTemplate [string]$DeviceNameTemplate,
[string]$WindowsArch,
[bool]$LegacyPrefixesWillBeStaged = $false
) )
if ($DeviceNamingMode -in @('Legacy', 'Prefixes')) { if ($DeviceNamingMode -eq 'None') {
Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null
return return
} }
[xml]$unattendXml = Get-Content -Path $SourcePath [xml]$unattendXml = Get-Content -Path $SourcePath
$computerNameComponent = $null $computerNamePath = Initialize-UnattendComputerNamePath -UnattendXml $unattendXml -WindowsArch $WindowsArch
foreach ($component in $unattendXml.unattend.settings.component) {
if ($component.ComputerName) { if ($computerNamePath.CreatedSpecializeSettings -or $computerNamePath.CreatedShellSetupComponent -or $computerNamePath.CreatedComputerNameElement) {
$computerNameComponent = $component $createdParts = @()
break if ($computerNamePath.CreatedSpecializeSettings) {
$createdParts += 'specialize settings'
} }
} if ($computerNamePath.CreatedShellSetupComponent) {
$createdParts += 'Microsoft-Windows-Shell-Setup component'
if ($null -eq $computerNameComponent) {
if ($DeviceNamingMode -eq 'None') {
Copy-Item -Path $SourcePath -Destination $DestinationPath -Force | Out-Null
return
} }
if ($computerNamePath.CreatedComputerNameElement) {
throw "ComputerName element not found in unattend source file: $SourcePath" $createdParts += 'ComputerName element'
}
WriteLog "Created $($createdParts -join ', ') while staging unattend file $DestinationPath"
} }
if ($DeviceNamingMode -eq 'None') { if ($DeviceNamingMode -eq 'Prompt') {
$computerNameComponent.ComputerName = '*' $computerNamePath.ComputerNameElement.InnerText = 'MyComputer'
}
elseif ($DeviceNamingMode -eq 'Prompt') {
$computerNameComponent.ComputerName = 'MyComputer'
} }
elseif ($DeviceNamingMode -eq 'Template') { elseif ($DeviceNamingMode -eq 'Template') {
$computerNameComponent.ComputerName = $DeviceNameTemplate $computerNamePath.ComputerNameElement.InnerText = $DeviceNameTemplate
}
elseif ($DeviceNamingMode -eq 'Prefixes') {
if ($computerNamePath.CreatedComputerNameElement) {
$computerNamePath.ComputerNameElement.InnerText = '*'
}
}
elseif (($DeviceNamingMode -eq 'Legacy') -and $computerNamePath.CreatedComputerNameElement) {
$computerNamePath.ComputerNameElement.InnerText = if ($LegacyPrefixesWillBeStaged) { '*' } else { 'MyComputer' }
} }
$unattendXml.Save($DestinationPath) $unattendXml.Save($DestinationPath)
@@ -4443,12 +4588,13 @@ Function New-DeploymentUSB {
WriteLog "Copying unattend file to $UnattendPathOnUSB" WriteLog "Copying unattend file to $UnattendPathOnUSB"
New-Item -Path $UnattendPathOnUSB -ItemType Directory -ErrorAction SilentlyContinue | Out-Null New-Item -Path $UnattendPathOnUSB -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
$unattendSource = Get-LocalUnattendSourcePath -UnattendFolder $using:UnattendFolder -WindowsArch $using:WindowsArch $unattendSource = Get-LocalUnattendSourcePath -UnattendFolder $using:UnattendFolder -WindowsArch $using:WindowsArch
Save-LocalStagedUnattendFile -SourcePath $unattendSource -DestinationPath (Join-Path $UnattendPathOnUSB 'Unattend.xml') -DeviceNamingMode $using:DeviceNamingMode -DeviceNameTemplate $using:normalizedDeviceNameTemplate $legacyPrefixesWillBeStaged = ($using:DeviceNamingMode -eq 'Legacy') -and (Test-Path -Path $using:resolvedDeviceNamePrefixesPath -PathType Leaf)
Save-LocalStagedUnattendFile -SourcePath $unattendSource -DestinationPath (Join-Path $UnattendPathOnUSB 'Unattend.xml') -DeviceNamingMode $using:DeviceNamingMode -DeviceNameTemplate $using:normalizedDeviceNameTemplate -WindowsArch $using:WindowsArch -LegacyPrefixesWillBeStaged $legacyPrefixesWillBeStaged
if ($using:DeviceNamingMode -eq 'Prefixes') { if ($using:DeviceNamingMode -eq 'Prefixes') {
WriteLog "Writing prefixes.txt file to $UnattendPathOnUSB" WriteLog "Writing prefixes.txt file to $UnattendPathOnUSB"
$using:effectiveDeviceNamePrefixes | Set-Content -Path (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Encoding UTF8 $using:effectiveDeviceNamePrefixes | Set-Content -Path (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Encoding UTF8
} }
elseif (($using:DeviceNamingMode -eq 'Legacy') -and (Test-Path -Path $using:resolvedDeviceNamePrefixesPath -PathType Leaf)) { elseif ($legacyPrefixesWillBeStaged) {
WriteLog "Copying prefixes.txt file to $UnattendPathOnUSB" WriteLog "Copying prefixes.txt file to $UnattendPathOnUSB"
Copy-Item -Path $using:resolvedDeviceNamePrefixesPath -Destination (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Force | Out-Null Copy-Item -Path $using:resolvedDeviceNamePrefixesPath -Destination (Join-Path $UnattendPathOnUSB 'prefixes.txt') -Force | Out-Null
} }
@@ -5717,21 +5863,29 @@ if ($CopyUnattend) {
throw "-CopyUnattend is set to `$true, but the $UnattendFolder folder is missing a .XML file" throw "-CopyUnattend is set to `$true, but the $UnattendFolder folder is missing a .XML file"
} }
if ($DeviceNamingMode -in @('Prompt', 'Prefixes')) { if ($DeviceNamingMode -ne 'None') {
$unattendSourcePath = Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch $unattendSourcePath = Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch
if (-not (Test-UnattendHasComputerNameElement -Path $unattendSourcePath)) { try {
throw "DeviceNamingMode $DeviceNamingMode requires a ComputerName element in $unattendSourcePath" [xml]$validationUnattendXml = Get-Content -Path $unattendSourcePath
$null = Initialize-UnattendComputerNamePath -UnattendXml $validationUnattendXml -WindowsArch $WindowsArch
}
catch {
throw "DeviceNamingMode $DeviceNamingMode requires a valid specialize/Microsoft-Windows-Shell-Setup/ComputerName path in $unattendSourcePath. $($_.Exception.Message)"
} }
} }
WriteLog 'Unattend validation complete' WriteLog 'Unattend validation complete'
} }
if ($InjectUnattend -and $DeviceNamingMode -eq 'Template') { if ($InjectUnattend -and ($DeviceNamingMode -ne 'None')) {
$injectUnattendSourcePath = Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch $injectUnattendSourcePath = Get-UnattendSourcePath -UnattendFolder $UnattendFolder -WindowsArch $WindowsArch
if (Test-Path -Path $injectUnattendSourcePath -PathType Leaf) { if (Test-Path -Path $injectUnattendSourcePath -PathType Leaf) {
if (-not (Test-UnattendHasComputerNameElement -Path $injectUnattendSourcePath)) { try {
throw "DeviceNamingMode Template requires a ComputerName element in $injectUnattendSourcePath" [xml]$validationUnattendXml = Get-Content -Path $injectUnattendSourcePath
$null = Initialize-UnattendComputerNamePath -UnattendXml $validationUnattendXml -WindowsArch $WindowsArch
}
catch {
throw "DeviceNamingMode $DeviceNamingMode requires a valid specialize/Microsoft-Windows-Shell-Setup/ComputerName path in $injectUnattendSourcePath. $($_.Exception.Message)"
} }
} }
} }
@@ -6644,7 +6798,7 @@ if ($InstallApps) {
# Copy if source exists; otherwise log and skip # Copy if source exists; otherwise log and skip
if (Test-Path -Path $unattendSource -PathType Leaf) { if (Test-Path -Path $unattendSource -PathType Leaf) {
$destination = Join-Path $targetFolder 'Unattend.xml' $destination = Join-Path $targetFolder 'Unattend.xml'
Save-StagedUnattendFile -SourcePath $unattendSource -DestinationPath $destination -DeviceNamingMode $DeviceNamingMode -DeviceNameTemplate $normalizedDeviceNameTemplate Save-StagedUnattendFile -SourcePath $unattendSource -DestinationPath $destination -DeviceNamingMode $DeviceNamingMode -DeviceNameTemplate $normalizedDeviceNameTemplate -WindowsArch $WindowsArch
WriteLog "Injected unattend file into Apps: $unattendSource -> $destination" WriteLog "Injected unattend file into Apps: $unattendSource -> $destination"
} }
else { else {