modules/AzStack.Insights/AzStack.Insights.Helper.psm1

<################################################################
# #
# Copyright (C) Microsoft Corporation. All rights reserved. #
# #
################################################################>


function Test-isFailoverCluster {
    [CmdletBinding()]
    param()
    # Detecting Failover Cluster based on the presence of the Get-Cluster cmdlet
    return ((Get-Command "Get-Cluster" -ErrorAction Ignore) -and (Get-Cluster -ErrorAction Ignore))
}

function Get-FirewallEndpoints {
    <#
    .SYNOPSIS
        Retrieves the required firewall endpoints for a specified Azure region.
 
    .DESCRIPTION
        This function returns a collection of firewall endpoints that need to be opened
        for Azure Local operations in the specified region. This comes from https://learn.microsoft.com/en-us/azure/azure-local/concepts/system-requirements-23h2?view=azloc-2601&tabs=azure-public
 
    .OUTPUTS
        Collection of PSObjects representing firewall endpoints.
 
    .EXAMPLE
        $endpoints = Get-FirewallEndpoints -Region 'eastus'
        Retrieves all firewall endpoints for the 'eastus' region and stores them in the $endpoints variable.
    #>


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

    $endpointRootDir = Get-Item -Path "$PSScriptRoot\config\firewall_endpoints"
    try {
        switch ($Region) {
            'australiaeast' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "AustraliaEastEndpoints.psd1")
            }
            'canadacentral' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "CanadaCentralEndpoints.psd1")
            }
            'eastus' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "EastUSEndpoints.psd1")
            }
            'centralindia' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "IndiaCentralEndpoints.psd1")
            }
            'japaneast' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "JapanEastEndpoints.psd1")
            }
            'southcentralus' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "SouthCentralUSEndpoints.psd1")
            }
            'southeastasia' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "SoutheastAsiaEndpoints.psd1")
            }
            'westeurope' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "WestEuropeEndpoints.psd1")
            }
            'usgovvirginia' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "USGovVirginiaEndpoints.psd1")
            }
            default {
                return $null
            }
        }

        return $configData.Endpoints
    }
    catch {
        throw "Failed to load firewall endpoints for region '$Region'. $_"
    }
}

function Get-MocConfigCached {
    <#
    .SYNOPSIS
        Returns MOC configuration, using the global cache when available.
    .DESCRIPTION
        Reads from $Global:MocArbCachedMocConfig if populated (set by the
        MocArb component). On cache miss, calls Get-MocConfig
        directly with up to 5 retry attempts and exponential backoff to
        handle transient MOC file lock errors on the shared catalogs file.
    .PARAMETER CallerName
        Label used in diagnostic messages.
    .OUTPUTS
        The MOC configuration object, or $null if all attempts fail.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$CallerName = 'MocArb'
    )

    if ($Global:MocArbCachedMocConfig) {
        return $Global:MocArbCachedMocConfig
    }

    Write-Verbose "[$env:COMPUTERNAME][$CallerName] Cache miss, calling Get-MocConfig directly"
    $mocConfig = $null
    for ($attempt = 1; $attempt -le 5; $attempt++) {
        try { $mocConfig = Get-MocConfig -ErrorAction SilentlyContinue 2>$null } catch { }
        if ($mocConfig) {
            Write-Verbose "[$env:COMPUTERNAME][$CallerName] Get-MocConfig succeeded on attempt $attempt"
            return $mocConfig
        }
        if ($attempt -lt 5) {
            $backoff = 2 * $attempt
            Write-Warning "[$env:COMPUTERNAME][$CallerName] Get-MocConfig attempt $attempt failed, retrying in ${backoff}s"
            Start-Sleep -Seconds $backoff
        }
    }

    Write-Warning "[$env:COMPUTERNAME][$CallerName] Get-MocConfig failed after 5 attempts"
    return $null
}

