AzStackHciHardware/AzStackHci.Hardware.Helpers.psm1
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 (New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName cimData = $cimData }) } $remoteOutput = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $cimTest = Test-CimData -Data $remoteOutput -ClassName Processor -Severity WARNING $cimData = $remoteOutput.cimData $PropertyResult = @() $PropertySyncResult = @() $matchProperty = @( 'Caption' 'Family' 'Manufacturer' 'MaxClockSpeed' 'NumberOfCores' 'NumberOfEnabledCore' 'NumberOfLogicalProcessors' 'ThreadCount' ) $warningDesiredPropertyValue = @{ 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' } } $criticalDesiredPropertyValue = @{ 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 $hypervisorDtl = ($lhwTxt.HypervisorPresent -f (($CheckHyperVisor | ForEach-Object {"{0}:{1}" -f $_.Name, $_.HypervisorPresent }) -join ',')) if (($CheckHyperVisor | Select-Object -ExpandProperty HypervisorPresent) -notcontains $false) { Log-Info $hypervisorDtl Remove-Variable -Name criticalDesiredPropertyValue } else { Log-Info $hypervisorDtl -Type CRITICAL } Log-CimData -cimData $cimData -Properties $matchProperty,$warningDesiredPropertyValue,$criticalDesiredPropertyValue $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 $warningDesiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Warning if ($criticalDesiredPropertyValue) { $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $criticalDesiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity CRITICAL } $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 + $cimTest) } 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 } Log-CimData -cimData $cimData -Properties HypervisorPresent 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 (New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName cimData = $cimData }) } $remoteOutput = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $cimTest = Test-CimData -Data $remoteOutput -ClassName NetAdapter -Severity CRITICAL $cimData = $remoteOutput.cimData $PropertyResult = @() $PropertySyncResult = @() $GroupResult = @() $CountResult = @() # Blocking properties $criticalMatchProperty = @( 'DriverDate' 'DriverDescription' 'DriverMajorNdisVersion' 'DriverMinorNdisVersion' 'DriverProvider' 'DriverVersionString' 'MajorDriverVersion' 'MinorDriverVersion' ) # non-block warning properties $warningMatchProperty = @( 'ActiveMaximumTransmissionUnit' 'ReceiveLinkSpeed' 'Speed' 'TransmitLinkSpeed' 'VlanID' 'MtuSize' ) $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' ) Log-CimData -cimData $cimData -Properties $desiredPropertyValue,$warningMatchProperty,$criticalMatchProperty $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 + $cimTest) } catch { throw $_ } } function Test-MemoryCapacity { <# .SYNOPSIS Test Memory .DESCRIPTION Test Memory #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { if ((Get-WmiObject -Class Win32_ComputerSystem).Model -eq "Virtual Machine") { $environmentType = "Virtual" $minimumMemory = 24GB } else { $environmentType = "Physical" $minimumMemory = 32GB } Log-Info -Message ($lhwTxt.MemoryCapacityRequirement -f $minimumMemory, $environmentType) $instanceResults = @() $sb = { $cimParams = @{ ClassName = 'Win32_PhysicalMemory' Property = '*' } $cimData = @(Get-CimInstance @cimParams) return (New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName cimData = $cimData }) } $remoteOutput = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $cimTest = Test-CimData -Data $remoteOutput -ClassName PhysicalMemory -Severity WARNING $cimData = $remoteOutput.cimData Log-CimData -cimData $cimData -Properties Capacity # 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 = 'FAILURE' Log-Info $dtl -Type Warning } else { $Status = 'SUCCESS' Log-Info $dtl } $params = @{ Name = 'AzStackHci_Hardware_Test_MemoryCapacity' Title = 'Test Memory Capacity' DisplayName = "Test Memory Capacity $systemName" Severity = 'WARNING' Description = 'Checking Memory Capacity' Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'Memory' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = 'Memory Capacity' Resource = $totalMemory.Sum Detail = $dtl Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } return ($instanceResults + $cimTest) } 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 (New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName cimData = $cimData }) } $remoteOutput = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $cimTest = Test-CimData -Data $remoteOutput -ClassName PhysicalMemory -Severity WARNING $cimData = $remoteOutput.cimData # Add EEC property $eccSb = { if ($this.TotalWidth -gt $this.DataWidth) { return $true } else { return $false } } $cimData | Add-Member -MemberType ScriptProperty -Name ECC -Value $eccSb -Force $PropertyResult = @() $PropertySyncResult = @() $matchProperty = @( 'ConfiguredClockSpeed' 'ConfiguredVoltage' 'MaxVoltage' 'MemoryType' 'SMBIOSMemoryType' 'Speed' 'TotalWidth' 'TypeDetail' ) $warningdesiredPropertyValue = @{ DataWidth = @{ value = 64; hint = '64-bit' } # x64 FormFactor = @{ value = 8; hint = 'DIMM' } # DIMM } $criticaldesiredPropertyValue = @{ ECC = $true # Error Correction Code (ECC) memory } Log-CimData -cimData $cimData -Properties $warningdesiredPropertyValue,$criticaldesiredPropertyValue,$matchProperty # 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 $warningdesiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Warning $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $criticaldesiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity CRITICAL $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 + $cimTest) } 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 (New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName cimData = $cimData }) } $remoteOutput = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $cimTest = Test-CimData -Data $remoteOutput -ClassName VideoController -Severity WARNING $cimData = $remoteOutput.cimData $PropertyResult = @() $GroupResult = @() $InstanceCount = @() $InstanceCountByGroup = @() $matchProperty = @( 'AdapterRam' 'Name' 'DriverDate' 'DriverVersion' 'VideoMemoryType' 'VideoProcessor' ) $desiredPropertyValue = @{ ConfigManagerErrorCode = @{ value = 0; hint = 'The device is working properly' } # The device is working properly Status = 'OK' } $groupProperty = @( 'Name' ) Log-CimData -cimData $cimData -Properties $desiredPropertyValue,$matchProperty # 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 } # Check property sync for all nodes as well $GroupResult += Test-GroupProperty -CimData $cimData -GroupProperty $groupProperty -MatchProperty $MatchProperty -ValidatorName Hardware -Severity Warning $InstanceCount += Test-InstanceCount -CimData $cimData -Severity Warning -ValidatorName 'Hardware' $InstanceCountByGroup += Test-InstanceCountByGroup -CimData $cimData -ValidatorName 'Hardware' -GroupProperty $groupProperty -Severity Warning return @($GroupResult + $InstanceCount + $InstanceCountByGroup + $PropertyResult + $cimTest) } 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 (New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName cimData = $cimData }) } $remoteOutput = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $cimTest = Test-CimData -Data $remoteOutput -ClassName Bios -Severity WARNING $cimData = $remoteOutput.cimData $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' } Log-CimData -cimData $cimData -Properties $desiredPropertyValue,$matchProperty # 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 + $cimTest) } 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 (New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName cimData = $cimData }) } $remoteOutput = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } $cimTest = Test-CimData -Data $remoteOutput -ClassName ComputerSystem -Severity CRITICAL $cimData = $remoteOutput.cimData $PropertySyncResult = @() $matchProperty = @( 'Manufacturer' 'Model' ) Log-CimData -cimData $cimData -Properties $matchProperty # 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 + $cimTest) } catch { throw $_ } } function Test-PhysicalDisk { <# .SYNOPSIS Test Physical Disk .DESCRIPTION Test Physical Disk #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession, [string] $HardwareClass = 'Medium' ) try { $sb = { $fabricOp = $args[0] $eceNodeName = $args[1] $allowedBusTypes = @('SATA', 'SAS', 'NVMe', 'SCM') $allowedMediaTypes = @('HDD', 'SSD', 'SCM') $bootPhysicalDisk = Get-Disk | Where-Object {$_.IsBoot -or $_.IsSystem} | Get-PhysicalDisk if ($fabricOp -match 'AddNode|Repair') { if ($eceNodeName -eq $env:COMPUTERNAME) { # In AddNode we need a seperate check command to return disks that are spaces disks # to build a reference list for the new node $cimData = @(Get-StorageNode -Name $env:COMPUTERNAME* | ` Get-PhysicalDisk -PhysicallyConnected | ` Where-Object { ` $_.CanPool -eq $false -and ` $_.CannotPoolReason -eq 'In a Pool' } ) } else { if($fabricOp -like "*AddNode*") { # For AddNode node we expect CanPool true and to match ECE node above $cimData = @(Get-StorageNode -Name $env:COMPUTERNAME* | ` Get-PhysicalDisk -PhysicallyConnected | ` Where-Object { ` $_.BusType -in $allowedBusTypes -and ` $_.MediaType -in $allowedMediaTypes -and ` $_.DeviceId -notin $bootPhysicalDisk.DeviceId -and ` $_.CanPool -eq $true } ) } elseif ($fabricOp -like "*Repair*") { # For Repair we ignore CanPool and to match ECE node above $cimData = @(Get-StorageNode -Name $env:COMPUTERNAME* | ` Get-PhysicalDisk -PhysicallyConnected | ` Where-Object { ` $_.BusType -in $allowedBusTypes -and ` $_.MediaType -in $allowedMediaTypes -and ` $_.DeviceId -notin $bootPhysicalDisk.DeviceId } ) } else { throw "Invalid Fabric Operation: $fabricOp" } } } else { if ($fabricOp -like '*KeepStorage*') { $cimData = @(Get-StorageNode -Name $env:COMPUTERNAME* | ` Get-PhysicalDisk -PhysicallyConnected | ` Where-Object { ` $_.BusType -in $allowedBusTypes -and ` $_.MediaType -in $allowedMediaTypes -and ` $_.DeviceId -notin $bootPhysicalDisk.DeviceId } ) } else { $cimData = @(Get-StorageNode -Name $env:COMPUTERNAME* | ` Get-PhysicalDisk -PhysicallyConnected | ` Where-Object { ` $_.BusType -in $allowedBusTypes -and ` $_.MediaType -in $allowedMediaTypes -and ` $_.DeviceId -notin $bootPhysicalDisk.DeviceId -and ` $_.CanPool -eq $true } ) } } return (New-Object PsObject -Property @{ ComputerName = $ENV:ComputerName cimData = $cimData }) } $remoteOutput = if ($PsSession) { # When we are using PsSessions (every ECE fabric operation) # Inject our FabricOperation and local computer into the remote session, # so canPool expectation can be set for deployment and ScaleOut. Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList $ENV:EnvChkrId, $ENV:ComputerName } else { Invoke-Command -ScriptBlock $sb } $cimTest = Test-CimData -Data $remoteOutput -ClassName PhysicalDisk -Severity CRITICAL $cimData = $remoteOutput.cimData $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' } } Log-CimData -cimData $cimData -Properties $groupProperty,$warningDesiredPropertyValue,$matchProperty, CanPool, CannotPoolReason, Size, PhysicalLocation, UniqueId, SerialNumber $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) $systemCountResult = @() $countCommonParams = @{ ValidatorName = 'Hardware' Severity = 'CRITICAL' } # split into medium vs small # medium is subject storage spaces direct requirements # small is subject to the minimum number of disks being 1. # consistency across bustype, mediatype, and size and other property is still checked in the property sync for both form factors. if ($HardwareClass -eq 'Medium') { # As per https://docs.microsoft.com/en-us/windows-server/storage/storage-spaces/storage-spaces-direct-hardware-requirements # 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 } } elseif ($HardwareClass -eq 'Small') { # here we effectively forego the storage spaces direct requirements and just check for the minimum number of 2 disks $systemCountResult += Test-Count -cimData $sData -Minimum 1 @countCommonParams # and all nodes should be all flash [array]$CheckDisksAreAllFlash = CheckDisksAreAllFlash -CimData $sData } else { throw "Invalid HardwareClass: $HardwareClass" } $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'} $instParams = @{ ValidatorName = 'Hardware' Severity = 'CRITICAL' } if ($allSSD) { Log-Info ($lhwTxt.DiskInstanceCountByType -f 'SSD') $InstanceCount += Test-InstanceCount -CimData $allSSD @instParams -NamePostfix "SSD" } if ($allNVMe) { Log-Info ($lhwTxt.DiskInstanceCountByType -f 'NVMe') $InstanceCount += Test-InstanceCount -CimData $allNVMe @instParams -NamePostfix "NVMe" } if ($allSCM) { Log-Info ($lhwTxt.DiskInstanceCountByType -f 'SCM') $InstanceCount += Test-InstanceCount -CimData $allSCM @instParams -NamePostfix "SCM" } if ($allHDD) { Log-Info ($lhwTxt.DiskInstanceCountByType -f 'HDD') $InstanceCount += Test-InstanceCount -CimData $allHDD @instParams -NamePostfix "HDD" } if (($null -eq $PsSession -or $PsSession.Count -eq 1) -and $null -eq $CheckDisksAreAllFlash) { # Single Node deployments should be all flash [array]$CheckDisksAreAllFlash = CheckDisksAreAllFlash -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 + $CheckDisksAreAllFlash + $cimTest) } catch { throw $_ } } function Test-TpmVersion { [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession, [Parameter()] $version = '2.0' ) $tpms = @() $InstanceResults = @() $sb = { $tpm = Get-CimInstance -Namespace root/cimv2/Security/MicrosoftTpm -ClassName Win32_Tpm -ErrorAction SilentlyContinue $result = New-Object -TypeName PSObject -Property @{ ComputerName = $ENV:COMPUTERNAME TpmData = $tpm } return $result } $tpms += if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb } else { Invoke-Command -ScriptBlock $sb } Log-CimData -CimData $tpms.TpmData foreach ($tpm in $tpms) { $computerName = $tpm.ComputerName # Test properties $InstanceResults += foreach ($instance in $tpm.TpmData) { $instanceId = "Machine: $computerName, Class: Tpm, Manufacturer ID: $($instance.ManufacturerId)" $instanceVersion = $instance.SpecVersion -split ',' | Select-Object -First 1 $status = if ($instanceVersion -eq $version) { 'SUCCESS' } else { 'FAILURE' } $params = @{ Name = 'AzStackHci_Hardware_Test_Tpm_Version' Title = 'Test TPM Version' DisplayName = "Test TPM Version $computerName" Severity = 'CRITICAL' Description = "Checking TPM for desired version ($version)" Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'Tpm' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = 'Version' Resource = $instanceVersion Detail = "$instanceId Tpm version is $instanceVersion. Expected $version" Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } $instanceResult = New-AzStackHciResultObject @params if ($InstanceResult.AdditionalData.Status -eq 'SUCCESS') { Log-Info -Message $InstanceResult.AdditionalData.Detail } else { Log-Info -Message $InstanceResult.AdditionalData.Detail -Type Warning } $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-CimData -cimData $tpm -Properties $desiredPropertyValue 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)" 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 } $status = if ($passed) { 'SUCCESS' } else { 'FAILURE' } $params = @{ Name = 'AzStackHci_Hardware_Test_Tpm_Instance_Properties' Title = "Test TPM Property $propertyName is $($desiredPropertyValue.$propertyName)" DisplayName = "Test TPM Property $propertyName is $($desiredPropertyValue.$propertyName) $computerName" Severity = 'CRITICAL' Description = "Checking TPM for desired properties" Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'Tpm' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = $propertyName Resource = $instance.$propertyName Detail = $detail Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } } } 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 } Log-CimData -cimData $tpmKeys $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)" 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 $status = if ($validCert) { 'SUCCESS' } else { 'FAILURE' } $params = @{ Name = 'AzStackHci_Hardware_Test_Tpm_Certificate_Properties' Title = "Test TPM Certificate Properties" DisplayName = "Test TPM $computerName" Severity = 'CRITICAL' Description = "Checking TPM for desired properties" Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'TpmEndorsementKeyInfo' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = $cert.Thumbprint Resource = "Current: $currentCert. Valid Algorithm: $validAlgo. Valid Key Usage: $validUsage." Detail = ($certDetail -join "`r") Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } } return $InstanceResults } catch { throw $_ } } function Test-SecureBoot { [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $secureBoots = @() $sb = { if ((Get-Command Confirm-SecureBootUEFI -ErrorAction SilentlyContinue) -ne $null) { <# For devices that Standard hardware security is not supported, this means that the device does not meet at least one of the requirements of standard hardware security. This causes the Confirm-SecureBootUEFI command to fail with the error: Cmdlet not supported on this platform: 0xC0000002 #> try { $secureBoot = Confirm-SecureBootUEFI } catch { $secureBoot = $false } } else { $secureBoot = $false } New-Object PsObject -Property @{ SecureBoot = $secureBoot ComputerName = $env:COMPUTERNAME } } if ([string]::IsNullOrEmpty($PsSession)) { $secureBoots += Invoke-Command -ScriptBlock $sb } else { $secureBoots += Invoke-Command -Session $PsSession -ScriptBlock $sb } Log-CimData -cimData $secureboots $InstanceResults = @() $InstanceResults += foreach ($SecureBootUEFI in $secureBoots) { $dtl = $lhwTxt.SecureBoot -f $SecureBootUEFI.SecureBoot, 'True' if ($SecureBootUEFI.SecureBoot) { $status = 'SUCCESS' } else { $status = 'FAILURE' $dtl = "{0}. {1}" -f $dtl, $lhwTxt.SecureBootNotSupported } $params = @{ Name = 'AzStackHci_Hardware_Test_Secure_Boot' Title = "Test Secure Boot" DisplayName = "Test Secure Boot $($SecureBootUEFI.computerName)" Severity = 'CRITICAL' Description = "Checking Secure Boot" Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'SecureBoot' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = $SecureBootUEFI.ComputerName Resource = $SecureBootUEFI.SecureBoot Detail = $dtl Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } 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 } Log-CimData -cimData $StoragePoolsExist $InstanceResults = @() $InstanceResults += foreach ($StoragePool in $StoragePoolsExist) { $status = if (-not $StoragePool.StoragePoolExists) { 'SUCCESS' } else { 'FAILURE' } $params = @{ Name = 'AzStackHci_Hardware_Test_No_StoragePools' Title = "Test Storage Pools do not exist for new deployment" DisplayName = "Test Storage Pools do not exist for new deployment $($StoragePool.computerName)" Severity = 'CRITICAL' Description = "Checking no storage pools exist for new deployment" Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'StoragePool' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = 'StoragePool' Resource = if ([bool]$StoragePool.StoragePoolExists) { "Present" } else { "Not present" } Detail = $lhwTxt.StoragePoolFail -f [bool]$StoragePool.StoragePoolExists, 'False' Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } 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 } Log-CimData -cimData $results $InstanceResults += foreach ($result in $results) { $computerName = $result.ComputerName if ($result.DriveExists -eq $false) { $status = 'FAILURE' $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 = 'SUCCESS' Log-Info $dtl } else { $status = 'FAILURE' Log-Info $dtl -Type Warning } } $instanceId = "Machine: $computerName, Class: Disk, DriveLetter: $drive" $params = @{ Name = 'AzStackHci_Hardware_Test_Disk_Space' Title = "Test Disk Space" DisplayName = "Test Disk Space $computerName" Severity = 'WARNING' Description = "Checking Disk Space" Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'Disk' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = $computerName Resource = $cim.DeviceID Detail = $dtl Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } 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' ) Log-CimData -cimData $cimData -Properties $groupProperty $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 CheckDisksAreAllFlash { 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.DisksAreAllFlash -f $instanceId) # check if they are all flash if ($allSSD -xor $allNVMe -and !$allSCM -and !$allHDD) { $Status = 'SUCCESS' $type = 'Info' } else { $Status = 'FAILURE' $type = 'CRITICAL' } $detail = $lhwTxt.DisksAreAllFlashDetail -f $instanceId,("HDD: {0}, SSD:{1}, NVMe:{2}, SCM:{3}" -f [bool]$allHDD, [bool]$allSSD, [bool]$allNVMe, [bool]$allSCM) Log-Info $detail -Type $type $params = @{ Name = 'AzStackHci_Hardware_Test_PhysicalDisk_AllFlash' Title = "Test PhysicalDisks are All Flash" DisplayName = "Test PhysicalDisks are All Flash" Severity = 'CRITICAL' Description = "Checking PhysicalDisks are all flash" Tags = @{} Remediation = 'https://learn.microsoft.com/en-us/windows-server/storage/storage-spaces/storage-spaces-direct-hardware-requirements#minimum-number-of-drives-excludes-boot-drive' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = $className Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ 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 } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } 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 = @() Log-CimData -cimData $cimData -Properties NumberOfCores if ((Get-WmiObject -Class Win32_ComputerSystem).Model -eq "Virtual Machine") { $environmentType = "Virtual" $RequiredTotalNumberOfCores = 4 } else { $environmentType = "Physical" # 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)*"}) } Log-Info -Message ($lhwTxt.CoreCountRequirement -f $RequiredTotalNumberOfCores, $environmentType) if ($RequiredTotalNumberOfCores) { [array]$SystemNames = $cimData.CimSystemProperties.ServerName | Where-Object {$PSITEM -notlike "$($ENV:COMPUTERNAME)*"} | Sort-Object | Get-Unique $instanceResults += 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 = 'SUCCESS' Log-Info -message $detail } else { $status = 'FAILURE' Log-Info -message $detail -Type Warning } } else { $detail = $lhwTxt.UnexpectedCoreCount -f 'Unavailable','1' $status = 'FAILURE' Log-Info -message $detail -Type Warning } $params = @{ Name = 'AzStackHci_Hardware_Test_Minimum_CPU_Cores' Title = 'Test Minimum CPU Cores' DisplayName = "Test Minimum CPU Cores $systemName" Severity = 'WARNING' Description = 'Checking minimum CPU cores' Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $systemName TargetResourceName = $systemName TargetResourceType = $cimParams.className Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = $cimParams.ClassName Resource = 'Core Count' Detail = $detail Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } } 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 } Log-CimData -cimData $VirtualDiskExists $instanceResults = @() $instanceResults += foreach ($virtualDisk in $VirtualDiskExists) { if ($virtualDisk.VirtualDiskExists) { $status = 'SUCCESS' $detail = $lhwTxt.VirtualDiskExists -f $virtualDisk.ComputerName Log-Info $detail } else { $status = 'FAILURE' $detail = $lhwTxt.VirtualDiskNotExists -f $virtualDisk.ComputerName Log-Info $detail -Type Warning } $instanceId = "Machine: $($virtualDisk.ComputerName), Class: VirtualDisk" $params = @{ Name = 'AzStackHci_Hardware_Test_VirtualDisk_Exists' Title = 'Test Virtual Disk exists' DisplayName = "Test Virtual Disk exists $($virtualDisk.ComputerName)" Severity = 'CRITICAL' Description = 'Checking virtual disk(s) exist for repair' Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'VirtualDisk' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = $virtualDisk.ComputerName Resource = if ($virtualDisk.VirtualDiskExists) { "Present" } else { "Not present" } Detail = $detail Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } } catch { throw $_ } } function Test-VirtualizationBasedSecurity { <# .SYNOPSIS Test Virtualization-based Security (VBS) .DESCRIPTION Test if hardware supports VBS, which is required on HCI #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $vbsMethodDefinition = @' public enum Values { SecureKernelRunning, HvciEnabled, HvciStrictMode, DebugEnabled, FirmwarePageProtection, EncryptionKyAvailable, SpareFlags, TrustletRunning, HvciDisableAllowed, SpareFlags2, Sparce1, Sparce2, Sparce3, Sparce4, Sparce5, Sparce6 } public enum SYSTEM_INFORMATION_CLASS_EX : uint { SystemBootEnvironmentInformation = 90, SystemIsolatedUserModeInformation = 165, SystemDmaGuardPolicyInformation = 202 } public struct SYSTEM_ISOLATED_USER_MODE_INFORMATION { public Values Bits; public ulong Spare7; } public static bool GetVBSCapable() { bool capable = false; bool enabled = false; GetVBSInfo(ref capable, ref enabled); return capable; } [DllImport("ntdll", CharSet = CharSet.Auto, SetLastError = true)] public static extern uint NtQuerySystemInformation( [In] SYSTEM_INFORMATION_CLASS_EX SystemInformationClass, [In][Out] IntPtr SystemInformation, [In] uint SystemInformationLength, [Out] uint ReturnLength); public static void GetVBSInfo(ref bool capable, ref bool enabled) { var outSize = (uint)0; var outBuffer = IntPtr.Zero; try { outSize = (uint)Marshal.SizeOf(typeof(SYSTEM_ISOLATED_USER_MODE_INFORMATION)); outBuffer = Marshal.AllocHGlobal((int)outSize); for (long offset = 0; offset < outSize; offset++) { Marshal.WriteByte(outBuffer, (int)offset, 0); } uint retValue = NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS_EX.SystemIsolatedUserModeInformation, outBuffer, outSize, 0); if (retValue != 0) { throw new Exception(Marshal.GetLastWin32Error().ToString()); } SYSTEM_ISOLATED_USER_MODE_INFORMATION iumInfo = new SYSTEM_ISOLATED_USER_MODE_INFORMATION(); iumInfo = (SYSTEM_ISOLATED_USER_MODE_INFORMATION)Marshal.PtrToStructure(outBuffer, typeof(SYSTEM_ISOLATED_USER_MODE_INFORMATION)); capable = ((int)iumInfo.Bits | 0x01) != 0; enabled = ((int)iumInfo.Bits | 0x02) != 0; } finally { Marshal.FreeHGlobal(outBuffer); } } '@ $null = Add-Type -MemberDefinition $vbsMethodDefinition -Name "VirtualizationBasedSecurity" -Namespace "Microsoft.PowerShell.AzStackHci.EnvironmentChecker.Hardware" -PassThru $vbsCapable = [Microsoft.PowerShell.AzStackHci.EnvironmentChecker.Hardware.VirtualizationBasedSecurity]::GetVBSCapable() New-Object -Type PsObject -Property @{ VbsCapable = $vbsCapable ComputerName = $ENV:COMPUTERNAME } } $vbsCapabilities = @() if ([string]::IsNullOrEmpty($PsSession)) { $vbsCapabilities += Invoke-Command -ScriptBlock $sb } else { $vbsCapabilities += Invoke-Command -Session $PsSession -ScriptBlock $sb } Log-CimData -cimData $vbsCapabilities $instanceResults = @() $instanceResults += foreach ($vbsCapability in $vbsCapabilities) { if ($vbsCapability.VbsCapable) { $status = 'SUCCESS' $detail = $lhwTxt.VbsCapable -f $vbsCapability.ComputerName Log-Info $detail } else { $status = 'FAILURE' $detail = $lhwTxt.VbsIncapable -f $vbsCapability.ComputerName Log-Info $detail -Type Warning } $instanceId = "Machine: $($vbsCapability.ComputerName), Class: Virtualization-based Security" $params = @{ Name = 'AzStackHci_Hardware_Test_VirtualizationBasedSecurity' Title = 'Test Virtualization-based Security' DisplayName = "Test Virtualization-based Security $($virtualDisk.ComputerName)" Severity = 'CRITICAL' Description = 'Checking Virtualization-based Security capability' Tags = @{} Remediation = 'https://aka.ms/hci-envch' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'Virtualization-based Security' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = $vbsCapability.ComputerName Resource = if ($vbsCapability.VbsCapable) { "Present" } else { "Not present" } Detail = $detail Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } 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 } } function Test-MountedMedia { <# .SYNOPSIS Test Mounted Media .DESCRIPTION Test is any media is mounted on the system such as CD, DVD, etc. #> [CmdletBinding()] param ( [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) try { $sb = { $cimParams = @{ ClassName = 'Win32_CDROMDrive' Property = '*' } $cimData = @(Get-CimInstance @cimParams) New-Object -Type PsObject -Property @{ MediaExists = [bool]$cimData.MediaLoaded cimData = $cimData ComputerName = $ENV:COMPUTERNAME } } $remoteOutput = @() if ([string]::IsNullOrEmpty($PsSession)) { $remoteOutput += Invoke-Command -ScriptBlock $sb } else { $remoteOutput += Invoke-Command -Session $PsSession -ScriptBlock $sb } Log-CimData -cimData $remoteOutput.CimData $instanceResults = @() $instanceResults += foreach ($media in $remoteOutput) { if ($media.MediaExists) { $status = 'FAILURE' $detail = $lhwTxt.MediaExists -f $media.ComputerName, ($media.CimData | % { "Name: $($_.Name), Media: $($_.Caption), Drive: $($_.Drive)" }) -join '. ' Log-Info $detail -Type CRITICAL } else { $status = 'SUCCESS' $detail = $lhwTxt.MediaNotExists -f $media.ComputerName Log-Info $detail } $instanceId = "Machine: $($media.ComputerName), Class: CDROMDrive" $params = @{ Name = 'AzStackHci_Hardware_Test_MountedMedia_Exists' Title = 'Test No Mounted Media exists' DisplayName = "Test No Mounted Media exists $($virtualDisk.ComputerName)" Severity = 'CRITICAL' Description = 'Checking mounted media does not exist' Tags = @{} Remediation = 'https://aka.ms/nomountedmedia' TargetResourceID = $instanceId TargetResourceName = $instanceId TargetResourceType = 'CDROMDrive' Timestamp = [datetime]::UtcNow Status = $status AdditionalData = @{ Source = $media.ComputerName Resource = if ($virtualDisk.MediaExists) { "Present" } else { "Not present" } Detail = $detail Status = $status TimeStamp = [datetime]::UtcNow } HealthCheckSource = $ENV:EnvChkrId } New-AzStackHciResultObject @params } return $instanceResults } catch { throw $_ } } Export-ModuleMember -Function Test-* # SIG # Begin signature block # MIIoRgYJKoZIhvcNAQcCoIIoNzCCKDMCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBClZ0DaFUywLaK # O/+by/pQXecH8cWmyAaY3Z6oNXX6nqCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0 # Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz # NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo # DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3 # a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF # HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy # 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC # Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj # L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp # h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3 # cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X # dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL # E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi # u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1 # sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq # 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb # DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/ # V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg # Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 # a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr # rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg # OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy # 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 # sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh # dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k # A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB # w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn # Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 # lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w # ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o # ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa # BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG # AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV # HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG # AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl # AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb # C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l # hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 # I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 # wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 # STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam # ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa # J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah # XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA # 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt # Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr # /Xmfwb1tbWrJUnMTDXpQzTGCGiYwghoiAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIC8Fs4KdwtWli0N3HIvxiRYU # 5Nk+H4Gp9XvCiCkR55I/MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAeQlZOqif2RepTdeMLfPMoQXvUUJ4d4hVPkQo7sUWQK7b49fIe68d7GQS # /lS9NYMY6HZ2AjmIwIRqq0OnUo4tz1bcbozagjlpCm7P9M2LAoNjz32yp6RzmneU # pDzb2RYievEj4i84VkTZG8XT3+XuvNLlgmrBo3M663AJaszWPeAZWkdQhW+P6qdW # oyZ/cx3HJwwSVVnzlrclEiydtQif9lvI0ZULz45qCYobsn+wQ5iSknB+xiUvKFtv # 5YSlq7pkxYm+M69dSiW+SpQVF04V/PZw8d0HPNNihBnpuxyIyJ2AXkPEhfKi33Yt # 3qN9Lfhl7Nq81rR7q75rk7q1A4ld9KGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCC # F5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq # hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCB4SrXxv+6ep4txAtUxKIijXDTBHvAIBZ2gZXY9QlpNIgIGZ0or4OWE # GBMyMDI0MTIwNDE1MDMzMS41NjZaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # Tjo1MjFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAACAAvXqn8bKhdWAAEAAAIAMA0G # CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u # MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp # b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0 # MDcyNTE4MzEyMVoXDTI1MTAyMjE4MzEyMVowgdMxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w # ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjUyMUEt # MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr1XaadKkP2TkunoTF573 # /tF7KJM9Doiv3ccv26mqnUhmv2DM59ikET4WnRfo5biFIHc6LqrIeqCgT9fT/Gks # 5VKO90ZQW2avh/PMHnl0kZfX/I5zdVooXHbdUUkPiZfNXszWswmL9UlWo8mzyv9L # p9TAtw/oXOYTAxdYSqOB5Uzz1Q3A8uCpNlumQNDJGDY6cSn0MlYukXklArChq6l+ # KYrl6r/WnOqXSknABpggSsJ33oL3onmDiN9YUApZwjnNh9M6kDaneSz78/YtD/2p # Gpx9/LXELoazEUFxhyg4KdmoWGNYwdR7/id81geOER69l5dJv71S/mH+Lxb6L692 # n8uEmAVw6fVvE+c8wjgYZblZCNPAynCnDduRLdk1jswCqjqNc3X/WIzA7GGs4HUS # 4YIrAUx8H2A94vDNiA8AWa7Z/HSwTCyIgeVbldXYM2BtxMKq3kneRoT27NQ7Y7n8 # ZTaAje7Blfju83spGP/QWYNZ1wYzYVGRyOpdA8Wmxq5V8f5r4HaG9zPcykOyJpRZ # y+V3RGighFmsCJXAcMziO76HinwCIjImnCFKGJ/IbLjH6J7fJXqRPbg+H6rYLZ8X # BpmXBFH4PTakZVYxB/P+EQbL5LNw0ZIM+eufxCljV4O+nHkM+zgSx8+07BVZPBKs # looebsmhIcBO0779kehciYMCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBSAJSTavgkj # Kqge5xQOXn35fXd3OjAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf # BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz # L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww # bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m # dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El # MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF # BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAKPCG9njRtIqQ # +fuECgxzWMsQOI3HvW7sV9PmEWCCOWlTuGCIzNi3ibdLZS0b2IDHg0yLrtdVuBi3 # FxVdesIXuzYyofIe/alTBdV4DhijLTXtB7NgOno7G12iO3t6jy1hPSquzGLry/2m # EZBwIsSoS2D+H+3HCJxPDyhzMFqP+plltPACB/QNwZ7q+HGyZv3v8et+rQYg8sF3 # PTuWeDg3dR/zk1NawJ/dfFCDYlWNeCBCLvNPQBceMYXFRFKhcSUws7mFdIDDhZpx # qyIKD2WDwFyNIGEezn+nd4kXRupeNEx+eSpJXylRD+1d45hb6PzOIF7BkcPtRtFW # 2wXgkjLqtTWWlBkvzl2uNfYJ3CPZVaDyMDaaXgO+H6DirsJ4IG9ikId941+mWDej # kj5aYn9QN6ROfo/HNHg1timwpFoUivqAFu6irWZFw5V+yLr8FLc7nbMa2lFSixzu # 96zdnDsPImz0c6StbYyhKSlM3uDRi9UWydSKqnEbtJ6Mk+YuxvzprkuWQJYWfpPv # ug+wTnioykVwc0yRVcsd4xMznnnRtZDGMSUEl9tMVnebYRshwZIyJTsBgLZmHM7q # 2TFK/X9944SkIqyY22AcuLe0GqoNfASCIcZtzbZ/zP4lT2/N0pDbn2ffAzjZkhI+ # Qrqr983mQZWwZdr3Tk1MYElDThz2D0MwggdxMIIFWaADAgECAhMzAAAAFcXna54C # m0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp # Y2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMy # MjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH # EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV # BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0B # AQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51 # yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY # 6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9 # cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN # 7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDua # Rr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74 # kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2 # K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5 # TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk # i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9Q # BXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3Pmri # Lq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUC # BBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJl # pxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y # eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA # YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU # 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny # bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw # MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w # Ni0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/yp # b+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulm # ZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM # 9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECW # OKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4 # FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3Uw # xTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPX # fx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVX # VAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGC # onsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU # 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEG # ahC0HVUzWLOhcGbyoYIDWTCCAkECAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # Tjo1MjFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAjJOfLZb3ivipL3sSLlWFbLrWjmSggYMw # gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF # AAIFAOr6mPAwIhgPMjAyNDEyMDQwOTAxMzZaGA8yMDI0MTIwNTA5MDEzNlowdzA9 # BgorBgEEAYRZCgQBMS8wLTAKAgUA6vqY8AIBADAKAgEAAgIbuwIB/zAHAgEAAgIS # SDAKAgUA6vvqcAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow # CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQBHgNG/1vgC # NVLC+4luI4uHBRgVj6v2+QIyujWPIQ1dYxsj+nuYd1iFcM8Bey5HGWHIVdgcAfp1 # kCncg7ZB/JYPUisBgmkhM1rFXE8zfj/hE2AM+YLaW1A3/jqlZJoArwhypDJyrCy8 # DpKnF7uzFGxmz96jr1f0cFYNMp827FWQvC+9ZwQo/DBcCq45A+5BfKrzuChDGJxA # SdVj2VRQQ6VcZiwmP84KxEaM6msBDQOrnL5xZznPHG84rDoRidGGGRqAdzW/4ooV # TvKjM//RaMxOCXZ8FJTqo423D8+mOd2P4WClH2glk/6TQZ49qaeaYKQzPqKsgLta # YClDCHdUsQloMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB # IDIwMTACEzMAAAIAC9eqfxsqF1YAAQAAAgAwDQYJYIZIAWUDBAIBBQCgggFKMBoG # CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQg2Ez5njrq # Jj1ZmbLp8aPa3I3wP3A3JLB20yxRfWvbUgcwgfoGCyqGSIb3DQEJEAIvMYHqMIHn # MIHkMIG9BCDUyO3sNZ3burBNDGUCV4NfM2gH4aWuRudIk/9KAk/ZJzCBmDCBgKR+ # MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT # HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACAAvXqn8bKhdWAAEA # AAIAMCIEIMaU6J1dp6uAu7CgFTMZBBbh4Qo+kpwMyZSFgys4/zMDMA0GCSqGSIb3 # DQEBCwUABIICABOS4qMIb/RrkjWQNGLPpgH763yaGzN9I4yRYlLgAXRNQz0FVVdo # cyHtpf8CX+o/YxFJfB0W9hll4fxZFPPUqrQZjq+NWzp0HiGTT4Ix6pI7wj7ltry7 # 1EL7J51DZHGmt7a+uaDF6QjAbcgHO4EYKVh1QbBFoDr+ZubT3BFGzZY8hVKm538g # ISnOFcXwz7N/ZCRCqRJgf0Equ9JCG3NOdj2qYyC++kX6Ku9xs3qFRzzNnT8mWpLh # p93avLkpw1gxRwGLcGrGXJI2DSPGG5cQh/dC9cgGsbnZrUBOTJzfSR4SJ4tF3AoT # jAk9sT5Ig9oyTnBw08wuKcDQQ4KirYYe7d39G0SzHBVtRvHLQ5r02EXxD1aZXjOC # LwpydcmvwcqftH8toIrpjaoNKEP8XfZdz6/Nf4RyAE3h+AVWscWDxTiGtHKirH3D # nvNQL7xrAko7W+9/hx87o10SVNtDT4ygjeWfEr8KTZDHRfikmhhoksBhhjB9dW01 # 95+Bxj1IU8Q1HRQk7iKe1U0QDRaP1xqa1+5rT4RK1DHl8DK/YyEe+RGndmsZU8bf # v8Jt5JWzv+P380BCjSAgcxIJHpt+L/BgKYOhWwplc4+502egB932s30exvZFN65P # tJM6joEVTuunYTk1RY+MS/3FIWZi0aYUTgaHmEPLBVhD/GLJlWy3MR6N # SIG # End signature block |