Public/Test-NexthinkNutanixConnector.ps1

function Test-NexthinkNutanixConnector {
    <#
    .SYNOPSIS
        Checks a Nutanix connector installation and reports, for each problem, what to fix.
    .DESCRIPTION
        Runs the checks below in order and prints a PASS/WARN/FAIL/SKIP report. A check whose
        prerequisite failed is skipped instead of failing again.

        - Environment: PowerShell version, operating system, execution policy, current account.
        - Prerequisites: Logging and CredentialManager modules, connector module signature.
        - Configuration: config.json found, valid, environment present, no template placeholder, host
          names without scheme or port, risky options, log level, and both credential entries readable
          by the current account (user name only shown).
        - Logging: write access to the connector folder and to Logs, State, Spool and Storage, then one
          marker line written to today's connector log file.
        - Connectivity: DNS, TCP and a TLS handshake to Prism Central and Nexthink (skipped for an
          endpoint reached through the system proxy: the API checks test that path), then one read-only
          Prism Central call, a Nexthink access token, and one test entry prefixed with [diagnose] sent
          to the logs API.
        - Scheduling: on Windows, the scheduled tasks starting the Runner for this environment, their
          account, executable, execution policy, state and last result. Then, unless -SkipCollection is
          set, one real, read-only collection cycle against Prism Central in a new collection-<id> folder
          under OutputPath.

        Nothing is sent to Nexthink except the [diagnose] test entry, and nothing is changed in the
        connector folder except one marker line appended to today's log file. The report is also saved
        to a text and a JSON file.
    .PARAMETER NutanixEnvironment
        The environment name, as in nutanixEnvironments[].Name in Config/config.json.
    .PARAMETER ScriptRootPath
        The connector folder, the one holding Config/config.json and the Runner. Defaults to the current folder.
    .PARAMETER SkipCollection
        Skips the collection test. By default the checks end with one collection cycle against Prism
        Central with every connector folder redirected to a collection-<id> folder created under
        OutputPath: the connector's state, spool and VM cache are not read or written, even when
        OutputPath is the connector folder, and the payloads are written to files instead of being sent.
    .PARAMETER OutputPath
        Where the report, the collection test's DEBUG log and the payload files are written. Defaults to a
        new folder under the temporary directory.
    .PARAMETER PassThru
        Returns the result records instead of the exit code.
    .OUTPUTS
        0 when no check failed, otherwise 1. With -PassThru, the result records.
    .EXAMPLE
        Test-NexthinkNutanixConnector -NutanixEnvironment 'prod' -ScriptRootPath 'C:\NutanixConnector'
    .EXAMPLE
        Test-NexthinkNutanixConnector -NutanixEnvironment 'prod' -ScriptRootPath 'C:\NutanixConnector' -SkipCollection -Verbose

        -Verbose also prints the connector's DEBUG log lines while the checks run.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$NutanixEnvironment,

        [Parameter(Mandatory = $false)]
        [string]$ScriptRootPath = (Get-Location).Path,

        [Parameter(Mandatory = $false)]
        [switch]$SkipCollection,

        [Parameter(Mandatory = $false)]
        [string]$OutputPath,

        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )

    $version = Get-ModuleVersion
    $identity = Get-DiagnosticCurrentIdentity
    $onWindows = (Get-OperatingSystemType) -eq 'Windows'
    $consoleLogLevel = if ($VerbosePreference -eq 'Continue') { 'DEBUG' } else { 'OFF' }
    $previousConsoleLogLevel = $script:CONSOLE_LOG_LEVEL
    $results = [System.Collections.Generic.List[object]]::new()

    $ScriptRootPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ScriptRootPath)
    if ([string]::IsNullOrWhiteSpace($OutputPath)) {
        $safeEnvironment = $NutanixEnvironment -replace '[^A-Za-z0-9._-]', '_'
        $OutputPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) `
            -ChildPath "nutanix-connector-diagnose-$safeEnvironment-$((Get-Date).ToString('yyyyMMdd-HHmmss'))"
    }
    $OutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)

    try {
        $script:IN_MEMORY_LOGS.Clear()
        Set-Variable -Name 'CONSOLE_LOG_LEVEL' -Value $consoleLogLevel -Scope Script -Force

        #
        # Environment and prerequisites
        #
        Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Environment' -Check 'Connector module' `
                -Status 'INFO' -Detail "Nexthink-Nutanix-Connector $version from $script:ModuleRoot")
        Add-DiagnosticResult -Results $results -Result (Test-DiagnosticPowerShellVersion)
        Add-DiagnosticResult -Results $results -Result (Test-DiagnosticOperatingSystem)
        Add-DiagnosticResult -Results $results -Result (Test-DiagnosticExecutionPolicy `
                -RunnerPath (Join-Path -Path $ScriptRootPath -ChildPath $script:DIAGNOSTIC_RUNNER_SCRIPT_NAME))
        Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Environment' -Check 'Current account' `
                -Status 'INFO' -Detail $identity `
                -Hint 'Credential entries belong to one account: run this as the account the scheduled task uses.')
        Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Environment' -Check 'Connector folder' `
                -Status 'INFO' -Detail $ScriptRootPath)

        $loggingModule = Test-DiagnosticModuleInstalled -Name 'Logging'
        Add-DiagnosticResult -Results $results -Result $loggingModule
        if ($onWindows) {
            Add-DiagnosticResult -Results $results -Result (Test-DiagnosticModuleInstalled -Name 'CredentialManager')
        }
        Add-DiagnosticResult -Results $results -Result (Test-DiagnosticModuleSignature -ModuleRoot $script:ModuleRoot)

        #
        # Configuration
        #
        $config = $null
        $configPath = Get-DiagnosticConfigurationPath -ScriptRootPath $ScriptRootPath
        if ($null -eq $configPath) {
            Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Configuration' -Check 'Configuration file' `
                    -Status 'FAIL' -Detail "No Config/config.json under $ScriptRootPath." `
                    -Hint 'Run this from the connector folder, or pass -ScriptRootPath <connector folder>.')
        }
        else {
            Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Configuration' -Check 'Configuration file' `
                    -Status 'PASS' -Detail $configPath)
            try {
                # Stop-ConfigError writes to the error stream before throwing; the report carries the message.
                $config = Get-Configuration -ScriptRootPath $ScriptRootPath -NutanixEnvironment $NutanixEnvironment 2>$null
                Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Configuration' -Check 'Configuration valid' `
                        -Status 'PASS' -Detail "Environment '$NutanixEnvironment' found and valid.")
            }
            catch {
                $hint = 'Fix the value named in the message in Config/config.json.'
                $environmentNames = @(Get-DiagnosticEnvironmentNames -ConfigurationPath $configPath)
                if ($_.Exception.Message -like '*not found in configuration*' -and $environmentNames.Count -gt 0) {
                    $hint = "Environments in the file: $($environmentNames -join ', '). Pass one of them as -NutanixEnvironment."
                }
                Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Configuration' -Check 'Configuration valid' `
                        -Status 'FAIL' -Detail $_.Exception.Message -Hint $hint)
            }
        }

        if ($null -eq $config) {
            Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Configuration' -Check 'Remaining checks' `
                    -Status 'SKIP' -Detail 'Credentials, logging and folders, connectivity, API access, scheduling and the collection test need a valid configuration.')
        }
        else {
            Add-DiagnosticResult -Results $results -Result (Test-DiagnosticConfigurationValues -Config $config)
            $environmentName = $config.EnvironmentConfig.Name

            # Credentials are local reads of the entries named in the configuration, so they are part of it.
            $prismCredential = Test-DiagnosticCredential -Check 'Prism Central credential' -Target $config.EnvironmentConfig.WindowsCredentialEntry
            $nexthinkCredential = Test-DiagnosticCredential -Check 'Nexthink API credential' -Target $config.NexthinkAPI.WindowsCredentialEntry
            Add-DiagnosticResult -Results $results -Result $prismCredential
            Add-DiagnosticResult -Results $results -Result $nexthinkCredential

            #
            # Logging and folders: the log file goes into the Logs folder, so the folders come first.
            #
            Add-DiagnosticResult -Results $results -Result (Test-DiagnosticFolderAccess -Path $ScriptRootPath -Check 'Connector folder')
            $folderResults = @{}
            foreach ($folder in $script:DIAGNOSTIC_DATA_FOLDERS) {
                $folderResults[$folder] = Test-DiagnosticFolderAccess -Check $folder `
                    -Path (Join-Path -Path $ScriptRootPath -ChildPath (Join-Path -Path $folder -ChildPath $environmentName))
                Add-DiagnosticResult -Results $results -Result $folderResults[$folder]
            }

            if ($loggingModule.Status -eq 'PASS' -and $folderResults['Logs'].Status -eq 'PASS') {
                Add-DiagnosticResult -Results $results -Result (Test-DiagnosticLocalLogging -Config $config -ConsoleLogLevel $consoleLogLevel `
                        -Message "$($script:DIAGNOSTIC_LOG_MARKER) Diagnostic run by Test-NexthinkNutanixConnector $version as $identity")
            }
            else {
                Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Logging' -Check 'Local log file' `
                        -Status 'SKIP' -Detail 'Needs the Logging module and a writable Logs folder.')
            }

            #
            # Connectivity and API access: DNS, TCP and TLS first, no credential needed. Behind a proxy the
            # probe is INFO and the API checks still run: they go through the proxy like the connector.
            #
            $prismNetwork = Test-DiagnosticEndpoint -Check 'Prism Central network' `
                -HostName $config.EnvironmentConfig.PrismCentralFQDN -Port $config.EnvironmentConfig.PrismCentralPort `
                -TlsHint 'PrismCentralPort must be the Prism Central HTTPS port (9440 by default). Also check that no firewall or proxy intercepts the connection.'
            $loginNetwork = Test-DiagnosticEndpoint -Check 'Nexthink login network' -HostName $config.NexthinkAPI.LoginFQDN -Port 443
            $apiNetwork = $loginNetwork
            Add-DiagnosticResult -Results $results -Result $prismNetwork
            Add-DiagnosticResult -Results $results -Result $loginNetwork
            if ($config.NexthinkAPI.HostFQDN -ne $config.NexthinkAPI.LoginFQDN) {
                $apiNetwork = Test-DiagnosticEndpoint -Check 'Nexthink API network' -HostName $config.NexthinkAPI.HostFQDN -Port 443
                Add-DiagnosticResult -Results $results -Result $apiNetwork
            }

            # Authenticated calls, needing both the network and the credential
            $prismApi = $null
            if ($prismCredential.Status -eq 'PASS' -and $prismNetwork.Status -ne 'FAIL') {
                $prismApi = Test-DiagnosticPrismCentralApi -EnvironmentConfig $config.EnvironmentConfig
                Add-DiagnosticResult -Results $results -Result $prismApi
            }
            else {
                Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Connectivity' -Check 'Prism Central API' `
                        -Status 'SKIP' -Detail 'Needs the Prism Central credential and network checks to pass.')
            }

            $token = $null
            if ($nexthinkCredential.Status -eq 'PASS' -and $loginNetwork.Status -ne 'FAIL') {
                $token = Test-DiagnosticNexthinkToken -NexthinkApi $config.NexthinkAPI
                Add-DiagnosticResult -Results $results -Result $token
            }
            else {
                Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Connectivity' -Check 'Nexthink access token' `
                        -Status 'SKIP' -Detail 'Needs the Nexthink API credential and login network checks to pass.')
            }

            if ($null -ne $token -and $token.Status -eq 'PASS' -and $apiNetwork.Status -ne 'FAIL') {
                Add-DiagnosticResult -Results $results -Result (Test-DiagnosticLogsApi -Config $config -Version $version)
            }
            else {
                Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Connectivity' -Check 'Nexthink logs API' `
                        -Status 'SKIP' -Detail 'Needs an access token and the Nexthink API network check to pass.')
            }

            #
            # Scheduling and collection test
            #
            Add-DiagnosticResult -Results $results -Result (Test-DiagnosticScheduledTask -NutanixEnvironment $environmentName -CurrentIdentity $identity)

            if ($SkipCollection) {
                Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Scheduling' -Check 'Collection cycle' `
                        -Status 'SKIP' -Detail 'Skipped with -SkipCollection.')
            }
            elseif ($null -eq $prismApi -or $prismApi.Status -ne 'PASS') {
                Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Scheduling' -Check 'Collection cycle' `
                        -Status 'SKIP' -Detail 'Needs the Prism Central API check to pass.')
            }
            else {
                New-Item -Path $OutputPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
                Add-DiagnosticResult -Results $results -Result (Invoke-DiagnosticCollection -Config $config -OutputPath $OutputPath -ConsoleLogLevel $consoleLogLevel)
            }
        }
    }
    catch {
        Add-DiagnosticResult -Results $results -Result (New-DiagnosticResult -Category 'Diagnostics' -Check 'Unexpected error' `
                -Status 'FAIL' -Detail (Get-DiagnosticErrorMessage -ErrorRecord $_) `
                -Hint 'Run again with -Verbose and share the report with Nexthink support.')
    }
    finally {
        # Nothing collected while diagnosing may be shipped by a later connector run in this session.
        $script:IN_MEMORY_LOGS.Clear()
        Wait-LoggingFlush
        Set-Variable -Name 'CONSOLE_LOG_LEVEL' -Value $previousConsoleLogLevel -Scope Script -Force
    }

    $title = "Nexthink Nutanix connector diagnostics - environment '$NutanixEnvironment', module $version, $((Get-Date).ToString('u'))"
    $lines = [System.Collections.Generic.List[object]]::new()
    foreach ($line in (ConvertTo-DiagnosticReportLines -Results $results.ToArray() -Title $title)) {
        $lines.Add($line)
    }

    try {
        New-Item -Path $OutputPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
        $reportFile = Join-Path -Path $OutputPath -ChildPath 'diagnostics-report.txt'
        Set-Content -LiteralPath $reportFile -Value @($lines | ForEach-Object { $_.Text }) -Encoding utf8 -ErrorAction Stop
        $results | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path -Path $OutputPath -ChildPath 'diagnostics-report.json') -Encoding utf8 -ErrorAction Stop
        $lines.Add([pscustomobject]@{ Text = "Report saved to $reportFile"; Colour = 'White' })
    }
    catch {
        $lines.Add([pscustomobject]@{ Text = "Could not save the report to $OutputPath. $(Get-DiagnosticErrorMessage -ErrorRecord $_)"; Colour = 'Yellow' })
    }

    Write-DiagnosticReport -Lines $lines.ToArray()

    if ($PassThru) {
        return $results.ToArray()
    }

    if (@($results | Where-Object { $_.Status -eq 'FAIL' }).Count -gt 0) {
        return 1
    }
    return 0
}