function Get-ArbControlPlaneVMs {
    <#
    .SYNOPSIS
        Returns local Hyper-V VM objects for the ARB control-plane VM.
    .DESCRIPTION
        Calls Get-VM -Name '*control-plan*' on the local node, then disambiguates
        when multiple VMs match (e.g., AKS workloads also create control-plane VMs).
        Uses two methods to identify the real ARB VM:
        1. Cluster group: ARB VM belongs to the '<clusterName>-arcbridge' cluster group.
           The cluster group name reliably contains 'arcbridge' even though the VM name does not.
        2. MOC kubeconfig IP: Matches the API server IP from the ARB kubeconfig to VM network adapters.
           Kubeconfigs under paths containing 'arcbridge' are preferred over AKS kubeconfigs.
        Falls back to returning all matches if disambiguation fails.
    .OUTPUTS
        Array of Hyper-V VM objects, or empty array if none found on this node.
    #>

    [CmdletBinding()]
    param ()

    $vms = @(Get-VM -Name '*control-plan*' -ErrorAction Ignore)

    if ($vms.Count -le 1) {
        return $vms
    }

    # Multiple VMs matched — AKS workloads can create additional control-plane VMs.
    Write-Verbose "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Found $($vms.Count) VMs matching '*control-plan*', disambiguating ARB VM..."

    # Method 1: Cross-reference with the ARB failover cluster group (<clusterName>-arcbridge)
    # The cluster group name reliably contains 'arcbridge'; the VM name itself does not.
    try {
        $clusterName = Get-AzsSupportEceManagementClusterName -ErrorAction Stop
        $arbGroupName = "$clusterName-arcbridge"
        $arbGroup = Get-ClusterGroup -Name $arbGroupName -ErrorAction Ignore
        if ($arbGroup) {
            $arbGroupResources = @(Get-ClusterResource -InputObject $arbGroup -ErrorAction Ignore |
                Where-Object { $_.ResourceType -eq 'Virtual Machine' })
            if ($arbGroupResources.Count -gt 0) {
                $arbResourceNames = @($arbGroupResources | ForEach-Object { $_.Name })
                # Cluster resource names for VMs are typically 'Virtual Machine <VMName>'
                $matched = @($vms | Where-Object {
                    $vmName = $_.Name
                    $arbResourceNames -contains $vmName -or $arbResourceNames -contains "Virtual Machine $vmName"
                })
                if ($matched.Count -gt 0) {
                    Write-Verbose "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Narrowed to $($matched.Count) VM(s) by cluster group '$arbGroupName'"
                    return $matched
                }
            }
        }
    }
    catch {
        Write-Warning "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Cluster group cross-reference skipped: $_"
    }

    # Method 2: Match VM network adapter IPs against the ARB kubeconfig API server IP
    # Prefer kubeconfigs whose path contains 'arcbridge' to avoid picking up AKS kubeconfigs.
    $arbIp = $null
    $mocCfg = Get-MocConfigCached -CallerName 'Get-ArbControlPlaneVMs'
    if ($mocCfg -and $mocCfg.WorkingDir -and (Test-Path $mocCfg.WorkingDir)) {
        $kubeconfigs = @(Get-ChildItem -Path $mocCfg.WorkingDir -Filter 'kubeconfig' -Recurse -Depth 10 -ErrorAction Ignore)
        # Prefer kubeconfigs under arcbridge paths
        $arcbridgeKubeconfigs = @($kubeconfigs | Where-Object { $_.FullName -like '*arcbridge*' })
        $orderedKubeconfigs = if ($arcbridgeKubeconfigs.Count -gt 0) { $arcbridgeKubeconfigs } else { $kubeconfigs }
        foreach ($kc in $orderedKubeconfigs) {
            $content = Get-Content -Path $kc.FullName -Raw -ErrorAction Ignore
            if ($content -match 'server:\s*https?://([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):6443') {
                $arbIp = $matches[1]
                break
            }
        }
    }

    if ($arbIp) {
        $matched = @($vms | Where-Object {
            $vmIps = @(
                $_ | Get-VMNetworkAdapter -ErrorAction Ignore |
                ForEach-Object { $_.IPAddresses } |
                Where-Object { $_ -match '^\d+\.' }
            )
            $vmIps -contains $arbIp
        })
        if ($matched.Count -gt 0) {
            Write-Verbose "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Narrowed to $($matched.Count) VM(s) by MOC kubeconfig IP ($arbIp)"
            return $matched
        }
    }

    Write-Warning "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Could not disambiguate $($vms.Count) control-plane VMs. Returning all matches."
    return $vms
}

