Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

All notable changes to this project will be documented in this file.

## 26.10.10.1 - October 10, 2026

### Added

- Expanded the OSDCore device inventory with processor, physical memory, and network adapter IPv4 details, and added a physical memory diagnostic log.
- Expanded `Show-OSDCloudDeviceInfo` to display device and BIOS identity, processor details, memory capacity and configuration, disk drives, and network adapters with IPv4 addresses. Warn when physical memory is below 6 GB; serial number and UUID remain suppressed for privacy.
- Added tests for hardware inventory collection and device information display, including initialization and unavailable-data scenarios.

### Changed

- Renamed time synchronization status messages from "System DateTime" to "Device DateTime" for clearer reporting.
- Module version bumped from `26.10.6.1` to `26.10.10.1`.

## 26.10.6.1 - October 6, 2026

### Added
Expand Down
2 changes: 1 addition & 1 deletion OSDCloud/OSDCloud.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@{
# --- Identity ---
RootModule = 'OSDCloud.psm1'
ModuleVersion = '26.10.6.1'
ModuleVersion = '26.10.10.1'
CompatiblePSEditions = @('Core', 'Desktop')
GUID = '2fbd5c65-79c7-4561-9a2e-c4a4eebc89c7'

Expand Down
195 changes: 195 additions & 0 deletions OSDCloud/private/core-device/Initialize-OSDCoreDevice.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
BeforeAll {
$tokens = $null
$parseErrors = $null
$initializerAst = [System.Management.Automation.Language.Parser]::ParseFile(
"$PSScriptRoot\Initialize-OSDCoreDevice.ps1", [ref]$tokens, [ref]$parseErrors
)
if ($parseErrors.Count -gt 0) {
throw ($parseErrors -join "`n")
}
$snapshotAssignment = $initializerAst.Find({
param($node)
$node -is [System.Management.Automation.Language.AssignmentStatementAst] -and
$node.Left.Extent.Text -eq '$global:OSDCoreDevice' -and
$node.Right.Extent.Text -like '[[]ordered]*'
}, $true)
$snapshotMap = $snapshotAssignment.Find({
param($node)
$node -is [System.Management.Automation.Language.HashtableAst]
}, $true)
# Evaluate only the actual snapshot mappings, not the initializer's hardware
# queries, clock synchronization, disk discovery, or diagnostic writes.
$inventoryKeys = @('DiskDrives', 'NetworkAdapters', 'Processors', 'PhysicalMemory', 'NativeOSArchitecture', 'TotalPhysicalMemory')
$inventoryMappings = @($snapshotMap.KeyValuePairs | Where-Object {
$_.Item1.Value -in $inventoryKeys
})
$mappingScript = [scriptblock]::Create(
"[ordered]@{`n" + (($inventoryMappings | ForEach-Object {
$_.Item1.Extent.Text + ' = ' + $_.Item2.Extent.Text
}) -join "`n") + "`n}"
)
$initializerFunction = $initializerAst.Find({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst]
}, $true)
$statements = $initializerFunction.Body.EndBlock.Statements
for ($i = 0; $i -lt $statements.Count; $i++) {
if ($statements[$i] -is [System.Management.Automation.Language.AssignmentStatementAst] -and
$statements[$i].Left.Extent.Text -eq '$classWin32PhysicalMemory') {
$physicalMemoryCollection = [scriptblock]::Create(
($statements[$i..($i + 2)] | ForEach-Object { $_.Extent.Text }) -join "`n"
)
break
}
}
}

Describe 'Initialize-OSDCoreDevice physical memory collection' {
BeforeEach {
$LogsPath = $TestDrive
Mock Out-File {}
}

It 'queries modules once and exports the complete diagnostic inventory' {
Mock Get-CimInstance {
[pscustomobject]@{ DeviceLocator = 'DIMM1'; Speed = 5600; ConfiguredClockSpeed = 4800 }
[pscustomobject]@{ DeviceLocator = 'DIMM2'; Speed = 5600; ConfiguredClockSpeed = 4800 }
}
. $physicalMemoryCollection
$classWin32PhysicalMemory.Count | Should -Be 2
$classWin32PhysicalMemory[0].DeviceLocator | Should -Be 'DIMM1'
Should -Invoke Get-CimInstance -Times 1 -Exactly -ParameterFilter {
$ClassName -eq 'Win32_PhysicalMemory' -and $ErrorAction -eq 'Stop'
}
Should -Invoke Out-File -Times 2 -Exactly -ParameterFilter {
$FilePath -eq (Join-Path $LogsPath 'Win32_PhysicalMemory.txt')
}
}

It 'warns and leaves an empty inventory if module collection fails' {
Mock Get-CimInstance { throw 'Memory provider unavailable.' }
Mock Write-Warning {}
. $physicalMemoryCollection
$classWin32PhysicalMemory.Count | Should -Be 0
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter {
$Message -like '*Unable to query Win32_PhysicalMemory:*Memory provider unavailable*'
}
}
}

