AzStackHciHardware/AzStackHci.Hardware.Helpers.psm1
class HealthModel { # Attributes for Azure Monitor schema [string]$Name #Name of the individual test/rule/alert that was executed. Unique, not exposed to the customer. [string]$Title #User-facing name; one or more sentences indicating the direct issue. [string]$Severity #Severity of the result (Critical, Warning, Informational, Hidden) – this answers how important the result is. Critical is the only update-blocking severity. [string]$Description #Detailed overview of the issue and what impact the issue has on the stamp. [psobject]$Tags #Key-value pairs that allow grouping/filtering individual tests. For example, "Group": "ReadinessChecks", "UpdateType": "ClusterAware" [string]$Status #The status of the check running (i.e. Failed, Succeeded, In Progress) – this answers whether the check ran, and passed or failed. [string]$Remediation #Set of steps that can be taken to resolve the issue found. [string]$TargetResourceID #The unique identifier for the affected resource (such as a node or drive). [string]$TargetResourceName #The name of the affected resource. [string]$TargetResourceType #The type of resource being referred to (well-known set of nouns in infrastructure, aligning with Monitoring). [datetime]$Timestamp #The Time in which the HealthCheck was called. [psobject[]]$AdditionalData #Property bag of key value pairs for additional information. [string]$HealthCheckSource #The name of the services called for the HealthCheck (I.E. Test-AzureStack, Test-Cluster). } class AzStackHciHardwareTarget : HealthModel {} Import-LocalizedData -BindingVariable lhwTxt -FileName AzStackHci.Hardware.Strings.psd1 function Test-Processor { <# .SYNOPSIS Test CPU .DESCRIPTION Test CPU #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $cimParams = @{ ClassName = 'Win32_Processor' Property = '*' } $cimData = @(Get-CimInstance @cimParams) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $PropertyResult = @() $PropertySyncResult = @() $matchProperty = @( 'Caption' 'Family' 'Manufacturer' 'MaxClockSpeed' 'NumberOfCores' 'NumberOfEnabledCore' 'NumberOfLogicalProcessors' 'ThreadCount' ) $desiredPropertyValue = @{ AddressWidth = @{ value = 64; hint = '64-bit' } Architecture = @{ value = 9; hint = '64-bit' } # x64 Availability = @{ value = 3; hint = 'Running/Full Power' } # Running/Full Power CpuStatus = @{ value = 1; hint = 'CPU Enabled' } # CPU Enabled DataWidth = @{ value = 64; hint = '64-bit' } # x64 ProcessorType = @{ value = 3; hint = 'Central Processor' } # Central Processor Status = @{ value = 'OK'; hint = 'OK' } SecondLevelAddressTranslationExtensions = @{ value = $true; hint = 'Virtualization Support' } VirtualizationFirmwareEnabled = @{ value = $true; hint = 'Virtualization Support' } VMMonitorModeExtensions = @{ value = $true; hint = 'Virtualization Support' } } # if Hypervisorpresent is all true, SecondLevelAddressTranslationExtensions, VirtualizationFirmwareEnabled, VMMonitorModeExtensions should not be tested $CheckHyperVisor = IsHypervisorPresent -PsSession $PsSession if (($CheckHyperVisor | Select-Object -ExpandProperty HypervisorPresent) -notcontains $false) { Log-Info "HypervisorPresent: removing SecondLevelAddressTranslationExtensions, VirtualizationFirmwareEnabled, VMMonitorModeExtensions as properties to test" $desiredPropertyValue.Remove('SecondLevelAddressTranslationExtensions') $desiredPropertyValue.Remove('VirtualizationFirmwareEnabled') $desiredPropertyValue.Remove('VMMonitorModeExtensions') } else { Log-Info ($lhwTxt.HypervisorNotPresent -f (($CheckHyperVisor | ForEach-Object {"{0}:{1}" -f $_.Name, $_.HypervisorPresent }) -join ',')) -Type Warning } $instanceIdStr = 'Write-Output "Machine: $($instance.SystemName), Class: $ClassName, Instance: $($instance.DeviceId)"' # Check property sync for nodes individually $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } Log-Info -Message ($lhwTxt.ProcessorCount -f $systemName, $sData.Count) $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $desiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Warning $PropertySyncResult += Test-PropertySync -CimData $sData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning } # Check property sync for all nodes as well $PropertySyncResult += Test-PropertySync -CimData $cimData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning return @($PropertyResult + $PropertySyncResult) } catch { throw $_ } } function IsHypervisorPresent { <# .SYNOPSIS Retrieves HypervisorPresent property from Win32_ComputerSystem #> [cmdletbinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $cimParams = @{ ClassName = 'Win32_ComputerSystem' Property = 'HypervisorPresent' } $cimData = @(Get-CimInstance @cimParams) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } return $cimData } catch { throw $_ } } function Test-NetAdapter { <# .SYNOPSIS Test Network Adapter .DESCRIPTION Test Network Adapter #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $cimData = @(Get-NetAdapter -Physical | Where-Object { $_.NdisMedium -eq 0 -and $_.Status -eq 'Up' -and $_.NdisPhysicalMedium -eq 14 -and $_.PnPDeviceID -notlike 'USB\*'}) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } if (-not $cimData) { $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_NetworkAdapter' $instanceResult.Title = 'Test Network Adapater' $instanceResult.Severity = 'Critical' $instanceResult.Description = 'Checking Network Adapaters' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = if ([string]::IsNullOrEmpty($PsSession.PSComputerName)) { "Machine: $env:COMPUTERNAME, Class: NetworkAdapter" } else { "Machine: $($PsSession.PSComputerName), Class: NetworkAdapter" } $instanceResult.TargetResourceName = if ([string]::IsNullOrEmpty($PsSession.PSComputerName)) { "Machine: $env:COMPUTERNAME, Class: NetworkAdapter" } else { "Machine: $($PsSession.PSComputerName), Class: NetworkAdapter" } $instanceResult.TargetResourceType = 'NetworkAdapter' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $instanceResult.Status = 'Failed' $instanceResult.AdditionalData = New-Object -TypeName PSObject -Property @{ Source = if ([string]::IsNullOrEmpty($PsSession.PSComputerName)) { $ENV:COMPUTERNAME } else { $PsSession.PSComputerName } Resource = $cimData Detail = $lhwTxt.NicCount -f $instanceResult.TargetResourceID, '0' Status = 'Failed' TimeStamp = [datetime]::UtcNow } return $InstanceResult } $PropertyResult = @() $PropertySyncResult = @() $GroupResult = @() $CountResult = @() # Blocking properties $criticalMatchProperty = @( 'ActiveMaximumTransmissionUnit' 'DriverDate' 'DriverDescription' 'DriverMajorNdisVersion' 'DriverMinorNdisVersion' 'DriverProvider' 'DriverVersionString' 'MajorDriverVersion' 'MinorDriverVersion' 'MtuSize' ) # non-block warning properties $warningMatchProperty = @( 'ReceiveLinkSpeed' 'Speed' 'TransmitLinkSpeed' 'VlanID' ) $desiredPropertyValue = @{ AdminLocked = $false ConnectorPresent = $true EndpointInterface = $false ErrorDescription = $null FullDuplex = $true HardwareInterface = $true Hidden = $false IMFilter = $false InterfaceAdminStatus = @{ value = 1; hint = 'Up' } # Up InterfaceOperationalStatus = @{ value = 1; hint = 'Up' } # Up iSCSIInterface = $false LastErrorCode = $null MediaConnectState = @{ value = 1; hint = 'Connected' } # Connected MediaDuplexState = 2 NdisMedium = @{ value = 0; hint = '802.3' } # 802.3 NdisPhysicalMedium = @{ value = 14; hint = '802.3' } # 802.3 OperationalStatusDownDefaultPortNotAuthenticated = $false OperationalStatusDownInterfacePaused = $false OperationalStatusDownLowPowerState = $false OperationalStatusDownMediaDisconnected = $false #PromiscuousMode = $false State = @{ value = 2; hint = 'Started' } # 802.3 # Started #Status = 'Up' Virtual = $false } $groupProperty = @( 'DriverDescription' ) $minimum = 1 $instanceIdStr = 'Write-Output "Machine: $($instance.SystemName), ClassName: $ClassName, Instance: $($instance.Name), Description: $($instance.InterfaceDescription), Address: $($instance.PermanentAddress)"' # Check property sync for nodes individually $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } Log-Info -Message ($lhwTxt.NicCount -f $systemName, $sData.Count) # Make sure each system has the requisite number of Network Adapters $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $desiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Critical $CountResult += Test-Count -CimData $sData -minimum $minimum -ValidatorName 'Hardware' -Severity Critical } # Check property sync for all nodes as well $GroupResult += Test-GroupProperty -CimData $cimData -GroupProperty $groupProperty -MatchProperty $warningMatchProperty -ValidatorName Hardware -Severity Warning $GroupResult += Test-GroupProperty -CimData $cimData -GroupProperty $groupProperty -MatchProperty $criticalMatchProperty -ValidatorName Hardware -Severity Critical $InstanceCount += Test-InstanceCount -CimData $cimData -Severity Critical -ValidatorName 'Hardware' $InstanceCountByGroup += Test-InstanceCountByGroup -CimData $cimData -ValidatorName 'Hardware' -GroupProperty $groupProperty -Severity Critical # Finally, the all properties from the $matchProperty array have to be compared for all instances across all nodes. return @($PropertyResult + $GroupResult + $CountResult + $InstanceCountByGroup + $InstanceCount) } catch { throw $_ } } function Test-MemoryCapacity { <# .SYNOPSIS Test Memory .DESCRIPTION Test Memory #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $minimumMemory = 32GB $instanceResults = @() $AdditionalData = @() $sb = { $cimParams = @{ ClassName = 'Win32_PhysicalMemory' Property = '*' } $cimData = @(Get-CimInstance @cimParams) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } # Check property sync for nodes individually $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique $totalMemoryLocalNode = $cimData | Where-Object { $_.CimSystemProperties.ServerName -like "$($ENV:COMPUTERNAME)*"} | Measure-Object -Property Capacity -Sum | Select-Object -ExpandProperty Sum $instanceResults += foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } $instanceId = "Machine: $($Instance.CimSystemProperties.ServerName), Class: $ClassName, Instance: All" $totalMemory = $sData | Measure-Object -Property Capacity -Sum $dtl = $lhwTxt.MemoryCapacity -f $systemName, $totalMemory.Sum, $minimumMemory, $totalMemoryLocalNode if ($totalMemory.Sum -lt $minimumMemory -or $totalMemory.Sum -lt $totalMemoryLocalNode) { $Status = 'Failed' Log-Info $dtl -Type Warning } else { $Status = 'Succeeded' Log-Info $dtl } $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_MemoryCapacity' $instanceResult.Title = 'Test Memory Capacity' $instanceResult.Severity = 'Warning' $instanceResult.Description = 'Checking Memory Capacity' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = $instanceId $instanceResult.TargetResourceName = $instanceId $instanceResult.TargetResourceType = 'Memory' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $instanceResult.Status = $status $instanceResult.AdditionalData += New-Object -TypeName PSObject -Property @{ Source = 'Memory Capacity' Resource = $totalMemory.Sum Detail = $dtl Status = $status TimeStamp = [datetime]::UtcNow } $instanceResult } return $instanceResults } catch { throw $_ } } function Test-MemoryProperties { <# .SYNOPSIS Test Memory .DESCRIPTION Test Memory #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $cimParams = @{ ClassName = 'Win32_PhysicalMemory' Property = '*' } $cimData = @(Get-CimInstance @cimParams) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $PropertyResult = @() $PropertySyncResult = @() $matchProperty = @( 'ConfiguredClockSpeed' 'ConfiguredVoltage' 'MaxVoltage' 'MemoryType' 'SMBIOSMemoryType' 'Speed' 'TotalWidth' 'TypeDetail' ) $desiredPropertyValue = @{ DataWidth = @{ value = 64; hint = '64-bit' } # x64 FormFactor = @{ value = 8; hint = 'DIMM' } # DIMM } # Check property sync for nodes individually $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } $instanceIdStr = 'Write-Output "Machine: $($Instance.CimSystemProperties.ServerName), Class: $ClassName, Instance: $($instance.DeviceLocator), Tag: $($instance.Tag)"' $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $desiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Warning $PropertySyncResult += Test-PropertySync -CimData $sData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning } # Check property sync for all nodes as well $PropertySyncResult += Test-PropertySync -CimData $cimData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning return @($PropertyResult + $PropertySyncResult) } catch { throw $_ } } function Test-Gpu { <# .SYNOPSIS Test Gpu .DESCRIPTION Test Gpu #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $cimParams = @{ ClassName = 'Win32_VideoController' Property = '*' } $cimData = @(Get-CimInstance @cimParams) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $PropertyResult = @() $PropertySyncResult = @() $matchProperty = @( 'AdapterRam' 'Name' 'DriverDate' 'DriverVersion' 'VideoMemoryType' 'VideoProcessor' ) $desiredPropertyValue = @{ ConfigManagerErrorCode = @{ value = 0; hint = 'The device is working properly' } # The device is working properly Status = 'OK' } # Check property sync for nodes individually $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } $totalGpuRam = $sData | Measure-Object -Property AdapterRam -Sum Log-Info -Message ($lhwTxt.TotalGPUMem -f $systemName, $sData.Count, ($totalGpuRam.Sum / 1GB)) $instanceIdStr = 'Write-Output "Machine: $($instance.SystemName), Class: $ClassName, Instance: $($instance.DeviceID), Name: $($instance.Name)"' $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $desiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Warning $PropertySyncResult += Test-PropertySync -CimData $sData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning } # Check property sync for all nodes as well $PropertySyncResult += Test-PropertySync -CimData $cimData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning return @($PropertyResult + $PropertySyncResult) } catch { throw $_ } } function Test-Baseboard { <# .SYNOPSIS Test Baseboard .DESCRIPTION Test Baseboard #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $cimParams = @{ ClassName = 'Win32_Bios' Property = '*' } $cimData = @(Get-CimInstance @cimParams) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $PropertyResult = @() $PropertySyncResult = @() $matchProperty = @( #'BiosVersion' # this property is a string array and non-trivial to compare 'Caption' 'Description' 'EmbeddedControllerMajorVersion' 'EmbeddedControllerMinorVersion' 'Manufacturer' 'Name' 'ReleaseDate' 'SMBIOSBIOSVersion' 'SMBIOSMajorVersion' 'SMBIOSMinorVersion' 'SoftwareElementId' 'SystemBiosMajorVersion' 'SystemBiosMinorVersion' 'Version' ) $desiredPropertyValue = @{ SMBIOSPresent = $true SoftwareElementState = @{ value = 3; hint = 'Running' } # Running Status = 'OK' } # Check property sync for nodes individually $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } Log-Info -Message ($lhwTxt.TestBaseboard -f $systemName, $sData.Name, $sData.SerialNumber) $instanceIdStr = 'Write-Output "Machine: $($instance.CimSystemProperties.ServerName), Class: $ClassName, Serial: $($instance.SerialNumber)"' $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $desiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Warning $PropertySyncResult += Test-PropertySync -CimData $sData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning } # Check property sync for all nodes as well $PropertySyncResult += Test-PropertySync -CimData $cimData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning return @($PropertyResult + $PropertySyncResult) } catch { throw $_ } } function Test-Model { <# .SYNOPSIS Test Hardware Model is the same .DESCRIPTION Test Hardware Model is the same #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $cimParams = @{ ClassName = 'Win32_ComputerSystem' Property = '*' } $cimData = @(Get-CimInstance @cimParams) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $PropertySyncResult = @() $matchProperty = @( 'Manufacturer' 'Model' ) # Check property sync for nodes individually $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } Log-Info -Message ($lhwTxt.TestModel -f $systemName, $sData.Manufacturer, $sData.Model) $PropertySyncResult += Test-PropertySync -CimData $sData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Critical } # Check property sync for all nodes as well $PropertySyncResult += Test-PropertySync -CimData $cimData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Critical return @($PropertyResult + $PropertySyncResult) } catch { throw $_ } } function Test-PhysicalDisk { <# .SYNOPSIS Test Physical Disk .DESCRIPTION Test Physical Disk #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $allowedBusTypes = @('SATA', 'SAS', 'NVMe', 'SCM') $allowedMediaTypes = @('HDD', 'SSD', 'SCM') $bootPhysicalDisk = Get-Disk | Where-Object {$_.IsBoot -or $_.IsSystem} | Get-PhysicalDisk $cimData = @(Get-StorageNode -Name $env:COMPUTERNAME* | Get-PhysicalDisk -PhysicallyConnected | Where-Object { $_.BusType -in $allowedBusTypes -and $_.MediaType -in $allowedMediaTypes -and $_.DeviceId -notin $bootPhysicalDisk.DeviceId}) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } if (-not $cimData) { $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_PhysicalDisk' $instanceResult.Title = 'Test Physical Disk' $instanceResult.Severity = 'Critical' $instanceResult.Description = 'Checking Physical Disk' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = if ([string]::IsNullOrEmpty($PsSession.PSComputerName)) { "Machine: $env:COMPUTERNAME, Class: Physical Disk" } else { "Machine: $($PsSession.PSComputerName), Class: PhysicalDisk" } $instanceResult.TargetResourceName = if ([string]::IsNullOrEmpty($PsSession.PSComputerName)) { "Machine: $env:COMPUTERNAME, Class: Physical Disk" } else { "Machine: $($PsSession.PSComputerName), Class: PhysicalDisk" } $instanceResult.TargetResourceType = 'Physical Disk' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $instanceResult.Status = 'Failed' $instanceResult.AdditionalData = New-Object -TypeName PSObject -Property @{ Source = if ([string]::IsNullOrEmpty($PsSession.PSComputerName)) { $ENV:COMPUTERNAME } else { $PsSession.PSComputerName } Resource = $cimData Detail = $lhwTxt.DiskTotal -f $instanceResult.TargetResourceID, '0','0' Status = 'Failed' TimeStamp = [datetime]::UtcNow } return $InstanceResult } $PropertyResult = @() $GroupResult = @() $CountResult = @() $InstanceCount = @() $InstanceCountByGroup = @() $matchProperty = @( 'FirmwareVersion' ) $groupProperty = @( 'FriendlyName' ) $warningDesiredPropertyValue = @{ HealthStatus = @{ value = @('Healthy', 0); hint = 'Healthy' } # Healthy IsIndicationEnabled = @{ value = @($false, $null); hint = 'Indicator Off' } OperationalStatus = @{ value = @('OK', 2); hint = 'OK' } } $instanceIdStr = 'Write-Output "Machine: $($instance.CimSystemProperties.ServerName), Class: $ClassName, Location: $($instance.PhysicalLocation), Unique ID: $($instance.UniqueId), Size: $("{0:N2}" -f ($instance.Size / 1TB)) TB"' # Check disk count for nodes individually [array]$SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } $totalSize = $sData | Measure-Object -Property Size -Sum Log-Info -Message ($lhwTxt.DiskTotal -f $systemName, $($sData.Count), ('{0:N2}' -f ($totalSize.Sum / 1TB))) $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $warningDesiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Warning # Split disks into type $SSD = $sData | Where-Object {$_.MediaType -match 'SSD|4' -and $_.BusType -match 'SAS|10|SATA|11'} $NVMe = $sData | Where-Object {$_.MediaType -match 'SSD|4' -and $_.BusType -match 'NVMe|17'} $SCM = $sData | Where-Object {$_.MediaType -match 'SCM|5'} $HDD = $sData | Where-Object {$_.MediaType -match 'HDD|3'} Log-Info ("Drive types detected HDD: {0}, SSD:{1}, NVMe:{2}, SCM:{3}" -f [bool]$HDD, [bool]$SSD, [bool]$NVMe, [bool]$SCM) # As per https://docs.microsoft.com/en-us/windows-server/storage/storage-spaces/storage-spaces-direct-hardware-requirements $systemCountResult = @() $countCommonParams = @{ ValidatorName = 'Hardware' Severity = 'Critical' } # all flash minimum should be 2 # Drive type present (capacity only) Minimum drives required (Windows Server) Minimum drives required (Azure Stack HCI) # All persistent memory (same model) 4 persistent memory 2 persistent memory # All NVMe (same model) 4 NVMe 2 NVMe # All SSD (same model) 4 SSD 2 SSD if ($SSD -xor $NVMe -xor $SCM) { $systemCountResult += Test-Count -cimData $sData -Minimum 2 @countCommonParams } # Drive type present Minimum drives required # Persistent memory + NVMe or SSD 2 persistent memory + 4 NVMe or SSD # NVMe + SSD 2 NVMe + 4 SSD # NVMe + HDD 2 NVMe + 4 HDD # SSD + HDD 2 SSD + 4 HDD if ($SCM -and ($NVMe -or $SSD)) { $systemCountResult += Test-Count -cimData $SCM -Minimum 2 @countCommonParams if ($NVMe) { Log-Info ($lhwTxt.MinCountDiskType -f 'NVMe', '4', $systemName ) $systemCountResult += Test-Count -cimData $NVMe -Minimum 4 @countCommonParams } else { Log-Info ($lhwTxt.MinCountDiskType -f 'SSD', '4', $systemName ) $systemCountResult += Test-Count -cimData $SSD -Minimum 4 @countCommonParams } } if ($NVMe -and $SSD) { Log-Info ($lhwTxt.MinCountDiskType -f 'NVMe', '2', $systemName ) $systemCountResult += Test-Count -cimData $NVMe -Minimum 2 @countCommonParams Log-Info ($lhwTxt.MinCountDiskType -f 'SSD', '4', $systemName ) $systemCountResult += Test-Count -cimData $SSD -Minimum 4 @countCommonParams } if ($NVMe -and $HHD) { Log-Info ($lhwTxt.MinCountDiskType -f 'NVMe', '2', $systemName ) $systemCountResult += Test-Count -cimData $NVMe -Minimum 2 @countCommonParams Log-Info ($lhwTxt.MinCountDiskType -f 'HDD', '4', $systemName ) $systemCountResult += Test-Count -cimData $HDD -Minimum 4 @countCommonParams } if ($SSD -and $HHD) { Log-Info ($lhwTxt.MinCountDiskType -f 'SSD', '2', $systemName ) $systemCountResult += Test-Count -cimData $SSD -Minimum 2 @countCommonParams Log-Info ($lhwTxt.MinCountDiskType -f 'HDD', '4', $systemName ) $systemCountResult += Test-Count -cimData $HDD -Minimum 4 @countCommonParams } if ($systemCountResult.count -eq 0) { Log-Info "We did not determine the disk combination correctly for $systemName. Checking minimum as per deployment guide." -Type Warning $systemCountResult += Test-Count -cimData $sData -Minimum 3 @countCommonParams } $CountResult += $systemCountResult } # Check property sync for all nodes $GroupResult += Test-GroupProperty -CimData $cimData -GroupProperty $groupProperty -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning # Split disks into type and check each server has the same count $allSSD = $cimData | Where-Object {$_.MediaType -match 'SSD|4' -and $_.BusType -match 'SAS|10|SATA|11'} $allNVMe = $cimData | Where-Object {$_.MediaType -match 'SSD|4' -and $_.BusType -match 'NVMe|17'} $allSCM = $cimData | Where-Object {$_.MediaType -match 'SCM|5'} $allHDD = $cimData | Where-Object {$_.MediaType -match 'HDD|3'} if ($allSSD) { Log-Info ($lhwTxt.DiskInstanceCountByType -f 'SSD') $InstanceCount += Test-InstanceCount -CimData $allSSD -Severity Critical -ValidatorName 'Hardware' } if ($allNVMe) { Log-Info ($lhwTxt.DiskInstanceCountByType -f 'NVMe') $InstanceCount += Test-InstanceCount -CimData $allNVMe -Severity Critical -ValidatorName 'Hardware' } if ($allSCM) { Log-Info ($lhwTxt.DiskInstanceCountByType -f 'SCM') $InstanceCount += Test-InstanceCount -CimData $allSCM -Severity Critical -ValidatorName 'Hardware' } if ($allHDD) { Log-Info ($lhwTxt.DiskInstanceCountByType -f 'HDD') $InstanceCount += Test-InstanceCount -CimData $allHDD -Severity Critical -ValidatorName 'Hardware' } if ($SystemNames.Count -eq 1) { # Single Node deployments should be all flash [array]$CheckSingleNodeAllFlash = CheckSingleNodeAllFlash -CimData $cimData } # Do all servers have the same count regardless of type $InstanceCount += Test-InstanceCount -CimData $cimData -Severity Critical -ValidatorName 'Hardware' Log-Info ($lhwTxt.DiskInstanceCountByType -f 'ALL') # Finally, the all properties from the $matchProperty array (Firmware) have to be compared for all instances # across all nodes grouped by property (FriendlyName) $InstanceCountByGroup += Test-InstanceCountByGroup -CimData $cimData -ValidatorName 'Hardware' -GroupProperty $groupProperty -Severity Warning return @($PropertyResult + $GroupResult + $CountResult + $InstanceCount + $InstanceCountByGroup + $CheckSingleNodeAllFlash) } catch { throw $_ } } function Test-CanPool { <# .SYNOPSIS Test Physical Disks can pool value .DESCRIPTION Test Physical Disks can pool value #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $allowedBusTypes = @('SATA', 'SAS', 'NVMe', 'SCM') $allowedMediaTypes = @('HDD', 'SSD', 'SCM') $bootPhysicalDisk = Get-Disk | Where-Object {$_.IsBoot -or $_.IsSystem} | Get-PhysicalDisk $cimData = @(Get-StorageNode | Get-PhysicalDisk -PhysicallyConnected | Where-Object { $_.BusType -in $allowedBusTypes -and $_.MediaType -in $allowedMediaTypes -and $_.DeviceId -notin $bootPhysicalDisk.DeviceId}) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } if (-not $cimData) { $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_PhysicalDisk_CanPool' $instanceResult.Title = 'Test Physical Disks Can Pool' $instanceResult.Severity = 'Critical' $instanceResult.Description = 'Checking Physical Disk can pool property' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = if ([string]::IsNullOrEmpty($PsSession.PSComputerName)) { "Machine: $env:COMPUTERNAME, Class: Physical Disk" } else { "Machine: $($PsSession.PSComputerName), Class: PhysicalDisk" } $instanceResult.TargetResourceName = if ([string]::IsNullOrEmpty($PsSession.PSComputerName)) { "Machine: $env:COMPUTERNAME, Class: Physical Disk" } else { "Machine: $($PsSession.PSComputerName), Class: PhysicalDisk" } $instanceResult.TargetResourceType = 'Physical Disk' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $instanceResult.Status = 'Failed' $instanceResult.AdditionalData = New-Object -TypeName PSObject -Property @{ Source = if ([string]::IsNullOrEmpty($PsSession.PSComputerName)) { $ENV:COMPUTERNAME } else { $PsSession.PSComputerName } Resource = $cimData Detail = $lhwTxt.DiskTotal -f $instanceResult.TargetResourceID, '0','0' Status = 'Failed' TimeStamp = [datetime]::UtcNow } return $InstanceResult } $PropertyResult = @() $criticalDesiredPropertyValue = @{ CanPool = @{ value = @($global:PhysicalDiskCanPool); hint = 'Can Pool' } } $instanceIdStr = 'Write-Output "Machine: $($instance.CimSystemProperties.ServerName), Class: $ClassName, Location: $($instance.PhysicalLocation), Unique ID: $($instance.UniqueId), Size: $("{0:N2}" -f ($instance.Size / 1TB)) TB"' [array]$SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $criticalDesiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Critical } return $PropertyResult } catch { throw $_ } } function Test-TpmVersion { [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession, [Parameter()] $version = '2.0' ) $tpms = @() $InstanceResults = @() $sb = { $cim = New-Object PSObject @{ ComputerName = $ENV:COMPUTERNAME cim = Get-CimInstance -Namespace root/cimv2/Security/MicrosoftTpm -ClassName Win32_Tpm -ErrorAction SilentlyContinue } return $cim } if ([string]::IsNullOrEmpty($PsSession)) { $tpms += Invoke-Command -ScriptBlock $sb } else { $tpms += Invoke-Command -Session $PsSession -ScriptBlock $sb } foreach ($tpm in $tpms) { $computerName = $tpm.ComputerName Log-Info -Message ($lhwTxt.TestTpmVersion -f $computerName, $version) # Test properties $InstanceResults += foreach ($instance in $tpm.tpm) { $instanceId = "Machine: $computerName, Class: Tpm, Manufacturer ID: $($instance.ManufacturerId)" $instanceVersion = $instance.SpecVersion -split ',' | Select-Object -First 1 Log-Info -Message ($lhwTxt.Test -f 'TPM', $instanceId) $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_Test_Tpm_Version' $instanceResult.Title = 'Test TPM Version' $instanceResult.Severity = 'Critical' $instanceResult.Description = "Checking TPM for desired version ($instanceVersion)" $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = $instanceId $instanceResult.TargetResourceName = $instanceId $instanceResult.TargetResourceType = 'Tpm' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $AdditionalData = @() $AdditionalData += New-Object -TypeName PSObject -Property @{ Source = 'Version' Resource = $instanceVersion Detail = "$instanceId Tpm version is $instanceVersion" Status = if ($instanceVersion -eq $version) { 'Succeeded' } else { 'Failed' } TimeStamp = [datetime]::UtcNow } $instanceResult.AdditionalData = $AdditionalData $instanceResult.Status = if ($AdditionalData.Status -contains 'Failed') { 'Failed' } else { 'Succeeded' } $instanceResult } } return $InstanceResults } function Test-TpmProperties { [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $tpms = @() $InstanceResults = @() $sb = { $tpm = Get-Tpm New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName tpm = $tpm } } if ([string]::IsNullOrEmpty($PsSession)) { $tpms += Invoke-Command -ScriptBlock $sb } else { $tpms += Invoke-Command -Session $PsSession -ScriptBlock $sb } foreach ($tpm in $tpms) { $passed = $false $computerName = $tpm.ComputerName $desiredPropertyValue = @{ TpmPresent = $true #(Get-CimInstance -Namespace root/cimv2/Security/MicrosoftTpm -ClassName Win32_Tpm) is null TpmReady = $true #IsActivated() TpmEnabled = $true #IsEnabled() TpmActivated = $true #IsActivated() #TpmOwned = $true #IsOwned() #RestartPending = $false #GetPhysicalPresenceRequest()? ManagedAuthLevel = 'Full' #GetOwnerAuth()?? OwnerClearDisabled = $false #IsOwnerClearDisabled() AutoProvisioning = 'Enabled' #IsAutoProvisioningEnabled() LockedOut = $false #IsLockedOut() LockoutCount = 0 #GetCapLockoutInfo() } Log-Info -Message ($lhwTxt.TestTpm -f $computerName, $tpm.tpm.ManufacturerIdTxt, $tpm.tpm.ManufacturerVersion) # Test properties $InstanceResults += foreach ($instance in $tpm.tpm) { $instanceId = "Machine: $computerName, Class: Tpm, Manufacturer ID: $($tpm.tpm.ManufacturerId)" $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_Test_Tpm_Instance_Properties' $instanceResult.Title = 'Test TPM' $instanceResult.Severity = 'Critical' $instanceResult.Description = 'Checking TPM for desired properties' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = $instanceId $instanceResult.TargetResourceName = $instanceId $instanceResult.TargetResourceType = 'Tpm' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $AdditionalData = @() foreach ($propertyName in $desiredPropertyValue.Keys) { $detail = $null $passed = $false if ($instance.$propertyName -ne $desiredPropertyValue.$propertyName) { $passed = $false $detail = $lhwTxt.UnexProp -f $propertyName, $instance.$propertyName, $desiredPropertyValue.$propertyName Log-Info -Message $detail -Type Warning } else { $detail = $lhwTxt.Prop -f $propertyName, $instance.$propertyName, $desiredPropertyValue.$propertyName $passed = $true } $AdditionalData += New-Object -TypeName PSObject -Property @{ Source = $propertyName Resource = $instance.$propertyName Detail = $detail Status = if ($passed) { 'Succeeded' } else { 'Failed' } TimeStamp = [datetime]::UtcNow } } $instanceResult.AdditionalData = $AdditionalData $instanceResult.Status = if ($AdditionalData.Status -contains 'Failed') { 'Failed' } else { 'Succeeded' } $instanceResult } } return $InstanceResults } catch { throw $_ } } function Test-TpmCertificates { [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $allowedKeyUsage = '2.23.133.8.1' # Endorsement Key Certificate $allowedAlgorithms = @( '1.2.840.113549.1.1.11' # SHA256 '1.2.840.113549.1.1.12' # SHA384 '1.2.840.113549.1.1.13' # SHA512 '1.2.840.10045.4.3.2' # SHA256ECDSA '1.2.840.10045.4.3.3' # SHA384ECDSA '1.2.840.10045.4.3.4' # SHA512ECDSA ) $tpmKeys = @() $InstanceResults = @() $sb = { try { $tpmKeys = Get-TpmEndorsementKeyInfo -ErrorAction SilentlyContinue } catch {} return (New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName tpmKeys = $tpmKeys }) } if ([string]::IsNullOrEmpty($PsSession)) { $tpmKeys += Invoke-Command -ScriptBlock $sb } else { $tpmKeys += Invoke-Command -Session $PsSession -ScriptBlock $sb } $InstanceResults += foreach ($tpmKey in $tpmKeys) { $computerName = $tpmKey.ComputerName $tpmCert = $tpmKey.tpmKeys.ManufacturerCertificates + $tpmKey.tpmKeys.AdditionalCertificates $instanceId = "Machine: $computerName, Class: TpmCertificates, Subject: $($tpmKey.tpmKeys.ManufacturerCertificates.subject), Thumprint: $($tpmKey.tpmKeys.ManufacturerCertificates.Thumbprint)" $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_Test_Tpm_Certificate_Properties' $instanceResult.Title = 'Test TPM' $instanceResult.Severity = 'Critical' $instanceResult.Description = 'Checking TPM for desired certificates' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = $instanceId $instanceResult.TargetResourceName = $instanceId $instanceResult.TargetResourceType = 'TpmEndorsementKeyInfo' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $AdditionalData = @() foreach ($cert in $tpmCert) { $validCert = $false $certDetail = $null # Test TPM certificate expiration $now = [datetime]::UtcNow $sinceIssued = New-TimeSpan -Start $cert.NotBefore -End $now $untilExpired = New-TimeSpan -Start $now -End $cert.NotAfter $currentCert = $sinceIssued.Days -gt 0 -and $untilExpired.Days -gt 0 # Test TPM signature algorithm $validAlgo = $cert.SignatureAlgorithm.Value -in $allowedAlgorithms # Test TPM certificate Enhanced Key Usage $validUsage = $cert.EnhancedKeyUsageList.ObjectId -contains $allowedKeyUsage # Display certificate properties $validCert = $currentCert -and $validAlgo -and $validUsage [string[]]$certDetail = "TPM certificate $($cert.Thumbprint), valid = $validCert" $certDetail += " Issuer: $($cert.Issuer)" $certDetail += " Subject: $($cert.Subject)" $certDetail += " Key Usage: $($cert.EnhancedKeyUsageList.FriendlyName -join ', '), valid = $validUsage" #$cert.Extensions.Oid.FriendlyName | Foreach-Object { $certDetail += " Extension: $_" } $certDetail += " Valid from: $($cert.NotBefore) to $($cert.NotAfter), valid = $currentCert" $certDetail += " Algorithm: $($cert.SignatureAlgorithm.FriendlyName), valid = $validAlgo" $foundValidCert = $foundValidCert -or $validCert $AdditionalData += New-Object -TypeName PSObject -Property @{ Source = $cert.Thumbprint Resource = "Current: $currentCert. Valid Algorithm: $validAlgo. Valid Key Usage: $validUsage." Detail = ($certDetail -join "`r") Status = if ($validCert) { 'Succeeded' } else { 'Failed' } TimeStamp = [datetime]::UtcNow } } $instanceResult.AdditionalData = $AdditionalData $instanceResult.Status = if ($AdditionalData.Status -contains 'Succeeded') { 'Succeeded' } else { 'Failed' } $instanceResult } return $InstanceResults } catch { throw $_ } } function Test-SecureBoot { [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $secureBoots = @() $sb = { New-Object PsObject -Property @{ SecureBoot = Confirm-SecureBootUEFI ComputerName = $env:COMPUTERNAME } } if ([string]::IsNullOrEmpty($PsSession)) { $secureBoots += Invoke-Command -ScriptBlock $sb } else { $secureBoots += Invoke-Command -Session $PsSession -ScriptBlock $sb } $InstanceResults = @() $InstanceResults += foreach ($SecureBootUEFI in $secureBoots) { $instanceId = "Machine: $($SecureBootUEFI.ComputerName), Class: SecureBoot" $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_Test_Secure_Boot' $instanceResult.Title = 'Test Secure Boot' $instanceResult.Severity = 'Critical' $instanceResult.Description = 'Checking Secure Boot' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = $instanceId $instanceResult.TargetResourceName = $instanceId $instanceResult.TargetResourceType = 'SecureBoot' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $instanceResult.Status = if ($SecureBootUEFI.SecureBoot) { 'Succeeded' } else { 'Failed' } $instanceResult.AdditionalData = New-Object -TypeName PSObject -Property @{ Source = $SecureBootUEFI.ComputerName Resource = $SecureBootUEFI.SecureBoot Detail = $lhwTxt.SecureBoot -f $SecureBootUEFI.SecureBoot, 'True' Status = if ($SecureBootUEFI) { 'Succeeded' } else { 'Failed' } TimeStamp = [datetime]::UtcNow } $instanceResult } return $InstanceResults } catch { throw $_ } } function Test-StoragePool { [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $StoragePoolsExist = @() $sb = { New-Object PsObject -Property @{ StoragePoolExists = [bool](Get-StoragePool -IsPrimordial:$false -ErrorAction SilentlyContinue) ComputerName = $ENV:ComputerName } } if ([string]::IsNullOrEmpty($PsSession)) { $StoragePoolsExist += Invoke-Command -ScriptBlock $sb } else { $StoragePoolsExist += Invoke-Command -Session $PsSession -ScriptBlock $sb } $InstanceResults = @() $InstanceResults += foreach ($StoragePool in $StoragePoolsExist) { $instanceId = "Machine: $($StoragePool.ComputerName), Class: StoragePool" $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_Test_No_StoragePools' $instanceResult.Title = 'Test Storage Pools do not exist' $instanceResult.Severity = 'Critical' $instanceResult.Description = 'Checking no storage pools exist' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = $instanceId $instanceResult.TargetResourceName = $instanceId $instanceResult.TargetResourceType = 'StoragePool' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $instanceResult.Status = if ($StoragePool.StoragePoolExists) { 'Failed' } else { 'Succeeded' } $instanceResult.AdditionalData = New-Object -TypeName PSObject -Property @{ Source = 'StoragePool' Resource = if ([bool]$StoragePool.StoragePoolExists) { "Present" } else { "Not present" } Detail = $lhwTxt.StoragePoolFail -f [bool]$StoragePool.StoragePoolExists, 'False' Status = if ($StoragePool.StoragePoolExists) { 'Failed' } else { 'Succeeded' } TimeStamp = [datetime]::UtcNow } $instanceResult } return $InstanceResults } catch { throw $_ } } function Test-FreeSpace { <# .SYNOPSIS Test free space #> [CmdletBinding()] param ( [Parameter()] [string] $Drive = $env:LocalRootFolderPath, [Parameter()] [int64] $Threshold = 30GB, [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $cimData = @() $InstanceResults = @() if ([string]::IsNullOrEmpty($Drive)) { $Drive = "C:\" } $Drive = (Split-Path -Path $Drive -Qualifier) $sb = { $DriveExists = Test-Path -Path $args[0] -IsValid if ($DriveExists) { $cimData = Get-CimInstance -ClassName Win32_LogicalDisk -Property DeviceId, FreeSpace | Where-Object DeviceID -EQ $args[0] } return New-Object PsObject -Property @{ DriveExists = $DriveExists ComputerName = $ENV:ComputerName DeviceID = $cimData.DeviceID FreeSpace = $cimData.FreeSpace } } if ([string]::IsNullOrEmpty($PsSession)) { $results += Invoke-Command -ScriptBlock $sb -ArgumentList $Drive } else { $results += Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList $Drive } $InstanceResults += foreach ($result in $results) { $computerName = $result.ComputerName if ($result.DriveExists -eq $false) { $status = 'Failed' $dtl = $lhwtxt.LocalRootFolderPathFail -f $computerName, $Drive Log-Info $dtl -Type Warning } else { $freeSpaceStr = [int]($result.FreeSpace / 1GB) $thresholdStr = [int]($threshold / 1GB) $dtl = $lhwtxt.LocalRootFolderPathFreeSpace -f $computerName, $Drive, $freeSpaceStr, $thresholdStr if ($result.FreeSpace -gt $Threshold) { $status = 'Succeeded' Log-Info $dtl } else { $status = 'Failed' Log-Info $dtl -Type Warning } } $instanceId = "Machine: $computerName, Class: Disk, DriveLetter: $drive" $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_Test_Disk_Space' $instanceResult.Title = 'Test Disk Space' $instanceResult.Severity = 'Warning' $instanceResult.Description = 'Checking Disk Space' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = $instanceId $instanceResult.TargetResourceName = $instanceId $instanceResult.TargetResourceType = 'Disk' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $instanceResult.AdditionalData = New-Object -TypeName PSObject -Property @{ Source = $computerName Resource = $cim.DeviceID Detail = $dtl Status = $status TimeStamp = [datetime]::UtcNow } $instanceResult.Status = $instanceResult.AdditionalData.Status $instanceResult } return $InstanceResults } catch { throw $_ } } function Test-Volume { <# .SYNOPSIS Test free space #> [CmdletBinding()] param ( [Parameter()] [string[]] $Drive = @('C'), # TO DO: Implement Free Space check [Parameter()] [int64] $Threshold = 30GB, [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $cimData = @() $CountResult = @() $InstanceCountByGroup = @() $sb = { $cimData = Get-Volume | Where-Object DriveLetter -in $args return $cimData } if ([string]::IsNullOrEmpty($PsSession)) { $cimData += Invoke-Command -ScriptBlock $sb -ArgumentList $Drive } else { $cimData += Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList $Drive } $groupProperty = @( 'DriveLetter' ) $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } #Log-Info -Message ($lhwTxt.VolumeCount -f $systemName, ($Drive -join ','), $sData.Count) # Make sure each system has the requisite number of Network Adapters $CountResult += Test-Count -CimData $sData -minimum $Drive.Count -ValidatorName 'Hardware' -Severity Critical } # Make sure each node has the same count by group $InstanceCountByGroup += Test-InstanceCountByGroup -CimData $cimData -ValidatorName 'Hardware' -GroupProperty $groupProperty -Severity Critical # Finally, the all properties from the $matchProperty array have to be compared for all instances across all nodes. return @($CountResult + $InstanceCountByGroup) } catch { throw $_ } } function CheckSingleNodeAllFlash { param ($cimData) # Split disks into type and check each server has the same count $allSSD = $cimData | Where-Object {$_.MediaType -match 'SSD|4' -and $_.BusType -match 'SAS|10|SATA|11'} $allNVMe = $cimData | Where-Object {$_.MediaType -match 'SSD|4' -and $_.BusType -match 'NVMe|17'} $allSCM = $cimData | Where-Object {$_.MediaType -match 'SCM|5'} $allHDD = $cimData | Where-Object {$_.MediaType -match 'HDD|3'} $className = $CimData.CimSystemProperties.ClassName -split '_' | Select-Object -Last 1 $instanceId = $CimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique Log-Info $lhwTxt.SingleNodeAllFlash -f $instanceId # check if they are all flash if ($allSSD -xor $allNVMe -and !$allSCM -and !$allHDD) { $Status = 'Succeeded' $type = 'Info' } else { $Status = 'Failed' $type = 'Warning' } $detail = $lhwTxt.SingleNodeAllFlashDetail -f $instanceId,("HDD: {0}, SSD:{1}, NVMe:{2}, SCM:{3}" -f [bool]$allHDD, [bool]$allSSD, [bool]$allNVMe, [bool]$allSCM) Log-Info $detail -Type $type $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_Test_SingleNode_AllFlash' $instanceResult.Title = 'Test Single Node All Flash' $instanceResult.Severity = 'Critical' $instanceResult.Description = "Checking single node is all flash" $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/windows-server/storage/storage-spaces/storage-spaces-direct-hardware-requirements#minimum-number-of-drives-excludes-boot-drive' $instanceResult.TargetResourceID = $instanceId $instanceResult.TargetResourceName = $instanceId $instanceResult.TargetResourceType = $className $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $AdditionalData = @() $AdditionalData += New-Object -TypeName PSObject -Property @{ Source = "$className Drive Type" Resource = "HDD: {0}, SSD:{1}, NVMe:{2}, SCM:{3}" -f [bool]$allHDD, [bool]$allSSD, [bool]$allNVMe, [bool]$allSCM Detail = $detail Status = $status TimeStamp = [datetime]::UtcNow } $instanceResult.AdditionalData = $AdditionalData $instanceResult.Status = if ($AdditionalData.Status -contains 'Failed') { 'Failed' } else { 'Succeeded' } $instanceResult } function Test-MinCoreCount { <# .SYNOPSIS Get minimum core count .DESCRIPTION Get core count from local machine to use as minimum core count Expecting data from PsSessions to include local machine indicating ECE. #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $cimParams = @{ ClassName = 'Win32_Processor' Property = 'NumberOfCores' } $cimData = @(Get-CimInstance @cimParams) return $cimData } $cimData = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $instanceResults = @() # Set min cores to local machine # This should only apply the scenario where there is a PsSession to all nodes, # and one of the nodes is also the local machine i.e. ECE invocation $RequiredTotalNumberOfCores = GetTotalNumberOfCores -cimData ($cimData | Where-Object { $_.CimSystemProperties.ServerName -like "$($ENV:COMPUTERNAME)*"}) if ($RequiredTotalNumberOfCores) { [array]$SystemNames = $cimData.CimSystemProperties.ServerName | Where-Object {$PSITEM -notlike "$($ENV:COMPUTERNAME)*"} | Sort-Object | Get-Unique foreach ($systemName in $SystemNames) { $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName } $TotalNumberOfCores = GetTotalNumberOfCores -cimData $sData if ($TotalNumberOfCores) { $detail = $lhwTxt.CheckMinCoreCount -f $SystemName, $TotalNumberOfCores, $RequiredTotalNumberOfCores if ($TotalNumberOfCores -ge $RequiredTotalNumberOfCores) { $status = 'Succeeded' Log-Info -message $detail } else { $status = 'Failed' Log-Info -message $detail -Type Warning } } else { $detail = $lhwTxt.UnexpectedCoreCount -f 'Unavailable','1' $status = 'Skipped' Log-Info -message $detail -Type Warning } $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_Test_Minimum_CPU_Cores' $instanceResult.Title = 'Test Minimum CPU Cores' $instanceResult.Severity = 'Warning' $instanceResult.Description = "Checking minimum CPU cores" $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceType = $cimParams.className $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $instanceResult.TargetResourceID = $systemName $instanceResult.TargetResourceName = $systemName $AdditionalData = @() $AdditionalData += New-Object -TypeName PSObject -Property @{ Source = $className Resource = 'Core Count' Detail = $detail Status = $status TimeStamp = [datetime]::UtcNow } $instanceResult.AdditionalData = $AdditionalData $instanceResult.Status = $status $instanceResults += $instanceResult } } else { Log-info $lhwTxt.SkippedCoreCount -type Warning } return $instanceResults } catch { throw $_ } } function Test-VirtualDisk { <# .SYNOPSIS Test Virtual Disk .DESCRIPTION During repair test virtual disk #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { New-Object -Type PsObject -Property @{ VirtualDiskExists = [bool](Get-StoragePool -IsPrimordial:$false -ErrorAction SilentlyContinue | Get-VirtualDisk) ComputerName = $ENV:COMPUTERNAME } } $VirtualDiskExists = @() if ([string]::IsNullOrEmpty($PsSession)) { $VirtualDiskExists += Invoke-Command -ScriptBlock $sb } else { $VirtualDiskExists += Invoke-Command -Session $PsSession -ScriptBlock $sb } $AdditionalData = @() foreach ($virtualDisk in $VirtualDiskExists) { if ($virtualDisk.VirtualDiskExists) { $status = 'Succeeded' $detail = $lhwTxt.VirtualDiskExists -f $virtualDisk.ComputerName Log-Info $detail } else { $status = 'Failed' $detail = $lhwTxt.VirtualDiskNotExists -f $virtualDisk.ComputerName Log-Info $detail -Type Warning } $AdditionalData += New-Object -TypeName PSObject -Property @{ Source = $virtualDisk.ComputerName Resource = if ($virtualDisk.VirtualDiskExists) { "Present" } else { "Not present" } Detail = $detail Status = $status TimeStamp = [datetime]::UtcNow } } $instanceId = "Machine: $($virtualDisk.ComputerName), Class: VirtualDisk" $instanceResult = New-Object AzStackHciHardwareTarget $instanceResult.Name = 'AzStackHci_Hardware_Test_VirtualDisk_Exists' $instanceResult.Title = 'Test Virtual Disk exists' $instanceResult.Severity = 'Critical' $instanceResult.Description = 'Checking virtual disk(s) exist for repair' $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites' $instanceResult.TargetResourceID = $instanceId $instanceResult.TargetResourceName = $instanceId $instanceResult.TargetResourceType = 'VirtualDisk' $instanceResult.Timestamp = [datetime]::UtcNow $instanceResult.HealthCheckSource = $ENV:EnvChkrId $instanceResult.Status = if ( $AdditionalData.Status -contains 'Failed' ) { 'Failed' } else { 'Succeeded' } $instanceResult.AdditionalData = $AdditionalData return $InstanceResults } catch { throw $_ } } function GetTotalNumberOfCores { <# .SYNOPSIS Multiply number of cores by number of processors .DESCRIPTION Multiply number of cores by number of processors #> param ($cimData) try { if ($cimData) { $numberOfCores = $cimData | Select-Object -ExpandProperty NumberOfCores | Sort-Object | Get-Unique if ($numberOfCores.count -ne 1) { throw ($lhwTxt.UnexpectedCoreCount -f $numberOfCores.count, '1') } else { return ($numberOfCores * $cimData.count) } } else { throw $lhwTxt.NoCoreReference } } catch { Log-Info ($lhwTxt.UnableCoreCount -f $_) -Type Warning } } Export-ModuleMember -Function Test-* # SIG # Begin signature block # MIInpAYJKoZIhvcNAQcCoIInlTCCJ5ECAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCK83ku/Z1V0pa1 # t+jOaXv5ZiG3x6t+qVHvMR6kJcPInKCCDYUwggYDMIID66ADAgECAhMzAAADTU6R # phoosHiPAAAAAANNMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI4WhcNMjQwMzE0MTg0MzI4WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDUKPcKGVa6cboGQU03ONbUKyl4WpH6Q2Xo9cP3RhXTOa6C6THltd2RfnjlUQG+ # Mwoy93iGmGKEMF/jyO2XdiwMP427j90C/PMY/d5vY31sx+udtbif7GCJ7jJ1vLzd # j28zV4r0FGG6yEv+tUNelTIsFmmSb0FUiJtU4r5sfCThvg8dI/F9Hh6xMZoVti+k # bVla+hlG8bf4s00VTw4uAZhjGTFCYFRytKJ3/mteg2qnwvHDOgV7QSdV5dWdd0+x # zcuG0qgd3oCCAjH8ZmjmowkHUe4dUmbcZfXsgWlOfc6DG7JS+DeJak1DvabamYqH # g1AUeZ0+skpkwrKwXTFwBRltAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUId2Img2Sp05U6XI04jli2KohL+8w # VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh # dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwMDUxNzAfBgNVHSMEGDAW # gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw # MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx # XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB # ACMET8WuzLrDwexuTUZe9v2xrW8WGUPRQVmyJ1b/BzKYBZ5aU4Qvh5LzZe9jOExD # YUlKb/Y73lqIIfUcEO/6W3b+7t1P9m9M1xPrZv5cfnSCguooPDq4rQe/iCdNDwHT # 6XYW6yetxTJMOo4tUDbSS0YiZr7Mab2wkjgNFa0jRFheS9daTS1oJ/z5bNlGinxq # 2v8azSP/GcH/t8eTrHQfcax3WbPELoGHIbryrSUaOCphsnCNUqUN5FbEMlat5MuY # 94rGMJnq1IEd6S8ngK6C8E9SWpGEO3NDa0NlAViorpGfI0NYIbdynyOB846aWAjN # fgThIcdzdWFvAl/6ktWXLETn8u/lYQyWGmul3yz+w06puIPD9p4KPiWBkCesKDHv # XLrT3BbLZ8dKqSOV8DtzLFAfc9qAsNiG8EoathluJBsbyFbpebadKlErFidAX8KE # usk8htHqiSkNxydamL/tKfx3V/vDAoQE59ysv4r3pE+zdyfMairvkFNNw7cPn1kH # Gcww9dFSY2QwAxhMzmoM0G+M+YvBnBu5wjfxNrMRilRbxM6Cj9hKFh0YTwba6M7z # ntHHpX3d+nabjFm/TnMRROOgIXJzYbzKKaO2g1kWeyG2QtvIR147zlrbQD4X10Ab # rRg9CpwW7xYxywezj+iNAc+QmFzR94dzJkEPUSCJPsTFMIIHejCCBWKgAwIBAgIK # YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm # aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw # OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD # VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la # UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc # 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D # dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+ # lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk # kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6 # A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd # X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL # 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd # sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3 # T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS # 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI # bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL # BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD # uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv # c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF # BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h # cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA # YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn # 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7 # v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b # pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/ # KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy # CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp # mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi # hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb # BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS # oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL # gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX # cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGXUwghlxAgEBMIGVMH4x # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p # Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAANNTpGmGiiweI8AAAAA # A00wDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw # HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIK9W # BAiyKBnOSIL0xbR9xnxaH2c8IvW78WP4c7Zvdk0BMEIGCisGAQQBgjcCAQwxNDAy # oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20wDQYJKoZIhvcNAQEBBQAEggEAEUf0jdcFbb/DHSXMXEeZg+hncaO8N+7nmTJ/ # BGFx1YlY2ugvjr6zYEcKTqXz6P2N2R/WqtaVbV7G9648w3KYtVe+mJRpiOYl5khw # NiHThRBfYGBBZkuL33/RoB1nrF4zR3KefEbZNzByb4aNJgCTw7ihhMMQ4qDHr5Pv # wFNiDKjl6bJKV5VPBXocpWo4pJZSc2RfaqWzokmJ0IxN+Ub93cPv7Ni9KQK3hLha # nrEMpUJfOL17jelcNdhVcVLZy/1ydVhI5YoVJvaikAGuU9pwq3twrZFVVjSsY6x4 # /IjWcqB+5v3EdISUOYOqA4H+NaJ7FNdE6eJdAQp6MkDWJQpYAqGCFv8wghb7Bgor # BgEEAYI3AwMBMYIW6zCCFucGCSqGSIb3DQEHAqCCFtgwghbUAgEDMQ8wDQYJYIZI # AWUDBAIBBQAwggFRBgsqhkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGE # WQoDATAxMA0GCWCGSAFlAwQCAQUABCAm4olQJ2vgUTaPE3Zed/4EWaASuHO0aaOy # SMtXKHgIhQIGZK/4ewXPGBMyMDIzMDcxMzE5MjYyNS4wOTJaMASAAgH0oIHQpIHN # MIHKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL # ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMg # VFNTIEVTTjpENkJELUUzRTctMTY4NTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt # U3RhbXAgU2VydmljZaCCEVYwggcMMIIE9KADAgECAhMzAAABx/sAoEpb8ifcAAEA # AAHHMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo # aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y # cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw # MB4XDTIyMTEwNDE5MDEzNVoXDTI0MDIwMjE5MDEzNVowgcoxCzAJBgNVBAYTAlVT # MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVy # aWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkQ2QkQtRTNF # Ny0xNjg1MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIC # IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr0LcVtnatNFMBrQTtG9P8ISA # PyyGmxNfhEzaOVlt088pBUFAIasmN/eOijE6Ucaf3c2bVnN/02ih0smSqYkm5P3Z # wU7ZW202b6cPDJjXcrjJj0qfnuccBtE3WU0vZ8CiQD7qrKxeF8YBNcS+PVtvsqhd # 5YW6AwhWqhjw1mYuLetF5b6aPif/3RzlyqG3SV7QPiSJends7gG435Rsy1HJ4Xnq # ztOJR41I0j3EQ05JMF5QNRi7kT6vXTT+MHVj27FVQ7bef/U+2EAbFj2X2AOWbvgl # YaYnM3m/I/OWDHUgGw8KIdsDh3W1eusnF2D7oenGgtahs+S1G5Uolf5ESg/9Z+38 # rhQwLgokY5k6p8k5arYWtszdJK6JiIRl843H74k7+QqlT2LbAQPq8ivQv0gdclW2 # aJun1KrW+v52R3vAHCOtbUmxvD1eNGHqGqLagtlq9UFXKXuXnqXJqruCYmfwdFMD # 0UP6ii1lFdeKL87PdjdAwyCiVcCEoLnvDzyvjNjxtkTdz6R4yF1N/X4PSQH4Flgs # lyBIXggaSlPtvPuxAtuac/ITj4k0IRShGiYLBM2Dw6oesLOoxe07OUPO+qXXOcJM # VHhE0MlhhnxfN2B1JWFPWwQ6ooWiqAOQDqzcDx+79shxA1Cx0K70eOBplMog27gY # oLpBv7nRz4tHqoTyvA0CAwEAAaOCATYwggEyMB0GA1UdDgQWBBQFUNLdHD7BAF/V # U/X/eEHLiUSSIDAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNV # HR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2Ny # bC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYI # KwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAy # MDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0G # CSqGSIb3DQEBCwUAA4ICAQDQy5c8ogP0y8xAsLVca07wWy1mT+nqYgAFnz2972kN # O+KJ7AE4f+SVbvOnkeeuOPq3xc+6TS8g3FuKKYEwYqvnRHxX58tjlscZsZeKnu7f # GNUlpNT9bOQFHWALURuoXp8TLHhxj3PEq9jzFYBP2YNMLol70ojY1qpze3nMMJfp # durdBBpaOLlJmRNTLhxd+RJGJQbY1XAcx6p/FigwqBasSDUxp+0yFPEBB9uBE3KI # LAtq6fczGp4EMeon6YmkyCGAtXMKDFQQgdP/ITe7VghAVbPTVlP3hY1dFgc+t8YK # 2obFSFVKslkASATDHulCMht+WrIsukclEUP9DaMmpq7S0RLODMicI6PtqqGOhdna # RltA0d+Wf+0tPt9SUVtrPJyO7WMPKbykCRXzmHK06zr0kn1YiUYNXCsOgaHF5ImO # 2ZwQ54UE1I55jjUdldyjy/UPJgxRm9NyXeO7adYr8K8f6Q2nPF0vWqFG7ewwaAl5 # ClKerzshfhB8zujVR0d1Ra7Z01lnXYhWuPqVZayFl7JHr6i6huhpU6BQ6/VgY0cB # iksX4mNM+ISY81T1RYt7fWATNu/zkjINczipzbfg5S+3fCAo8gVB6+6A5L0vBg39 # dsFITv6MWJuQ8ZZy7fwlFBZE4d5IFbRudakNwKGdyLGM2otaNq7wm3ku7x41UGAm # kDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQEL # BQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH # EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNV # BAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4X # DTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh # bXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM # 57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm # 95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzB # RMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBb # fowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCO # Mcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYw # XE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW # /aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/w # EPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPK # Z6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2 # BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfH # CBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYB # BAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8v # BO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYM # KwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEF # BQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBW # BgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUH # AQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp # L2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsF # AAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518Jx # Nj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+ # iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2 # pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefw # C2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7 # T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFO # Ry3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhL # mm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3L # wUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5 # m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE # 0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggLNMIICNgIB # ATCB+KGB0KSBzTCByjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEmMCQGA1UE # CxMdVGhhbGVzIFRTUyBFU046RDZCRC1FM0U3LTE2ODUxJTAjBgNVBAMTHE1pY3Jv # c29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAOIASP0JSbv5 # R23wxciQivHyckYooIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTAwDQYJKoZIhvcNAQEFBQACBQDoWnbzMCIYDzIwMjMwNzEzMjExMzIzWhgPMjAy # MzA3MTQyMTEzMjNaMHYwPAYKKwYBBAGEWQoEATEuMCwwCgIFAOhadvMCAQAwCQIB # AAIBEQIB/zAHAgEAAgIRyDAKAgUA6FvIcwIBADA2BgorBgEEAYRZCgQCMSgwJjAM # BgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEB # BQUAA4GBAB1FSRPydnvEFFfS60yPFo/F0snKBYdo/suq0DveDx1PND4wIaakYkeE # 5Z92fDfIwBpb0ozlzxDuc1Xb15Zy05G/mE1iAYGL83U9jDVCUiGpB8vaU7mzeX+X # +TNZd4Av+Lj9BJ+SpvihXCYrRqgG7DLLm9HF7Bvij0gGW1UhyW+RMYIEDTCCBAkC # AQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV # BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG # A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAHH+wCgSlvy # J9wAAQAAAccwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG # 9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgj3gWRSDH5+kafZst22vr26qLOgIDmXZW # TDko4MEQ8nIwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCBH5+Xb4lKyRQs5 # 5Vgtt4yCTsd0htESYCyPC1zLowmSyTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFBDQSAyMDEwAhMzAAABx/sAoEpb8ifcAAEAAAHHMCIEIHgkbe5TwCZ1EPqy # IWn3w1iAUlqH9tQPaST/ZdO6j7HQMA0GCSqGSIb3DQEBCwUABIICAHNaDq7niKfC # FH1xP07MuMiyNhMSZQi3R+XOH52KttaAap8PdzLhg2gWS+YNrqY9riLYFH45GH37 # pJwHKuY3Rwpi/4S40XnvYr57Gl4MEplD0taoPFkhXi2TPn1aBP/Z8q05elxXL1wB # zu925ffUb4i1ftmk4s/jET2IhmhfwiOcc4/aExBMaYJlYxGLAWRhAm3wCf7TBPYe # /tuTYpoYI6Q0Lul6gI4+2qP0/V4q8n/fTG1Wa7Ueamnmp6jBcewHFBL7ZvHTdCHx # a7d+V7hwmM+EH3SDoWfVFOytvjERiHRtGu46c+Spxum5spUJ6v13sQgC0rN4CxPd # 0l13mSL5Nf/js3el7nCuprDEl3Dji+2h4hKB9Cw/psexIE7ZzBooA1QBe6diE/cw # PNk3aAIWz0pDbahvoeJ4og6gFKgltfXGva8YVTIpFQ/Apx+LMAqgEfraY3WZBJYp # 3kTJnNO7mj2n+11ooaLiQ1DBa+/f5Fkpw++I3wcPnMbUTinFT2wMRkaUrEVLCpG0 # 9FJ68Gkso9dMF0/Z56HLCMN0PByXvGLMrBvDmd1aqY812aXVO2Oqtw+0Y78lpq5a # CCIi8bxlnh0S/19uYaUvYWsoQHznjVeoSPyMB+pT5nMAkYd33Upz3R0hCrtLW1ui # HmJMOTKOxrBXeX9SRRaS2OPkxr3vD/mR # SIG # End signature block |