Public/Get-HDTMachineFact.ps1
|
function Get-HDTMachineFact { <# .SYNOPSIS Gathers the machine facts through injected services. .DESCRIPTION HDT's replacement for ZTIGather.wsf. It produces the gathered-facts layer of the variable model - hardware, firmware, chassis and network - as an ordered, case-insensitive dictionary keyed by HDT variable name. It touches nothing itself. Every value arrives through one of three injected services, which is why the whole fact table can be proven under Pester against captured fixtures with no machine attached, and why a BIOS machine, an ARM machine and a machine with no TPM are all testable from a desk that has none of them. There is deliberately no Get-CimInstance, Get-ItemProperty or $env: reference anywhere in this file. CIM classes are queried once each, in a fixed order, and everything is derived from those locals - a second query is a second round trip to WMI, which is slow in WinPE. The order is asserted by a test: Win32_ComputerSystem, Win32_ComputerSystemProduct, Win32_BaseBoard, Win32_BIOS, Win32_SystemEnclosure, Win32_NetworkAdapterConfiguration, Win32_Tpm The first four are required: a deployment that cannot read Win32_ComputerSystem has no facts to rule on and must fail loudly. The last three are optional and degrade to "no instances" - WinPE without the TPM optional component, a VM with no enclosure data and a machine with no IP enabled adapter are all normal in the field. Network facts come from Win32_NetworkAdapterConfiguration rather than Get-NetIPAddress because WinPE has no NetTCPIP module, which a boot test confirmed. The IPEnabled filtering is done here, not by the provider: ICimProvider has no -Filter, so every implementation returns the unfiltered set. What this deliberately does NOT produce: HDTBootMode, which only the boot path knows (phase 05); HDTComputerName and HDTTimeZone, which rules decide rather than hardware; and anything named _HDT*, which is engine-owned. .PARAMETER CimProvider An ICimProvider - New-HDTCimProvider in production, New-HDTFakeCimProvider in a test. .PARAMETER RegistryService An IRegistryService, used only to read the Secure Boot state key. .PARAMETER EnvironmentProvider An IEnvironmentProvider, used for firmware_type and the processor architecture variables. .OUTPUTS System.Collections.Specialized.OrderedDictionary. Ordered so a facts.json diff stays readable, case-insensitive so a hand-written rules.yaml may spell a variable however it likes. .EXAMPLE $fact = Get-HDTMachineFact ` -CimProvider (New-HDTCimProvider) ` -RegistryService (New-HDTRegistryService) ` -EnvironmentProvider (New-HDTEnvironmentProvider) $fact['HDTModel'] Gathers from the live machine through the real adapters. .EXAMPLE $cim = New-HDTFakeCimProvider -FixturePath ./tests/fixtures/cim ` -NamespaceFixturePath @{ 'root/cimv2/security/microsofttpm' = './tests/fixtures/cim-microsofttpm' } Get-HDTMachineFact -CimProvider $cim ` -RegistryService (New-HDTFakeRegistryService) ` -EnvironmentProvider (New-HDTFakeEnvironmentProvider -Variable @{ firmware_type = 'BIOS' }) The same gatherer against captured fixtures, standing in for a BIOS machine with no Secure Boot key. .NOTES The chassis type tables (laptop 8, 9, 10, 11, 12, 14, 18, 21; desktop 3, 4, 5, 6, 7, 15, 16; server 23), the virtual-machine manufacturer list, and the PROCESSOR_ARCHITEW6432 precedence are derived from PSD (friendsOfMDT), Scripts/PSDGather.psm1, function Get-PSDLocalInfo. PSD is MIT licensed; see NOTICE.md at the repository root. Nothing else is carried across - PSD calls Get-CimInstance inline, which PROJECT constraint 4 forbids here. #> [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $CimProvider, [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $RegistryService, [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $EnvironmentProvider ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # -- required classes: no facts means no deployment, so these throw -------- $computerSystem = @($CimProvider.GetInstance('Win32_ComputerSystem')) $computerSystemProduct = @($CimProvider.GetInstance('Win32_ComputerSystemProduct')) $baseBoard = @($CimProvider.GetInstance('Win32_BaseBoard')) $bios = @($CimProvider.GetInstance('Win32_BIOS')) # -- optional classes: absent is a fact, not a failure -------------------- $enclosure = @() try { $enclosure = @($CimProvider.GetInstance('Win32_SystemEnclosure')) } catch { $enclosure = @() } $adapter = @() try { $adapter = @($CimProvider.GetInstance('Win32_NetworkAdapterConfiguration')) } catch { $adapter = @() } $tpm = @() try { $tpm = @($CimProvider.GetInstance('root/cimv2/security/microsofttpm', 'Win32_Tpm')) } catch { $tpm = @() } # -- identity ------------------------------------------------------------- $make = '' $model = '' $systemSku = '' $memoryByte = 0 if ($computerSystem.Count -gt 0) { $make = ([string] $computerSystem[0].Manufacturer).Trim() $model = ([string] $computerSystem[0].Model).Trim() $systemSku = ([string] $computerSystem[0].SystemSKUNumber).Trim() $memoryByte = [double] $computerSystem[0].TotalPhysicalMemory } $boardManufacturer = '' $product = '' if ($baseBoard.Count -gt 0) { $boardManufacturer = ([string] $baseBoard[0].Manufacturer).Trim() $product = ([string] $baseBoard[0].Product).Trim() } # A VM often reports an empty Manufacturer or Model on the computer system # and the truth on the baseboard. Reuse the single Win32_BaseBoard result. if ([string]::IsNullOrWhiteSpace($make)) { $make = $boardManufacturer } if ([string]::IsNullOrWhiteSpace($model)) { $model = $product } $serialNumber = '' if ($bios.Count -gt 0) { $serialNumber = ([string] $bios[0].SerialNumber).Trim() } $uuid = '' if ($computerSystemProduct.Count -gt 0) { $uuid = ([string] $computerSystemProduct[0].UUID).Trim().ToUpperInvariant() } # Floor, never a cast: [int] on a double rounds to even, which would make the # reported megabytes depend on the machine rather than on the memory. $memoryMegabyte = [int] [math]::Floor($memoryByte / 1MB) # -- chassis (PSD PSDGather.psm1, Get-PSDLocalInfo) ------------------------ $isDesktop = $false $isLaptop = $false $isServer = $false if ($enclosure.Count -gt 0) { $chassisType = @($enclosure[0].ChassisTypes) if ($chassisType.Count -gt 0) { $primaryChassis = [int] $chassisType[0] $isLaptop = (@(8, 9, 10, 11, 12, 14, 18, 21) -contains $primaryChassis) $isDesktop = (@(3, 4, 5, 6, 7, 15, 16) -contains $primaryChassis) $isServer = (@(23) -contains $primaryChassis) } } # -- virtualisation (PSD PSDGather.psm1, Get-PSDLocalInfo) ----------------- $isVirtualMachine = $false if (($make -like '*Microsoft*') -and ($model -like 'Virtual Machine')) { $isVirtualMachine = $true } foreach ($pattern in @('*VMware*', '*innotek*', '*VirtualBox*', '*QEMU*', '*KVM*', '*Xen*', '*Parallels*')) { if ($make -like $pattern) { $isVirtualMachine = $true } } # -- firmware, architecture, Secure Boot ---------------------------------- $firmwareType = [string] $EnvironmentProvider.GetVariable('firmware_type') $isUefi = ($firmwareType.Trim() -eq 'UEFI') # A 32-bit host process on a 64-bit machine reports x86 in # PROCESSOR_ARCHITECTURE and the truth in PROCESSOR_ARCHITEW6432. $architectureRaw = [string] $EnvironmentProvider.GetVariable('PROCESSOR_ARCHITEW6432') if ([string]::IsNullOrWhiteSpace($architectureRaw)) { $architectureRaw = [string] $EnvironmentProvider.GetVariable('PROCESSOR_ARCHITECTURE') } $architectureRaw = $architectureRaw.Trim() $architecture = $architectureRaw.ToUpperInvariant() if ($architecture -eq 'AMD64') { $architecture = 'x64' } # A BIOS machine has no SecureBoot\State key at all; the service returns # $null for that and this resolves to $false rather than throwing. $secureBootValue = $RegistryService.GetValue('HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\State', 'UEFISecureBootEnabled') $secureBootEnabled = $false if ($null -ne $secureBootValue) { $secureBootEnabled = ([string] $secureBootValue -eq '1') } # -- TPM ------------------------------------------------------------------ # SpecVersion is '2.0, 0, 1.38'; only the first component is the spec. $tpmVersion = $null if ($tpm.Count -gt 0) { $specVersion = [string] $tpm[0].SpecVersion if (-not [string]::IsNullOrWhiteSpace($specVersion)) { $tpmVersion = ($specVersion -split ',')[0].Trim() } } # -- network -------------------------------------------------------------- $macAddress = New-Object -TypeName System.Collections.ArrayList $ipAddress = New-Object -TypeName System.Collections.ArrayList $defaultGateway = New-Object -TypeName System.Collections.ArrayList foreach ($current in @($adapter | Where-Object { $_.IPEnabled } | Sort-Object -Property Index)) { if ($null -ne $current.MACAddress) { [void] $macAddress.Add([string] $current.MACAddress) } foreach ($address in @($current.IPAddress)) { if ($null -ne $address) { [void] $ipAddress.Add([string] $address) } } foreach ($gateway in @($current.DefaultIPGateway)) { if ($null -ne $gateway) { [void] $defaultGateway.Add([string] $gateway) } } } # -- the fact table ------------------------------------------------------- $fact = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::OrdinalIgnoreCase) $fact['HDTMake'] = $make $fact['HDTModel'] = $model $fact['HDTProduct'] = $product $fact['HDTSerialNumber'] = $serialNumber $fact['HDTUUID'] = $uuid $fact['HDTSystemSKU'] = $systemSku $fact['HDTMemory'] = $memoryMegabyte $fact['HDTArchitecture'] = $architecture # MDT SETS DeploymentType IN ZTIGather AND GATES WHOLE GROUPS ON IT. This # engine performs bare-metal installs only, so there is one value - and it # is written anyway, so a sequence can be conditioned on it now rather than # every group predating the refresh path having to be retrofitted later. # See Get-HDTVariableMap for the rest of MDT's set. $fact['HDTDeploymentType'] = 'NEWCOMPUTER' $fact['HDTIsUEFI'] = [bool] $isUefi $fact['HDTSecureBootEnabled'] = [bool] $secureBootEnabled $fact['HDTTPMVersion'] = $tpmVersion $fact['HDTIsDesktop'] = [bool] $isDesktop $fact['HDTIsLaptop'] = [bool] $isLaptop $fact['HDTIsServer'] = [bool] $isServer $fact['HDTIsVM'] = [bool] $isVirtualMachine $fact['HDTMacAddress'] = [string[]] @($macAddress) $fact['HDTIPAddress'] = [string[]] @($ipAddress) $fact['HDTDefaultGateway'] = [string[]] @($defaultGateway) return $fact } |