Add support for SerialComputerNames CSV mapping

Introduces a new `SerialComputerNames` device naming mode that allows automated device naming during deployment based on the BIOS serial number. The mapping is provided via a CSV file with `SerialNumber` and `ComputerName` columns.

This feature requires `CopyUnattend` and writes a `SerialComputerNames.csv` file to the USB deployment media, replacing the need for manual prompts or prefix selection when device serial numbers are known in advance. The UI has been updated to support creating, loading, and saving the CSV mapping content.
This commit is contained in:
rbalsleyMSFT
2026-04-15 14:39:14 -07:00
parent 24f10b89b0
commit 38323e6be1
12 changed files with 335 additions and 15 deletions
@@ -42,13 +42,17 @@ function Get-SelectedDeviceNamingMode {
return 'Prefixes'
}
if ($true -eq $State.Controls.rbDeviceNamingSerialComputerNames.IsChecked) {
return 'SerialComputerNames'
}
return 'None'
}
function Set-DeviceNamingMode {
param(
[PSCustomObject]$State,
[ValidateSet('None', 'Prompt', 'Template', 'Prefixes')]
[ValidateSet('None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')]
[string]$Mode
)
@@ -56,12 +60,13 @@ function Set-DeviceNamingMode {
$State.Controls.rbDeviceNamingPrompt.IsChecked = $Mode -eq 'Prompt'
$State.Controls.rbDeviceNamingTemplate.IsChecked = $Mode -eq 'Template'
$State.Controls.rbDeviceNamingPrefixes.IsChecked = $Mode -eq 'Prefixes'
$State.Controls.rbDeviceNamingSerialComputerNames.IsChecked = $Mode -eq 'SerialComputerNames'
}
function Set-DeviceNamingModeState {
param(
[PSCustomObject]$State,
[ValidateSet('None', 'Prompt', 'Template', 'Prefixes')]
[ValidateSet('None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')]
[string]$DisplayMode,
[AllowNull()]
[string]$LoadedMode
@@ -121,6 +126,20 @@ function Get-DeviceNamePrefixes {
)
}
function Get-SerialComputerNamesLines {
param([PSCustomObject]$State)
if ($null -eq $State.Controls.txtDeviceNameSerialComputerNames) {
return @()
}
return @(
$State.Controls.txtDeviceNameSerialComputerNames.Text -split "\r?\n" |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
ForEach-Object { $_.Trim() }
)
}
function Import-DeviceNamePrefixesFile {
param(
[PSCustomObject]$State,
@@ -140,6 +159,25 @@ function Import-DeviceNamePrefixesFile {
return $true
}
function Import-SerialComputerNamesFile {
param(
[PSCustomObject]$State,
[string]$FilePath
)
if ([string]::IsNullOrWhiteSpace($FilePath) -or -not (Test-Path -Path $FilePath -PathType Leaf)) {
return $false
}
$serialMappingLines = @(Get-Content -Path $FilePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
if ($null -ne $State.Controls.txtDeviceNameSerialComputerNamesPath) {
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = $FilePath
}
$State.Controls.txtDeviceNameSerialComputerNames.Text = $serialMappingLines -join [System.Environment]::NewLine
WriteLog "Imported serial computer-name mappings from $FilePath"
return $true
}
function Get-DefaultDeviceNamePrefixesPath {
param([string]$FFUDevelopmentPath)
@@ -150,6 +188,16 @@ function Get-DefaultDeviceNamePrefixesPath {
return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') 'prefixes.txt'
}
function Get-DefaultSerialComputerNamesPath {
param([string]$FFUDevelopmentPath)
if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath)) {
return $null
}
return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') 'SerialComputerNames.csv'
}
function Get-DefaultUnattendFilePath {
param(
[string]$FFUDevelopmentPath,
@@ -188,6 +236,29 @@ function Import-DeviceNamePrefixesFromConfiguredPath {
}
}
function Import-SerialComputerNamesFromConfiguredPath {
param(
[PSCustomObject]$State,
[switch]$SkipIfTextPresent
)
if ($SkipIfTextPresent -and -not [string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNameSerialComputerNames.Text)) {
return
}
$serialComputerNamesPath = $State.Controls.txtDeviceNameSerialComputerNamesPath.Text
if ([string]::IsNullOrWhiteSpace($serialComputerNamesPath)) {
$serialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
if (-not [string]::IsNullOrWhiteSpace($serialComputerNamesPath) -and $null -ne $State.Controls.txtDeviceNameSerialComputerNamesPath) {
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = $serialComputerNamesPath
}
}
if (Test-Path -Path $serialComputerNamesPath -PathType Leaf) {
Import-SerialComputerNamesFile -State $State -FilePath $serialComputerNamesPath | Out-Null
}
}
function Test-DeviceNameTemplateUsesSerialToken {
param([PSCustomObject]$State)
@@ -201,7 +272,7 @@ function Update-UnattendSelectionControls {
$isCopyUnattendSelected = $true -eq $State.Controls.chkCopyUnattend.IsChecked
$isInjectUnattendSelected = $true -eq $State.Controls.chkInjectUnattend.IsChecked
$deviceNameTemplateUsesSerialToken = Test-DeviceNameTemplateUsesSerialToken -State $State
$requiresCopiedUnattend = ($selectedDeviceNamingMode -in @('Prompt', 'Prefixes')) -or $deviceNameTemplateUsesSerialToken
$requiresCopiedUnattend = ($selectedDeviceNamingMode -in @('Prompt', 'Prefixes', 'SerialComputerNames')) -or $deviceNameTemplateUsesSerialToken
if ($isCopyUnattendSelected -and $isInjectUnattendSelected) {
if ($requiresCopiedUnattend) {
@@ -247,19 +318,24 @@ function Update-UnattendSelectionControls {
function Update-DeviceNamingControls {
param([PSCustomObject]$State)
if (($true -eq $State.Controls.chkInjectUnattend.IsChecked) -and (($true -eq $State.Controls.rbDeviceNamingPrompt.IsChecked) -or ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked))) {
if (($true -eq $State.Controls.chkInjectUnattend.IsChecked) -and (($true -eq $State.Controls.rbDeviceNamingPrompt.IsChecked) -or ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked) -or ($true -eq $State.Controls.rbDeviceNamingSerialComputerNames.IsChecked))) {
$State.Controls.rbDeviceNamingNone.IsChecked = $true
}
$selectedDeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
$State.Controls.deviceNameTemplatePanel.Visibility = if ($selectedDeviceNamingMode -eq 'Template') { 'Visible' } else { 'Collapsed' }
$State.Controls.deviceNamePrefixesPanel.Visibility = if ($selectedDeviceNamingMode -eq 'Prefixes') { 'Visible' } else { 'Collapsed' }
$State.Controls.deviceNameSerialComputerNamesPanel.Visibility = if ($selectedDeviceNamingMode -eq 'SerialComputerNames') { 'Visible' } else { 'Collapsed' }
$State.Controls.rbDeviceNamingPrompt.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
$State.Controls.rbDeviceNamingPrefixes.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
$State.Controls.rbDeviceNamingSerialComputerNames.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
if ($selectedDeviceNamingMode -eq 'Prefixes') {
Import-DeviceNamePrefixesFromConfiguredPath -State $State -SkipIfTextPresent
}
elseif ($selectedDeviceNamingMode -eq 'SerialComputerNames') {
Import-SerialComputerNamesFromConfiguredPath -State $State -SkipIfTextPresent
}
Update-UnattendSelectionControls -State $State
}
@@ -480,18 +556,24 @@ function Register-EventHandlers {
$selectedPath = Invoke-BrowseAction -Type 'Folder' -Title "Select FFU Development Path"
if ($selectedPath) {
$currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
$currentSerialComputerNamesPath = $localState.Controls.txtDeviceNameSerialComputerNamesPath.Text
$currentUnattendX64FilePath = $localState.Controls.txtUnattendX64FilePath.Text
$currentUnattendArm64FilePath = $localState.Controls.txtUnattendArm64FilePath.Text
$previousDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
$previousDefaultSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
$previousDefaultUnattendX64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'x64'
$previousDefaultUnattendArm64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'arm64'
$localState.Controls.txtFFUDevPath.Text = $selectedPath
$newDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $selectedPath
$newDefaultSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $selectedPath
$newDefaultUnattendX64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $selectedPath -WindowsArch 'x64'
$newDefaultUnattendArm64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $selectedPath -WindowsArch 'arm64'
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath) -or $currentPrefixesPath -ieq $previousDefaultPrefixesPath) {
$localState.Controls.txtDeviceNamePrefixesPath.Text = $newDefaultPrefixesPath
}
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath) -or $currentSerialComputerNamesPath -ieq $previousDefaultSerialComputerNamesPath) {
$localState.Controls.txtDeviceNameSerialComputerNamesPath.Text = $newDefaultSerialComputerNamesPath
}
if ([string]::IsNullOrWhiteSpace($currentUnattendX64FilePath) -or $currentUnattendX64FilePath -ieq $previousDefaultUnattendX64FilePath) {
$localState.Controls.txtUnattendX64FilePath.Text = $newDefaultUnattendX64FilePath
}
@@ -499,6 +581,7 @@ function Register-EventHandlers {
$localState.Controls.txtUnattendArm64FilePath.Text = $newDefaultUnattendArm64FilePath
}
Import-DeviceNamePrefixesFromConfiguredPath -State $localState
Import-SerialComputerNamesFromConfiguredPath -State $localState
Update-DeviceNamingControls -State $localState
}
})
@@ -560,6 +643,16 @@ function Register-EventHandlers {
}
Update-DeviceNamingControls -State $localState
})
$State.Controls.rbDeviceNamingSerialComputerNames.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
if (-not ($true -eq $localState.Flags.suppressDeviceNamingChangeTracking)) {
$localState.Flags.deviceNamingModeWasExplicitlyChanged = $true
$localState.Data.loadedDeviceNamingMode = $null
}
Update-DeviceNamingControls -State $localState
})
$State.Controls.btnBrowseDeviceNamePrefixesPath.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
@@ -580,6 +673,26 @@ function Register-EventHandlers {
Update-DeviceNamingControls -State $localState
}
})
$State.Controls.btnBrowseDeviceNameSerialComputerNamesPath.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$currentSerialComputerNamesPath = $localState.Controls.txtDeviceNameSerialComputerNamesPath.Text
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
$currentSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
}
$initialDirectory = if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
$null
}
else {
Split-Path $currentSerialComputerNamesPath -Parent
}
$fileName = if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) { 'SerialComputerNames.csv' } else { Split-Path $currentSerialComputerNamesPath -Leaf }
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title 'Select SerialComputerNames.csv file path' -Filter 'CSV files (*.csv)|*.csv|All files (*.*)|*.*' -InitialDirectory $initialDirectory -FileName $fileName
if (Import-SerialComputerNamesFile -State $localState -FilePath $selectedPath) {
Update-DeviceNamingControls -State $localState
}
})
$State.Controls.btnBrowseUnattendX64FilePath.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
@@ -653,6 +766,39 @@ function Register-EventHandlers {
[System.Windows.MessageBox]::Show("Saving prefixes failed for '$currentPrefixesPath'. $($_.Exception.Message)", "Save Prefixes Failed", "OK", "Error") | Out-Null
}
})
$State.Controls.btnSaveDeviceNameSerialComputerNames.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$serialComputerNameLines = @(Get-SerialComputerNamesLines -State $localState)
if ($serialComputerNameLines.Count -eq 0) {
[System.Windows.MessageBox]::Show("Enter CSV content before saving the serial mapping file.", "Serial Mapping Required", "OK", "Warning") | Out-Null
return
}
$currentSerialComputerNamesPath = $localState.Controls.txtDeviceNameSerialComputerNamesPath.Text
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
$currentSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
if (-not [string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
$localState.Controls.txtDeviceNameSerialComputerNamesPath.Text = $currentSerialComputerNamesPath
}
}
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
[System.Windows.MessageBox]::Show("Select a valid SerialComputerNames.csv file path before saving the serial mapping.", "Serial Mapping File Path Required", "OK", "Warning") | Out-Null
return
}
try {
$serialComputerNameLines | Set-Content -Path $currentSerialComputerNamesPath -Encoding UTF8
$localState.Controls.txtDeviceNameSerialComputerNamesPath.Text = $currentSerialComputerNamesPath
WriteLog "Saved serial computer-name mappings to $currentSerialComputerNamesPath"
}
catch {
[System.Windows.MessageBox]::Show("Saving serial mapping failed for '$currentSerialComputerNamesPath'. $($_.Exception.Message)", "Save Serial Mapping Failed", "OK", "Error") | Out-Null
}
})
$State.Controls.chkCopyUnattend.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)