# SIG # Begin signature block
# MIIohQYJKoZIhvcNAQcCoIIodjCCKHICAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBvYxHcGHTSJdtp
# 1mjT8eX4pldV5eFGp9mhDyIGJ1nDdqCCIZ0wggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg
# MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit
# eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS
# 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM
# swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC
# Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3
# /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j
# q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5
# OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo
# 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU
# tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm
# KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP
# TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq
# hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK
# r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda
# qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+
# lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a
# brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS
# y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK
# iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb
# KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q
# xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm
# zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn
# HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w
# gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1
# c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo
# dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi
# 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg
# xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF
# cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ
# m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS
# GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1
# ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9
# MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7
# Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG
# RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6
# X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd
# BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx
# XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF
# BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo
# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy
# bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL
# BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj
# aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0
# hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0
# F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT
# mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf
# ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE
# wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh
# OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX
# gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO
# LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG
# WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg
# AwIBAgIQCE/cM09+RU7bww+P+ZIYNTANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG
# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0
# IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex
# MB4XDTI2MDgwNTAwMDAwMFoXDTM3MTEwNDIzNTk1OVowYzELMAkGA1UEBhMCVVMx
# FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy
# NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjYgMTCCAiIwDQYJKoZI
# hvcNAQEBBQADggIPADCCAgoCggIBALZ7pvLJ/s1K+NSbTGWz/TjGMPh8CQ6RucZC
# Lv5anHzWJjF/NWJrFIhy24fcpKXlgRiky4WAawDfU3YP0BMxt9l3Dm5oCG5Z69Aq
# EN1kgHg2epx+l+lZBcmJCcN0ASURML5uFIS80sZsDwO3BSkUxDjLJhBI+qiZP3ai
# xAC/qEGLjsBNlLol9VZ7pfGEXiMlneJIC5/YKuizVzNFKZZEeoy/0B8Zm+nzKBgS
# WG52lCO1w+nCg6XpCtklTJXeIg283hw7TmmsZXR+SMbjbrEOvZ3fP2VxIgeR28Y9
# 0ZStd3F9VuA5RVynb/whITPAo9b75Zr4Ta6Mj3URm26QZYMn/FnbuTegcoRcFEZ9
# FOqM5T6MTdtr/n74lIT/ug0eeOzmZ6QTFg33otX+bFRsIolvykE1jive4PuESaT8
# zzVeFWDAMDtozNgLctkGD1ZjkEyZtJrLl5ya0m5doH/ScpaZCZVl6pNUOCybMc/k
# xC6EAmSJY24L0yYKD1Nkddsnb/ItVKi/2nXpQNMu1PT5prW83vV8d67WowuUs0Hd
# Y4H8AMLGvdL/WHEj3ZnqMqAQQP9u3Ai9t+5eQ02GDwy0ODjdzi0xlp70W+ow63/0
# ++YDEX1M0iwgUHwbrJvfpklkZQvw3+kv3vUPItdwroczk9icflf55W1zOEKAcJVA
# IXpcMCU9AgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQUyWOK
# MC7USvtulPPm40B+9ezN4jAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL
# TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG
# AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
# b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp
# Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy
# dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j
# cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB
# CwUAA4ICAQCNxTphHp1SCt+ZrAmAfn0oQLFr0mLywSLaDXQIENoyKqxrFbJblzCV
# P/pkXmwXOdrOpWygLzlT12os5ipDCy35RBCg2UMeApEtrfGhz45F4Wt4WGdNdIbR
# Wt3YTYJmpR+b7lr4d7Uwn+H600u4D7RnOGf8Wj4UNgAdZkfHhHv1mx9EVh71SJel
# cEN/oORSjXzdjfw1iZH9d8Nh/thn6hH23d+VsPAr6GAYyzSA02nXD1nYLI7Ijmiv
# +xLCiYC41DSFYL3GhTiy0PxpawPtGRyaBVGzq+UiTfM8pD7KVyF5aQyWP4KhVGUU
# Tnmm/RlYJoW3TiXA/+t0YcT2oRVBm3JETjajHug2AL+v5jhtKVnd3D0rbHXEu27o
# +Q8p4sEWPMqKDB+qbceb6T/6WcwTwXmQ9lOCLLYcsQeSWmvKqzpAec9etE14jOQA
# zLKWdE3w/TCaKtLRaRT7LCkRYVnhA2D73FLje1O5b3HR5eHs0NzU/+xX7NbEdcof
# y0W3Wdwd1XOqtlpg/JgwtKfZM5dqO94lbUveOiJBI+xZEbGRsMNbXmMREUTgu+Oc
# a7Y73MPWcslIx2VhkSKSXjDbD6rgg39H5Mh7QfieAIjWagkJNt68Yfim6cjEzVSi
# LSeZfdkr5dtFPTW6jATlWJdYeeDRGCyatf8R1hSjzSvdN8yWQPT9gzCCB6swggWT
# oAMCAQICEA20PGu9jXp4I/Dy4yL8Si8wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MTAeFw0yNTExMTMwMDAwMDBaFw0yODExMTQyMzU5NTlaMIGzMRMwEQYLKwYBBAGC
# NzwCAQMTAkNIMRUwEwYLKwYBBAGCNzwCAQITBFZhdWQxHTAbBgNVBA8MFFByaXZh
# dGUgT3JnYW5pemF0aW9uMRgwFgYDVQQFEw9DSEUtMTEyLjAwMC41NzkxCzAJBgNV
# BAYTAkNIMQ8wDQYDVQQHEwZQcmlsbHkxFjAUBgNVBAoTDU5FWFRoaW5rIFMuQS4x
# FjAUBgNVBAMTDU5FWFRoaW5rIFMuQS4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQDIbNJpFq6aDbvQP7zFPymmt3WRPkvbmnRL4g0imzFNbaj+U/RIGPL0
# 3l9TtYxzraPbwDdidmT5P50mK1JqjF5FzcZEC4kZfMMojY4J/gOylo8jOdLjBscL
# SYwsY+85ZRs2bvxibPXIu4AcZttxBx5XdbVyt2Bd1vtuJGj+wfU1vJu7NB3GHjPy
# 4QE1BFrgidqQTL+1HASQaWwNMetW8B1U1POZjr6uWp6Ga3DF/MAKxpbvU3iVFI8b
# YFpKiVIQ9Namj3DQ7Xj26+fSEP959+bukG8LyObHN5DDkRdpnCSVVMT6qxZ2QGri
# DFkyjOVaGkjJTI6mbKY1JPUJhYVo+7Jqhu+2EFGRy6wSj9wZBubnbwfuvgr/XlH9
# /xQQuX9i/EJvxJRQQyz9IzLtB/DBSoEQvtyCh8Po87k9x9CGYn7k1HxUg1F6oBKa
# kemv3G474RWKZwSEIbwUlygFydEpTDkRTqBDZKOQf3VaUvfUgGmBmEfq1m+cljHj
# fLDBtkDIGaZ7SvyO72q9Uj0HrIWvJDHHIYK84gqhe58vKgaQ0uH9V5BCUT5cLvau
# +1w93iq5AMCTtG96SEyjI/T9Jzh+2sM+PoUwlLNLqYFUvMI+l19X+5UtQnjU5k1c
# ooxqCebatsS642gd3X1VoeNyfJ5FB67b6KGNfrDOXBt5FXgKZAUOHwIDAQABo4IC
# AjCCAf4wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYE
# FICGOh06CZAfMG3M4l2zy9590p0OMD0GA1UdIAQ2MDQwMgYFZ4EMAQMwKTAnBggr
# BgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMA4GA1UdDwEB/wQE
# AwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1o
# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2ln
# bmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2
# U0hBMzg0MjAyMUNBMS5jcmwwgZQGCCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUFBzAB
# hhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6Ly9j
# YWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5n
# UlNBNDA5NlNIQTM4NDIwMjFDQTEuY3J0MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQEL
# BQADggIBALSLCU45H6xjr2Ce9mmr6WtqdnykOcc2s1iqd/UN88f4GlmzVmtA9Dts
# 9j8X77hxHYIAJwWM3NqKi8m55CB5HQBuOC3hOI8bWFdQLBsrAbBAApFh3cjRruGn
# r0mu7wlZo9HLgpM1PEy+hf90+cANYRBITrRkfVT4o2rTc8mmJyYzZM8WVwxGx+wm
# PAuvyjYKsjZNTeJ+miYqwJ/zHjc8LQP+/hYPNmP9ubYpMMkgC49vP9Uir0E4YI2C
# VTbUuhxao4Pmy2MnqhK20lDu50ZCNDNFurYhzXWBhK1cel6Ku4yuT6TlsjNye0AY
# v92FDyAaYH9VZuub7qZBNv/RwmcLUDqqh5rnEdeGMH7KFkUyN63RMEr7hLXV/Tav
# 7K0kWlXKPLIFenfqJnPYceni6PdJS/7WlxnBE0UwgoSoe4HawnEF0YvYBU3uyK+v
# TJWVau5Jptjp94uEQmW6PidWwUWNzVl4LQ7eEZe4ykZ0JLErbGnOeoUoZCeVzpx4
# uUEFiZv1KLeLEkB/8yFIrQp03sVMItqhNJ5mbDOoWW4y3Z8/jduhxVPwJMHhgglK
# 3z3sZr5xrpY3n1Qcqn1xFsI6U7rOrkzyQBw+d/yF82MW3LldZ7tPyU8LTEO89oOC
# EsGFwOfEb/wB7UJ1NulZ1R2nf5erYEnVsOsrGV0X1fYh2OoRQyxWMYIGPjCCBjoC
# AQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/
# BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYg
# U0hBMzg0IDIwMjEgQ0ExAhANtDxrvY16eCPw8uMi/EovMA0GCWCGSAFlAwQCAQUA
# oGowGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwG
# CisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIExwmiKEzCgD7gllNAO03u/QiaEZ
# fdHBkoQWEh7kkcgeMA0GCSqGSIb3DQEBAQUABIICAH+KqAbi5DV8I2yAveUwlesX
# 4hpfjy+TSustZ3qQXtwdet14MGG2Y9iXUJoXT7d+JXR3SASTMQYhGvforfP4XT8k
# P7zmSDBHbTWwM/4WZj7MeawZ62UhAEyDgjDcIJzz4sINHj9b/+Y2pSxmpX+k6Ce9
# bDqXuydTamMNQ3Foo7lLWmp7F3wGjLlL1zj4hFc3waIohAF6/UsZXdovvbdvVo2n
# wQI5yZFYupbLW6acswMF0KpWuuw5vJgsKoLh+gE3q9jwnIlngpyLES9SFTWxcjt3
# aMF4ShONuRvXdoNQF80MUqLucPJNbLoUHXNNqMPHjkoFlCB1BXtoOWsUO4XIg4/P
# FylTEofoVYU668g9Ay1LJQxdtYbvhknihoGtEJOGfoTvBO+EEm9r6/GCjjyE4GJq
# qd4ugppa016YhBRHHBUnAesEqgtrF0Cc4th/9rcaR4qSiPKUywjNdfjw7NekYK65
# di7FTt+bE9SoM9mgR3SMjPwZv5U0mc6W9A5HvFSXGU2Rc4yyFSI38B/BaxkCorif
# S6izbFDZhqfgXavJxUlxo6h4d+KickoWXV/OyQjZ2jf237dGl6yzyw+7NGPPbqIO
# myK9eaGjB7X2tPCAZL3y4MFZJyuxKh+vpS8wjyVu6EPLl+Ya1E8Eyp6auVIMBXVR
# c0nO2/29sawljC2kk28XoYIDJjCCAyIGCSqGSIb3DQEJBjGCAxMwggMPAgEBMH0w
# aTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQD
# EzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0MDk2IFNIQTI1
# NiAyMDI1IENBMQIQCE/cM09+RU7bww+P+ZIYNTANBglghkgBZQMEAgEFAKBpMBgG
# CSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTI2MTAyMTA4
# NDUzNVowLwYJKoZIhvcNAQkEMSIEILRkRKhPL/wjFlim7xdWNHJ00R48FC7gzvK6
# detcdbD3MA0GCSqGSIb3DQEBAQUABIICAHiBSMEZa8mHf5OXrwAqNaAj0UVQLCTE
# Q3j64j9faB3vQXm0MHocD+fXS/ojlVWJ92/TupFXzVkaOGkxcPv0Q23Xp5AjtsGZ
# WvR3Rokxp+tF9VDXmQnRbfCS+4ZWTcIYdzOS2bPM9IFnLj8I6WeECXWjt+JeIqmP
# f/dQg87geRxyiVIxR10MVhFV+7L10kxgIBwHi87ujXbSDakyIcamur5Uuoxp+ftA
# VefoI5+6n2vaBgFZPmjpaMPYBmY4weuKgDxLZ46hCKsszwcKe1lczCrdwWjvuL6m
# wX0D+wy2LEm9FuIDgejjB0ToXtgCngkxelX3jf+/ERMd2o9DRppfKh62IJ6e8k0t
# OqzW2ygC30LkptpG5q7asOdz3Vk23OwHl/PcqNh6DZx71dpYUEVRinrF3o7k/P/O
# dgdcRodhR/cgkZrTh4n4H7rFn4AESRDLoMZhjGVaFo+fw9MXFVX6WA7yTeKaESZx
# 5gVMsH93bYdCJVZUULMhJ/CWN5diO6eY3dVjL09IYQGU06sa6JxwVpOv5JvvrJFh
# TmVCdypAP4odVkKBdPx8yhZdQuT4BttXdVF5Ibn51d+asTwcmGUJ9KvXjNM3X0Rn
# DWagMqycgqaHsNqlF1Y45I7pDyarjFWNJvOsBzsAFvkZ+j6lppqX/Fgtl5WAGnQx
# mSkBFrSKss7o
# SIG # End signature block