function Get-ArbControlPlaneTargets {
    <#
    .SYNOPSIS
        Discovers ARB control-plane VM IPv4 addresses.
    .DESCRIPTION
        Uses two methods to find ARB control-plane VM IPs:
        1. Local Hyper-V (Get-VM) for VMs matching '*control-plan*' on this node.
        2. Parses kubeconfig files in the MOC working directory and ClusterStorage
           for the Kubernetes API server IP (port 6443).
        Method 2 uses Get-MocConfigCached to locate the MOC working directory.
    .PARAMETER CallerName
        Label used in diagnostic messages.
    .OUTPUTS
        Array of unique IPv4 address strings, or empty array if none found.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$CallerName = 'MocArb'
    )

    $arbTargets = @()

    # Method 1: Local Get-VM for ARB control-plane VMs on this node
    $arbVMs = Get-ArbControlPlaneVMs
    if ($arbVMs) {
        $arbTargets = @(
            $arbVMs |
            Get-VMNetworkAdapter -ErrorAction Ignore |
            ForEach-Object { $_.IPAddresses } |
            Where-Object { $_ } |
            Where-Object { $_ -match '^\d+\.' }
        )
    }

    # Method 2: Parse kubeconfig files for API server IP
    if ($arbTargets.Count -eq 0) {
        $searchPaths = @()
        $mocCfg = Get-MocConfigCached -CallerName $CallerName
        if ($mocCfg.WorkingDir) { $searchPaths += $mocCfg.WorkingDir }
        $searchPaths += 'C:\ClusterStorage'

        foreach ($searchPath in $searchPaths) {
            if (-not (Test-Path $searchPath)) { continue }
            $kubeconfigs = @(Get-ChildItem -Path $searchPath -Filter 'kubeconfig' -Recurse -Depth 10 -ErrorAction Ignore)

            # On clusters with AKS workloads, multiple kubeconfigs exist (ARB + AKS target clusters).
            # Prefer kubeconfigs whose path contains 'arcbridge' to avoid testing AKS endpoints.
            $arcbridgeKubeconfigs = @($kubeconfigs | Where-Object { $_.FullName -like '*arcbridge*' })
            $orderedKubeconfigs = if ($arcbridgeKubeconfigs.Count -gt 0) { $arcbridgeKubeconfigs } else { $kubeconfigs }

            foreach ($kc in $orderedKubeconfigs) {
                $content = Get-Content -Path $kc.FullName -Raw -ErrorAction Ignore
                if ($content -match 'server:\s*https?://([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):6443') {
                    $arbTargets += $matches[1]
                }
            }
            $arbTargets = @($arbTargets | Select-Object -Unique)
            if ($arbTargets.Count -gt 0) { break }
        }
    }

    return $arbTargets
}