Describe 'Initialize-OSDCoreDevice display inventory mappings' {
BeforeEach {
$classWin32NetworkAdapterConfiguration = @()
$classWin32PhysicalMemory = @()
}

It 'matches IPv4 addresses by adapter index and excludes IPv6 and unassigned addresses' {
$classWin32NetworkAdapter = @(
[pscustomobject]@{ Name = 'Ethernet'; Index = 1 }
[pscustomobject]@{ Name = 'Wi-Fi'; Index = 2 }
[pscustomobject]@{ Name = 'Disconnected'; Index = 3 }
[pscustomobject]@{ Name = 'IPv6 only'; Index = 4 }
)
$classWin32NetworkAdapterConfiguration = @(
[pscustomobject]@{ Index = 2; IPAddress = @('192.168.1.20') }
[pscustomobject]@{ Index = 1; IPAddress = @('fe80::1', '10.0.0.10', '10.0.0.11') }
[pscustomobject]@{ Index = 4; IPAddress = @('fe80::4') }
)
$snapshot = & $mappingScript
$snapshot.NetworkAdapters[0].IPv4Addresses | Should -Be @('10.0.0.10', '10.0.0.11')
$snapshot.NetworkAdapters[1].IPv4Addresses | Should -Be @('192.168.1.20')
$snapshot.NetworkAdapters[2].IPv4Addresses.Count | Should -Be 0
$snapshot.NetworkAdapters[3].IPv4Addresses.Count | Should -Be 0
$jsonSnapshot = $snapshot | ConvertTo-Json -Depth 10 -WarningVariable serializationWarnings | ConvertFrom-Json
@($serializationWarnings).Count | Should -Be 0
$jsonSnapshot.NetworkAdapters[0].IPv4Addresses | Should -Be @('10.0.0.10', '10.0.0.11')
}

It 'exposes all six fields from the existing collection variables' {
$inventoryMappings.Count | Should -Be 6
$classWin32DiskDrive = [pscustomobject]@{ Model = 'Disk'; ExtraDiskField = 'retained' }
$classWin32NetworkAdapter = [pscustomobject]@{ Name = 'Ethernet'; ExtraNetworkField = 'retained' }
$classWin32Processor = [pscustomobject]@{ Name = 'CPU'; ExtraProcessorField = 'retained' }
$classWin32PhysicalMemory = [pscustomobject]@{ ConfiguredClockSpeed = 4800; Speed = 5600; Capacity = 8GB }
$classWin32OperatingSystem = [pscustomobject]@{ OSArchitecture = '64-bit' }
$classWin32ComputerSystem = [pscustomobject]@{ TotalPhysicalMemory = 6.5GB }
$OSArchitecture = 'override'
$snapshot = & $mappingScript
$snapshot.NativeOSArchitecture | Should -Be '64-bit'
$snapshot.TotalPhysicalMemory | Should -Be 6.5GB
$snapshot.DiskDrives[0].Model | Should -Be 'Disk'
$snapshot.NetworkAdapters[0].Name | Should -Be 'Ethernet'
$snapshot.Processors[0].Name | Should -Be 'CPU'

$jsonSnapshot = $snapshot | ConvertTo-Json -Depth 10 | ConvertFrom-Json
$xmlPath = Join-Path $TestDrive 'inventory.xml'
$snapshot | Export-Clixml -Path $xmlPath
$xmlSnapshot = Import-Clixml -Path $xmlPath
foreach ($roundTrip in @($jsonSnapshot, $xmlSnapshot)) {
$roundTrip.DiskDrives[0].ExtraDiskField | Should -Be 'retained'
$roundTrip.NetworkAdapters[0].ExtraNetworkField | Should -Be 'retained'
$roundTrip.Processors[0].ExtraProcessorField | Should -Be 'retained'
$roundTrip.PhysicalMemory[0].ConfiguredClockSpeed | Should -Be 4800
$roundTrip.PhysicalMemory[0].Speed | Should -Be 5600
$roundTrip.PhysicalMemory[0].Capacity | Should -Be 8GB
$roundTrip.TotalPhysicalMemory | Should -Be 6.5GB
$roundTrip.NativeOSArchitecture | Should -Be '64-bit'
}
}

It 'excludes deep CIM metadata without truncating JSON or losing hardware properties' {
$metadata = [pscustomobject]@{ ClassName = 'TestClass' }
$metadata | Add-Member -NotePropertyName Parent -NotePropertyValue $metadata
$classWin32DiskDrive = [pscustomobject]@{
Model = 'Disk'
DeviceID = 'disk0'
Size = [uint64]1TB
Capabilities = [uint16[]]@(3, 4)
CimClass = $metadata
CimInstanceProperties = $metadata
CimSystemProperties = $metadata
}
$classWin32NetworkAdapter = $classWin32DiskDrive
$classWin32Processor = $classWin32DiskDrive
$classWin32PhysicalMemory = $classWin32DiskDrive
$classWin32OperatingSystem = $null
$classWin32ComputerSystem = $null
$snapshot = & $mappingScript
$json = $snapshot | ConvertTo-Json -Depth 10 -WarningVariable serializationWarnings
@($serializationWarnings).Count | Should -Be 0
$roundTrip = $json | ConvertFrom-Json
foreach ($key in @('DiskDrives', 'NetworkAdapters', 'Processors', 'PhysicalMemory')) {
$snapshot[$key][0].PSObject.Properties.Name | Should -Not -Contain 'CimClass'
$snapshot[$key][0].PSObject.Properties.Name | Should -Not -Contain 'CimInstanceProperties'
$snapshot[$key][0].PSObject.Properties.Name | Should -Not -Contain 'CimSystemProperties'
$roundTrip.$key[0].Model | Should -Be 'Disk'
$roundTrip.$key[0].Size | Should -Be 1TB
$roundTrip.$key[0].Capabilities | Should -Be @(3, 4)
}
$classWin32DiskDrive.CimClass | Should -Not -BeNullOrEmpty
}

It 'normalizes <Count> collected objects to arrays without null placeholders' -ForEach @(
@{ Count = 0 }
@{ Count = 1 }
@{ Count = 2 }
) {
$objects = @(
[pscustomobject]@{ Name = 'One' }
[pscustomobject]@{ Name = 'Two' }
)
$classWin32DiskDrive = $objects | Select-Object -First $Count
$classWin32NetworkAdapter = $objects | Select-Object -First $Count
$classWin32Processor = $objects | Select-Object -First $Count
$classWin32PhysicalMemory = $objects | Select-Object -First $Count
$classWin32OperatingSystem = $null
$classWin32ComputerSystem = $null
$snapshot = & $mappingScript
foreach ($key in @('DiskDrives', 'NetworkAdapters', 'Processors', 'PhysicalMemory')) {
($snapshot[$key] -is [array]) | Should -BeTrue
$snapshot[$key].Count | Should -Be $Count
}
$snapshot.TotalPhysicalMemory | Should -BeNullOrEmpty
$snapshot.NativeOSArchitecture | Should -BeNullOrEmpty
}
}
52 changes: 51 additions & 1 deletion OSDCloud/private/core-device/Initialize-OSDCoreDevice.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ function Initialize-OSDCoreDevice {
before workflow execution so downstream steps can rely on a consistent
device state snapshot.

The snapshot includes all hardware properties in the Processors,
DiskDrives, and NetworkAdapters inventories for display consumers,
excluding CIM infrastructure metadata to keep diagnostic JSON serializable.
It also includes raw TotalPhysicalMemory bytes and NativeOSArchitecture
(without overrides). NetworkAdapters includes IPv4Addresses matched by
adapter index from the collected IP-enabled network configurations.
PhysicalMemory includes the installed memory module inventory, including
Speed and ConfiguredClockSpeed, without CIM infrastructure metadata.

.EXAMPLE
Initialize-OSDCoreDevice

Expand Down Expand Up @@ -55,6 +64,18 @@ function Initialize-OSDCoreDevice {
- Updates global state in $global:OSDCoreDevice.

Changelog:
- 2026-10-10 | pending | Collect physical memory module inventory.
Added Win32_PhysicalMemory diagnostics and the PhysicalMemory snapshot
array for memory speed and installed DIMM count display.
- 2026-10-10 | pending | Include per-adapter IPv4 addresses for device display.
Matched existing network configurations by adapter index and retained
only IPv4 addresses without issuing additional hardware queries.
- 2026-10-10 | pending | Prevent hardware inventory JSON truncation.
Excluded CIM infrastructure metadata from display inventory snapshots
while retaining all hardware properties and complete per-class logs.
- 2026-10-10 | pending | Expose existing hardware inventory for device display.
Added complete processor, disk drive, and network adapter arrays,
raw memory bytes, and native OS architecture without additional queries.
- 2026-08-13 | pending | Add OSDeploy identity and license properties.
Added nullable idOSDeployDevice, idRegisteredEmail, and idRegisteredLicense values
from WinPE environment variables or local license discovery.
Expand Down Expand Up @@ -237,6 +258,16 @@ function Initialize-OSDCoreDevice {
$classWin32OperatingSystem | Out-File (Join-Path -Path $LogsPath -ChildPath 'Win32_OperatingSystem.txt') -Width 4096 -Force
$OSArchitecture = $classWin32OperatingSystem.OSArchitecture | ConvertTo-TrimmedString
#=================================================
# Win32_PhysicalMemory
$classWin32PhysicalMemory = @()
try {
$classWin32PhysicalMemory = @(Get-CimInstance -ClassName Win32_PhysicalMemory -ErrorAction Stop | Select-Object -Property *)
}
catch {
Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to query Win32_PhysicalMemory: $($_.Exception.Message)"
}
$classWin32PhysicalMemory | Out-File (Join-Path -Path $LogsPath -ChildPath 'Win32_PhysicalMemory.txt') -Width 4096 -Force
#=================================================
# Win32_PnPEntity
$classWin32PnPEntity = Get-CimInstance -ClassName Win32_PnPEntity | Select-Object -Property *
$classWin32PnPEntityError = $classWin32PnPEntity | Where-Object { $_.Status -eq 'Error' } | Sort-Object HardwareID -Unique | Sort-Object Name
Expand Down Expand Up @@ -765,6 +796,7 @@ function Initialize-OSDCoreDevice {
ComputerSystemProduct = [System.String]$ComputerSystemProduct
ComputerSystemSKU = [System.String]$ComputerSystemSKU
ComputerSystemType = [System.String]$ComputerSystemType
DiskDrives = @($classWin32DiskDrive | Where-Object { $null -ne $_ } | Select-Object -Property * -ExcludeProperty CimClass, CimInstanceProperties, CimSystemProperties)
HardwareHash = [System.String]$HardwareHash
IsAutopilotSpec = [System.Boolean]$IsAutopilotSpec
IsDesktop = [System.Boolean]$IsDesktop
Expand All @@ -782,15 +814,34 @@ function Initialize-OSDCoreDevice {
LocalDisk = $LocalDisk
LocalPartition = $LocalPartition
LocalVolume = $LocalVolume
NativeOSArchitecture = $classWin32OperatingSystem.OSArchitecture
NetGateways = $NetGateways
NetIPAddress = $NetIPAddress
NetMacAddress = $NetMacAddress
NetworkAdapters = @($classWin32NetworkAdapter | Where-Object { $null -ne $_ } |
Select-Object -Property * -ExcludeProperty CimClass, CimInstanceProperties, CimSystemProperties |
ForEach-Object {
$adapterIndex = $_.Index
$ipv4Addresses = [string[]]@($classWin32NetworkAdapterConfiguration |
Where-Object { $null -ne $adapterIndex -and $_.Index -eq $adapterIndex } |
ForEach-Object { $_.IPAddress } |
Where-Object {
$address = $null
[System.Net.IPAddress]::TryParse($_, [ref]$address) -and
$address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork
})
$_ | Add-Member -NotePropertyName IPv4Addresses -NotePropertyValue $ipv4Addresses -PassThru
}
)
OSArchitecture = $OSArchitecture
OSVersion = $classWin32OperatingSystem.Version
PhysicalMemory = @($classWin32PhysicalMemory | Where-Object { $null -ne $_ } | Select-Object -Property * -ExcludeProperty CimClass, CimInstanceProperties, CimSystemProperties)
ProcessorArchitecture = $ProcessorArchitecture
Processors = @($classWin32Processor | Where-Object { $null -ne $_ } | Select-Object -Property * -ExcludeProperty CimClass, CimInstanceProperties, CimSystemProperties)
SerialNumber = $SerialNumber
SystemFirmwareHardwareId = $SystemFirmwareHardwareId
TimeZone = $classWin32TimeZone.StandardName
TotalPhysicalMemory = $classWin32ComputerSystem.TotalPhysicalMemory
TotalPhysicalMemoryGB = $TotalPhysicalMemoryGB
TpmIsActivated = $DeviceTpmIsActivated
TpmIsEnabled = $DeviceTpmIsEnabled
Expand All @@ -809,7 +860,6 @@ function Initialize-OSDCoreDevice {
idRegisteredEmail = $idRegisteredEmail
idRegisteredLicense = $idRegisteredLicense
}
Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Ready: OSDCoreDevice"
#=================================================
# Export OSDCoreDevice to XML and JSON for use in other scripts or workflows
$OSDCoreDeviceClixmlPath = Join-Path -Path $LogsPath -ChildPath 'OSDCoreDevice.xml'
Expand Down
Loading
Loading