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 (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
        $cimData = $remoteOutput.cimData

        $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' }
        }

        Log-CimData -cimData $cimData -Properties $matchProperty,$desiredPropertyValue

        # 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 + $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
        $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
    {
        $minimumMemory = 32GB
        $instanceResults = @()
        $AdditionalData = @()
        $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
        $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 = 'Failed'
                Log-Info $dtl -Type Warning
            }
            else
            {
                $Status = 'Succeeded'
                Log-Info $dtl
            }

            $instanceResult = New-Object AzStackHciHardwareTarget
            $instanceResult.Name = 'AzStackHci_Hardware_Test_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 + $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
        $cimData = $remoteOutput.cimData

        $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
        }
        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 }
            $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 + $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
        $cimData = $remoteOutput.cimData

        $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'
        }
        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
            $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-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
        $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
        $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
    )
    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 (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 PhysicalDisk
        $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)

            # 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'}
        $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 ($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 + $cimTest)
    }
    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 -Name $env:COMPUTERNAME* | Get-PhysicalDisk -PhysicallyConnected | Where-Object { $_.BusType -in $allowedBusTypes -and $_.MediaType -in $allowedMediaTypes -and $_.DeviceId -notin $bootPhysicalDisk.DeviceId})
            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 PhysicalDisk
        $cimData = $remoteOutput.cimData

        $PropertyResult = @()
        $criticalDesiredPropertyValue = @{
            CanPool             = @{ value = @($global:PhysicalDiskCanPool); DiagnosticProperty = 'CannotPoolReason' }
        }

        Log-CimData -cimData $cimData -Properties CanPool, CannotPoolReason, Size, PhysicalLocation, UniqueId, FriendlyName, 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"'
        [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 + $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

            $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 ($version)"
            $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. Expected $version"
                Status    = if ($instanceVersion -eq $version) { 'Succeeded' } else { 'Failed' }
                TimeStamp = [datetime]::UtcNow
            }
            $instanceResult.AdditionalData = $AdditionalData
            $instanceResult.Status = if ($AdditionalData.Status -contains 'Failed') { 'Failed' } else { 'Succeeded' }

            if ($InstanceResult.AdditionalData.Status -eq 'Succeeded')
            {
                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)"

                $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
        }
        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)"

            $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
        }
        Log-CimData -cimData $secureboots
        $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
        }
        Log-CimData -cimData $StoragePoolsExist

        $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
        }
        Log-CimData -cimData $results
        $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'
        )

        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 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 = @()
        Log-CimData -cimData $cimData -Properties NumberOfCores

        # 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
        }
        Log-CimData -cimData $VirtualDiskExists
        $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
# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAxU4LSvd+wawhX
# TlpMUzJ3aKGkdNOCRpBjcH2Tjk/rSKCCDXYwggX0MIID3KADAgECAhMzAAADTrU8
# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU
# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1
# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm
# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa
# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq
# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk
# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31
# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2
# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d
# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM
# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh
# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX
# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir
# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8
# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A
# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H
# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIIQ/Trs2Z+qm4l7x0wi5fv1L
# YRCUkDw5kvlxwRtcjKbHMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEARyMHZ0o8vtzItDQNfNGY0rFPgQJNpGP3uIZ6seBzE59xHzao5nlhhDZs
# Onujo28SSAb0rWAHA4YZB1EuKWa6069RGr/iI8Xfh48bWAUDn/ioPsK+UXQ4Q30Y
# xu7Yfqi+7GpgNNNug3aMm0dAWbs20RxU12b5qCT72+B+66x+aRDSnHZiR+ZO0Ast
# 9PLrBwAp8TOul9Jkrqm7cTcmQyuseJVqqyjdTaHPamDO0jpZiqxtdHMsYSU6LItb
# OoyqA64OMWc8V/DCZm1yki6fOtd9iULfCu3B0gB/wu281dkLenO6YeStMIBPSqTg
# nB/uxuFHrs8NHnzp4vMEh5Bj+UiTOqGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC
# F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCBvWGbyyI1nB406Je++IVjpldcb0E4Qrvg4cWwV0iv6kAIGZMvM4HkW
# GBMyMDIzMDgwNzIxMzMyOC41OTFaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTAwMC0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHqMIIHIDCCBQigAwIBAgITMwAAAdB3CKrvoxfG3QABAAAB0DANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzA1MjUxOTEy
# MTRaFw0yNDAyMDExOTEyMTRaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTAwMC0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQDfMlfn35fvM0XAUSmI5qiG0UxPi25HkSyBgzk3zpYO
# 311d1OEEFz0QpAK23s1dJFrjB5gD+SMw5z6EwxC4CrXU9KaQ4WNHqHrhWftpgo3M
# kJex9frmO9MldUfjUG56sIW6YVF6YjX+9rT1JDdCDHbo5nZiasMigGKawGb2HqD7
# /kjRR67RvVh7Q4natAVu46Zf5MLviR0xN5cNG20xwBwgttaYEk5XlULaBH5OnXz2
# eWoIx+SjDO7Bt5BuABWY8SvmRQfByT2cppEzTjt/fs0xp4B1cAHVDwlGwZuv9Rfc
# 3nddxgFrKA8MWHbJF0+aWUUYIBR8Fy2guFVHoHeOze7IsbyvRrax//83gYqo8c5Z
# /1/u7kjLcTgipiyZ8XERsLEECJ5ox1BBLY6AjmbgAzDdNl2Leej+qIbdBr/SUvKE
# C+Xw4xjFMOTUVWKWemt2khwndUfBNR7Nzu1z9L0Wv7TAY/v+v6pNhAeohPMCFJc+
# ak6uMD8TKSzWFjw5aADkmD9mGuC86yvSKkII4MayzoUdseT0nfk8Y0fPjtdw2Wne
# jl6zLHuYXwcDau2O1DMuoiedNVjTF37UEmYT+oxC/OFXUGPDEQt9tzgbR9g8HLtU
# fEeWOsOED5xgb5rwyfvIss7H/cdHFcIiIczzQgYnsLyEGepoZDkKhSMR5eCB6Kcv
# /QIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFDPhAYWS0oA+lOtITfjJtyl0knRRMB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQCXh+ckCkZaA06SNW+qxtS9gHQp4x7G+gdi
# kngKItEr8otkXIrmWPYrarRWBlY91lqGiilHyIlZ3iNBUbaNEmaKAGMZ5YcS7IZU
# KPaq1jU0msyl+8og0t9C/Z26+atx3vshHrFQuSgwTHZVpzv7k8CYnBYoxdhI1uGh
# qH595mqLvtMsxEN/1so7U+b3U6LCry5uwwcz5+j8Oj0GUX3b+iZg+As0xTN6T0Qa
# 8BNec/LwcyqYNEaMkW2VAKrmhvWH8OCDTcXgONnnABQHBfXK/fLAbHFGS1XNOtr6
# 2/iaHBGAkrCGl6Bi8Pfws6fs+w+sE9r3hX9Vg0gsRMoHRuMaiXsrGmGsuYnLn3Aw
# TguMatw9R8U5vJtWSlu1CFO5P0LEvQQiMZ12sQSsQAkNDTs9rTjVNjjIUgoZ6XPM
# xlcPIDcjxw8bfeb4y4wAxM2RRoWcxpkx+6IIf2L+b7gLHtBxXCWJ5bMW7WwUC2Ll
# tburUwBv0SgjpDtbEqw/uDgWBerCT+Zty3Nc967iGaQjyYQH6H/h9Xc8smm2n6Vj
# ySRx2swnW3hr6Qx63U/xY9HL6FNhrGiFED7ZRKrnwvvXvMVQUIEkB7GUEeN6heY8
# gHLt0jLV3yzDiQA8R8p5YGgGAVt9MEwgAJNY1iHvH/8vzhJSZFNkH8svRztO/i3T
# vKrjb8ZxwjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN
# MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkEwMDAtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQC8
# t8hT8KKUX91lU5FqRP9Cfu9MiaCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6HuRTTAiGA8yMDIzMDgwNzE1NTAz
# N1oYDzIwMjMwODA4MTU1MDM3WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDoe5FN
# AgEAMAcCAQACAgn7MAcCAQACAhNFMAoCBQDofOLNAgEAMDYGCisGAQQBhFkKBAIx
# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI
# hvcNAQELBQADggEBACZlXGYildwi8A88nYqHN2HM8QyUZ5EXL5IPenhvckRdHdyV
# J/3DY3YgoI6CqwPJpipX1fuIzQkVYtLr/uoxJ6N3DNGTuYbJDaIRDq9IEHN0w5qG
# XGcCbtVZOlT87v6LpsZb2t7Ux0hmQi7tk163YL0ddGk+Fzc0PZPah3PLeH6Tzic4
# x2o7PXTjvOML4xfKsIsslRzZ3lBrCY65hu2M63z+gzOHFBa70UpDoWPqfeJXsD4m
# //BCuOrgEz6DX1gLxrDdkNBSfEKXDOJLhteWhVkENhnAd/NV4TsvR34vkTYngmQr
# 1mQjS42i1S5bIMBIA+YqchPVH+5rtnpAjjCkCkwxggQNMIIECQIBATCBkzB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAdB3CKrvoxfG3QABAAAB0DAN
# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G
# CSqGSIb3DQEJBDEiBCAOc77G0Dzx6J8dS50EJ8bQSLJd5Uueo+zwFi7BJIIDDTCB
# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIAiVQAZftNP/Md1E2Yw+fBXa9w6f
# jmTZ5WAerrTSPwnXMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTACEzMAAAHQdwiq76MXxt0AAQAAAdAwIgQgq7K70TUQtuVlHE2zaimieeN4nULR
# QcffxbgsiK4lvXcwDQYJKoZIhvcNAQELBQAEggIASpRmiXqHJJWORgaOjsCmEVsT
# q6OovA4rFinExVbT7Tw6iKicu+mG61vjzPb1ggaEJsOraNI7Z6SvRTNqM9UcIMT7
# OMVNFAjfjhjNTjB+or1yk7AyLX3PI05gqrtNEnTVGTN+z2nKDSB9KZ5eJLQT5G4s
# uIviXiaFb4yrcWD/Il0Nfi9ryKfb2fZXz7LafSULwY3v6WjvIJINb5mElCfQ+163
# r1VjqVPIPYD/A4Pl8d/nGY4hCeDmLLs4lcMxkA90I2z+on4dZukmFC2KsXycRP+x
# Vi8a5SwYKeDuvfNx4mv9gl9cLhMSiN6jk7vGmj7auyJJznTqBL8zb3ro71qAK8Xn
# /MtZKUQ6TEmQ4fCf3aObR8CFHcCb4upbjoVviECBaWvLE8WTk82Ax22l4cuyOWQ8
# au4XKBcgzZcQSgkcwczbgz6mPb75hHd1mAyN7mfeUXXO1F7GVv5mLArn871dVXsK
# v1mwXHsA/DJob5Enk6MVuKZYqf9wYxWK5ZFVwPZUn/RaQuCTaiJaxfrfl5j6kCj0
# kPHCsyPgg7Y5HyToEBtxuXVX98VWyv2lEV7Yt6mB2werEI/0K5o+T/iW24KVgWsP
# Zj+JP5qRpVJZ7k/aKSi23tnIA9ZuparwA/4qZ8VjCwa3N2MqCehb2FuhHbmfeta6
# ZcyxGbFwB53Lx1Bcb4M=
# SIG # End signature block