function Get-ArbKubeconfigs {
    <#
    .SYNOPSIS
        Finds ARB kubeconfig files on the local node.
    .DESCRIPTION
        Searches the MOC working directory and ClusterStorage for kubeconfig files
        that contain a Kubernetes API server endpoint (port 6443).
    .PARAMETER CallerName
        Label used in diagnostic messages.
    .OUTPUTS
        Array of FileInfo objects for matching kubeconfig files.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$CallerName = 'MocArb'
    )

    $searchPaths = @()
    $mocCfg = Get-MocConfigCached -CallerName $CallerName
    if ($mocCfg.WorkingDir) { $searchPaths += $mocCfg.WorkingDir }
    $searchPaths += 'C:\ClusterStorage'

    $found = @()
    foreach ($searchPath in $searchPaths) {
        if (-not (Test-Path $searchPath)) { continue }
        $kubeconfigs = @(Get-ChildItem -Path $searchPath -Filter 'kubeconfig' -Recurse -Depth 10 -ErrorAction Ignore)
        foreach ($kc in $kubeconfigs) {
            $content = Get-Content -Path $kc.FullName -Raw -ErrorAction Ignore
            if ($content -match 'server:\s*https?://[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:6443') {
                $found += $kc
            }
        }
        if ($found.Count -gt 0) { break }
    }

    return $found
}
# SIG # Begin signature block
# MIInSQYJKoZIhvcNAQcCoIInOjCCJzYCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBzuxeAg44Hx7Th
# lOt7GcfhJyC83BomupOLe68oqOXHn6CCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1
# OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8
# o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg
# 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4
# Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R
# X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk
# ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw
# HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg
# UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0
# JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh
# MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9
# s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H
# VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3
# w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n
# 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs
# A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo
# Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb
# SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6
# 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z
# V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v
# 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs
# /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA
# AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow
# VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo
# MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh
# emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h
# KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd
# M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp
# yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t
# Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5
# REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs
# 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK
# Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5
# pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW
# eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ
# 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC
# NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB
# gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI
# MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4
# NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh
# ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q
# hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU
# nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb
# H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z
# uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u
# vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW
# 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV
# DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10
# 1cY2L4A7GTQG1h32HHAvfQESWP0xghnlMIIZ4QIBATBuMFcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv
# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w
# DQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK
# KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIGtFhmXz
# CiXY6wcqvaa1O7HoA6t5VHqZowb3hY2nowHDMEIGCisGAQQBgjcCAQwxNDAyoBSA
# EgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20w
# DQYJKoZIhvcNAQEBBQAEggEASuBSjyZv8Y8Ke8TCunBNdNW7+vgeD5JfhWZbFMkB
# zJLO/CuG9oeemRlGqNqAMr5WNof1HpYIwV8NEXEtYCvpaWeXUAbcbgbjYUkhMFvi
# z64PMBrcC75k30lmO3vdLrJecusmG/lLVH3aXt9jfKLjICGdSER3ZSASTNzS2bwX
# xaw9scMSp4KFJfNUwF2RUT27mu9VbQ+Lf+9aWXNKrif6QIY+3uHEyLOoBstxQhgL
# NNbj7hEqHTIVyKpPvigLGN8d5XCh8LKGEsXN1TeCUIax73aAJovaEAvzJix/3hsa
# 7M8vDt156RgJ4n1I/ej0tgngajzlnrro0OP9mGuisRWFJ6GCF5cwgheTBgorBgEE
# AYI3AwMBMYIXgzCCF38GCSqGSIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZIAWUD
# BAIBBQAwggFSBgsqhkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoD
# ATAxMA0GCWCGSAFlAwQCAQUABCCAqnnrkas7oKB96vtdP1PtJoaFZ5KD72NuKgFJ
# ausUHAIGaedenBaKGBMyMDI2MDUwNTE2MTMwOS4zMTFaMASAAgH0oIHRpIHOMIHL
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxN
# aWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRT
# UyBFU046QTQwMC0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFNlcnZpY2WgghHtMIIHIDCCBQigAwIBAgITMwAAAijwpYfX88geQAABAAAC
# KDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAe
# Fw0yNjAyMTkxOTQwMDZaFw0yNzA1MTcxOTQwMDZaMIHLMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj
# YSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTQwMC0wNUUw
# LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCujvbk/sqcCSReZaJfCuf1NwRc
# c7XknhE6wkLofkNj1mxEAg35qy2xcFjgjartVvA09W8QHcpyMqVSXOTxNHJsmk0q
# P2CDLvUAulWg7aS5oBORpEX1oz3n0R2nPqeH0IHK1zJxjxaHW21AbuZ0Z+wM3WYN
# zkBlcHmVe03ZG7rlk28h72r5P5ME8FGpFmYW5Hl7psKbgLEfrYAitpttsb+sZsBU
# I+hMKl4uLJYotKyZv1ewOIinBfRU8QosivjofaBezUf9NdV+iGrWh321WnSsK3A/
# Jl6GLtbSWXcJWULgbxuqnobPK+YlB3174TMWTgX4YWjG7o0Otz/pjHNCKBbB788d
# ynhLdGY6B08E9+4SGrRpsty4iJHOydHCA5M4i5yYRwsdut+gmvxIpT8yNXJcjJCg
# 0vO8mv/nFY9Wytv2qmCtCFFivGUWqU20/sUeRooQZGiQOJQn095Cj3isIsvRP8KU
# 7hN/EDI8HVsb/NPzMFLvRznrRnj0TOnDiOTUcnYwmk+XfoS1owskcCCCwHnbC00D
# 58z83y7K5ZJB745hcn4CE2nR3e6RGsr42y5qtt6Mdz/s7MTnDS2UmVHWX1X/HZe3
# UlX8gj/t63L50xIPqkRCBEdM1ADNUaSfo9OQiKb/bj1diZCGTfEDUBBLop1mhkwI
# F82faplV2busZ+U4kQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFKrJpYz48tzouvVk
# BVthASFpQ93DMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1Ud
# HwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3Js
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggr
# BgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIw
# MTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgw
# DgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQCQ6NfLmrRahgVtgWg3
# 83GaS07fHyod6bhcUONt2tet+6BaNuH0r7ABkVHheOpxBdrUrOEYVEaIii9dK3cu
# ZLNmp1iUAx/VbmOZYl7xz+tNrjCWqrg1jQmq0oRB8iE4QJpwNhGP67oY5huYIU0D
# 4lhDoahqfgKJn/0Bk+9UKDPw5XlUYmreFmJlj9YQzcPPep8MxBXxh/Y5I7vQeRaW
# 5SjtiLQOLRk3ggvraDs5Sf49MJV6/BwxXC2rvUfEFX6SUDooqKIE9NgVIRq0RZu7
# Ot0i0Is+HvPP0hB6KwOxMg1SWKOfTtFpWpdo8MJvgKCHkPpXEzgprP+pyIHuO7gV
# RlSTsbYBFLh2yId/itM4uYL0R+2SSBBTpSSRthrGuEmElI5BCHMxzMg/oqHSPwZA
# IAkM2C4xxi0St7qMuA+m+ZzFYkfoF41QoSJn+HjqhqWYQ0m/SO9/KnJRJJUwMd5T
# iMnjZ+E/DJiUry5udiWyQpvfj2hQFI0djhahoAXDazeEciLF2uEnTur9UfjcwOun
# /oMY+ULftnOi2jKLMrreV097akzz/JxpnDgYJU/tgU7fQflg7IqiL9+0276+joQH
# o21mVeY5YD8Kh/kUaY6Jm/OTM88G7evTz/qnRumxovTjMStvpbAHNRhmSTdIPTV3
# 2CyuxDKS/V5a5iwA+f9ViBo+wjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkA
# AAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVow
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX
# 9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1q
# UoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8d
# q6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byN
# pOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2k
# rnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4d
# Pf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgS
# Uei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8
# QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6Cm
# gyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzF
# ER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQID
# AQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQU
# KqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1
# GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0
# bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMA
# QTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbL
# j+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1p
# Y3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz
# LmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwU
# tj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN
# 3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU
# 5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5
# KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGy
# qVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB6
# 2FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltE
# AY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFp
# AUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcd
# FYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRb
# atGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQd
# VTNYs6FwZvKhggNQMIICOAIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg
# T3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkE0MDAtMDVFMC1E
# OTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEw
# BwYFKw4DAhoDFQB1rbmFkzS7qAK1Oav08AUnhbNIUqCBgzCBgKR+MHwxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7aRP9jAiGA8y
# MDI2MDUwNTExMTYwNloYDzIwMjYwNTA2MTExNjA2WjB3MD0GCisGAQQBhFkKBAEx
# LzAtMAoCBQDtpE/2AgEAMAoCAQACAgHLAgH/MAcCAQACAhULMAoCBQDtpaF2AgEA
# MDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAI
# AgEAAgMBhqAwDQYJKoZIhvcNAQELBQADggEBAB+7nj/krNRMEyGqh4EKyDu1TUuE
# 1IZwW5TLG4NiK/U4shiR0WZbDWW+VEjRLSBwnqXmnVQ7WGQai5n4hKqn8JblCQo9
# LijYfvy7OCEtCGCWIdgwjs0gPo39Eo940FjNyVE1+8KjvuEbtdAz4rvzHqWCbywk
# CqNYLr2F1xCED7620V2mA/oIJl3TSnn331VuEioCOMFX9ayCzDMFa34Im8Zd0kuo
# WXuE7o02meR4DUQKrN2g+q+IrhwPQsD4sD6D8VQp++rmK9U2hVztIP4Wk0GnnuMd
# UhJImF14pzXHmRWpkU2OqSmCXKeI2UGW5KPzTOwYoyB2vqahE8Sr0rWobk0xggQN
# MIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ
# MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u
# MSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAijw
# pYfX88geQAABAAACKDANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0G
# CyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCCV/A9y9GiPllzyCVvk8TVSf2FB
# DVv5eXTyU7X/QBpgETCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIFWxikZR
# YGNf4oEVZK1eT45H+3GQ3/qxV75VwuBt+iLXMIGYMIGApH4wfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAIo8KWH1/PIHkAAAQAAAigwIgQgYhncZN0h
# T0WC3hhHRY1/TINR0P0ezFLwRE+6rjJehpgwDQYJKoZIhvcNAQELBQAEggIAeiiB
# 2WshkW5+L/SHJba3AIoAD6gK70rMlhNel0Na+ETtNe+Jn13OSaV87LRPDh9If7VJ
# 4cuiC63Et1WaC6Ao3V5Wim20PslhQ3wSKCCim+Sjw63Y7o/JH6Uu3nXDmlCbOCBK
# PTWv3zsXupLU5DGArFB2zm9up/S0QNQZHRy1equsUidbwnhXWH5siX51yaajLktJ
# vS1F4gZ8+NEHN4QiQ2Ow/siuUkXlwPhuCTyZNaVgY25fklkMuYQskIAsZ4McjHQE
# 1mFtvmXYmcTna4MWKpHv1sAdvEEjY7Ct1EneRBuDfImstYwSHFFrZfAlPepkmv7Q
# KrJPSdWIKYbsAeraRvaMRx9Lc6qs+67um9xFza62teQNneQc7ygROd7gBiDMzDv7
# xJh/uciTYLEKPjKyVK16DebxzN09MdoVQzAzcFdmrDi6iSotoFN8xeuIdVKcqGOy
# S6/nhkPbIfd6rizxOtdRcyLx0TppQfTcAor+PKuNUAbF7mWBYuoaKwmSWLU1t0ci
# zNkVlTkco3leq4WLhZCGKea7vcaYDj5plE4lZL+0Noi3e5fi+W49KRW9I6zP2C6+
# XvTInCLB+wYfxpHwxVgKNwnIK0fa6d3n/0AABhX1vK5zlOTdBTe8AoOUr75d9Wkg
# wvXt33os9JCul3KP9F1DsWF+woXF3A5vz6HTceg=
# SIG # End signature block