modules/AzStack.Insights/analyzers/Windows.Cluster.GhostCsvMountPoint.ps1

<#
.SYNOPSIS
    Detects ghost Cluster Shared Volume mount points (C:\ClusterStorage.00X).
.DESCRIPTION
    Failover Clustering renames the CSV root out of the way to a numbered name
    such as C:\ClusterStorage.000 when it cannot initialize C:\ClusterStorage,
    typically because another process (often antivirus or a filter driver) holds
    an open handle on it. The leftover numbered directory is a "ghost root".
 
    Ghost roots are usually harmless, but become high severity when a VM, a
    cluster resource, or Azure Local platform content still references one,
    because solution updates and Arc Resource Bridge operations then fail with
    errors that do not mention the path.
 
    This analyzer enumerates ghost roots on the local node and classifies each
    one by what still references it, so the rule can distinguish "safe to clean
    up" from "repoint a workload VM" from "engage support".
#>


[CmdletBinding()]
param ()

# Load the parent module when invoked directly (e.g. Pester). No-op when
# called via Invoke-SupportInsight because the module is already loaded.
if (-not (Get-Command -Name 'Initialize-InsightAnalyzer' -ErrorAction SilentlyContinue)) {
    Import-Module -Name $PSScriptRoot\..\AzStack.Insights.psm1 -Force -DisableNameChecking
}

Import-LocalizedData -BindingVariable 'localizedData' -BaseDirectory "$PSScriptRoot\locale" -UICulture $PSUICulture
$insightAnalyzer = Initialize-InsightAnalyzer -Id $localizedData.Id -Properties $localizedData.Insight

<# START ANALYZER LOGIC #>

try {
    # Bounded so a corrupt or circular differencing chain cannot spin forever.
    $script:GhostCsvMaxChainDepth = 50

    # The ghost path pattern and root discovery live in AzStack.Common so this
    # analyzer and its remediation share ONE source of truth. That matters for
    # safety, not just tidiness: if the remediation's delete gate ever drifted
    # from this classification, it could remove a root that is still referenced.
    $ghostPathPattern = Get-AzsGhostCsvPathPattern
    $ghostRoots = @(Get-AzsGhostCsvRoot)

    # Collect every reference to a ghost path once, then attribute per root.
    $references = New-Object -TypeName System.Collections.Generic.List[object]
    # Orphaned VMs (configured with a disk that no longer exists) are surfaced as
    # informational notes, never as blockers.
    $orphanNotes = New-Object -TypeName System.Collections.Generic.List[string]

    # Only gather reference evidence when there is something to classify. Skipping
    # it on a clean host also keeps the now-terminating cluster and Hyper-V queries
    # below from failing a host that legitimately has neither role and no ghost
    # roots either, which would turn a correct clean result into UNKNOWN.
    if ($ghostRoots.Count -gt 0) {

        # Every query below is safety critical and therefore TERMINATING. A failed
        # query run with -ErrorAction SilentlyContinue returns nothing, so the loop
        # iterates zero times and the root is reported as Unreferenced, telling the
        # operator that "nothing on this cluster references them" when the evidence
        # was never gathered. A check that cannot run must surface as UNKNOWN via
        # the outer catch instead, which is also what the paired remediation does
        # when its equivalent gate cannot run.
        foreach ($vm in (Get-VM -ErrorAction Stop)) {
            foreach ($disk in ($vm | Get-VMHardDiskDrive -ErrorAction Stop)) {
                if ($disk.Path -match $ghostPathPattern) {
                    $references.Add([PSCustomObject]@{
                            Kind = 'VMHardDisk'; Object = $vm.Name; Property = 'Path'; Value = $disk.Path })
                }

                # Walk the parent chain. A checkpoint or differencing PARENT can still
                # live on a ghost root while the attached child sits on a healthy CSV,
                # so testing only $disk.Path reports that root as unreferenced and the
                # remediation then deletes the parent, breaking the chain and leaving
                # the child disk unusable. This mirrors the walk the TSG documents.
                #
                # An unreadable link fails CLOSED at every depth, including the attached
                # leaf: if the chain cannot be read, its cleanliness cannot be proven, so
                # the analyzer must not report the root as unreferenced.
                $parentPath = $disk.Path
                $depth = 0
                while ($parentPath) {
                    # Bound checked BEFORE each hop. Checking after the loop reported a
                    # chain of exactly the limit as too deep even though it had been
                    # fully walked and $parentPath was already null, turning a valid
                    # bounded checkpoint chain into a false refusal.
                    if ($depth -ge $script:GhostCsvMaxChainDepth) {
                        throw "Parent chain of $($vm.Name) exceeded $($script:GhostCsvMaxChainDepth) links and was not fully walked, so the ghost roots cannot be classified safely."
                    }

                    # A disk file that does not EXIST ends the chain rather than failing
                    # it. An orphaned VM whose configuration still points at a deleted
                    # VHDX is an ordinary state in the field, seen on a real cluster with
                    # a stopped AKS node pool VM, and it is provably harmless here: a
                    # file that is not there cannot hold a parent link to a ghost root,
                    # and the attached path itself is already matched directly above. If
                    # this aborted the analyzer, one orphaned VM would report UNKNOWN for
                    # every unrelated ghost root on the node.
                    if (-not (Test-Path -LiteralPath $parentPath -ErrorAction Stop)) {
                        # The file genuinely does not exist: an orphaned VM. Test-Path is
                        # made TERMINATING so an access or provider error surfaces as
                        # UNKNOWN via the outer catch instead of being read as "missing".
                        # Surface the orphan as an informational note (never a blocker),
                        # then end the chain.
                        $note = "VM '$($vm.Name)' is configured with a disk that no longer exists ($parentPath)."
                        if (-not $orphanNotes.Contains($note)) { $orphanNotes.Add($note) }
                        Write-AzsSupportLog -Level 'Informational' -Message "Also seen while checking ghost CSV mount points: $note"
                        break
                    }

                    # An existing file that cannot be READ is different: coverage is
                    # genuinely unknown, so that still fails closed.
                    $vhd = Get-VHD -Path $parentPath -ErrorAction SilentlyContinue
                    if (-not $vhd) {
                        throw "Get-VHD could not read '$parentPath' (parent chain of $($vm.Name), depth $depth); parent-chain coverage is incomplete, so the ghost roots cannot be classified safely."
                    }
                    $parentPath = $vhd.ParentPath
                    if ($parentPath -and ($parentPath -match $ghostPathPattern)) {
                        $references.Add([PSCustomObject]@{
                                Kind = 'VMDiskParent'; Object = $vm.Name; Property = 'ParentPath'; Value = $parentPath })
                    }
                    $depth++
                }
            }
            foreach ($property in 'ConfigurationLocation', 'SnapshotFileLocation', 'SmartPagingFilePath') {
                $value = $vm.$property
                if ($value -and ($value -match $ghostPathPattern)) {
                    $references.Add([PSCustomObject]@{
                            Kind = 'VMConfig'; Object = $vm.Name; Property = $property; Value = $value })
                }
            }
        }

        foreach ($openFile in (Get-SmbOpenFile -ErrorAction Stop)) {
            if ($openFile.Path -match $ghostPathPattern) {
                $references.Add([PSCustomObject]@{
                        Kind     = 'SmbOpenFile'
                        Object   = "$($openFile.ClientUserName)@$($openFile.ClientComputerName)"
                        Property = 'Path'; Value = $openFile.Path
                    })
            }
        }

        # Cluster resources are cluster wide. This is what catches clustered VMs owned
        # by another node and platform managed roles that Get-VM cannot see locally.
        #
        # Reading each resource's parameters is terminating for the same reason. The
        # previous tolerance here rested on a false premise: a resource type that
        # exposes no parameters does NOT throw, it returns an empty collection.
        # Measured read-only across three live clusters, 195 cluster resources over 14
        # resource types: zero throws, and the only two parameterless types returned
        # empty without raising. So swallowing here never protected that case and only
        # hid real failures.
        foreach ($resource in (Get-ClusterResource -ErrorAction Stop)) {
            foreach ($parameter in (Get-ClusterParameter -InputObject $resource -ErrorAction Stop)) {
                if (($parameter.Value -is [string]) -and ($parameter.Value -match $ghostPathPattern)) {
                    $references.Add([PSCustomObject]@{
                            Kind = 'ClusterResource'; Object = $resource.Name
                            Property = $parameter.Name; Value = $parameter.Value
                        })
                }
            }
        }
    }

    # An ACTIVE CSV mounted under a numbered root means the numbered root is the
    # live namespace, not a ghost. That is a support case, never a cleanup.
    $activeCsvUnderNumberedRoot = @(
        $(if ($ghostRoots.Count -gt 0) { Get-ClusterSharedVolume -ErrorAction Stop }) |
            Where-Object { $_.SharedVolumeInfo.FriendlyVolumeName -match $ghostPathPattern }
    )

    $ghostDetails = foreach ($root in $ghostRoots) {
        $rootPath = $root.FullName
        $escapedRoot = [regex]::Escape($rootPath)
        # Match this root only when it is the whole value or is followed by a path
        # separator, so root ".000" cannot spuriously match a reference that is
        # actually under ".0001". Over-matching here fails safe (it would keep a
        # removable root), but scoping it correctly keeps the classification honest.
        $rootBoundaryPattern = $escapedRoot + '(?:[\\/]|$)'

        $rootReferences = @($references | Where-Object { $_.Value -match $rootBoundaryPattern })

        # Terminating: if this listing fails, the root would otherwise report
        # ItemCount 0, IsEmpty true and ReparsePointCount 0, presenting a root that
        # may still hold a live mount point as an empty shell that is safe to delete.
        $children = @(Get-ChildItem -LiteralPath $rootPath -Force -ErrorAction Stop)

        # The ReparsePoint attribute is the authoritative signal that this root
        # still redirects to a live volume. LinkType and Target do not reliably
        # populate for volume mount points, so they must not be used here.
        #
        # Scanned RECURSIVELY, because the delete is recursive: a reparse point
        # nested below the first level is just as much a redirect to live storage
        # as one at the top, and checking only immediate children left that blind.
        $descendants = @(
            Get-ChildItem -LiteralPath $rootPath -Force -Recurse -ErrorAction Stop |
                Sort-Object -Property FullName -Unique
        )

        $reparsePoints = @(
            $descendants | Where-Object { $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint }
        )

        # Live platform WORKING DATA: Arc Resource Bridge / MOC directories, the
        # image store, or any virtual hard disk. This is a support case on its own,
        # with or without a reference, because deleting it destroys live state.
        $hardPlatformContent = @(
            $descendants | Where-Object { $_.Name -match (Get-AzsGhostCsvPlatformContentPattern) }
        )

        # Anchored to whole PATH SEGMENTS. Matching bare substrings escalated a
        # workload path such as ...\UserStorage_1\WorkingDirectory\app.vhdx, and even
        # ...\MyImageStore\, to PlatformReferenced, which routes the customer to
        # support instead of the self-service Move-VMStorage path.
        #
        # Infrastructure_<n> and MocArb are distinctive platform-owned names, so a
        # segment match on either is sufficient. Generic names like Orchestration,
        # ImageStore and WorkingDirectory are NOT listed separately on purpose: when
        # they are genuinely platform data they sit under one of those two roots and
        # are already covered, and when they are not, they are ordinary workload
        # folders that must stay on the self-service path.
        $isPlatformPath = [bool](
            $rootReferences | Where-Object { $_.Value -match '(?:^|[\\/])(?:Infrastructure_\d+|MocArb)(?:[\\/]|$)' }
        )

        # An Infrastructure_<n> directory on its own is usually an orchestrator
        # breadcrumb, a few hundred bytes with nothing pointing at it, so it only
        # escalates when the root is ALSO referenced. Live platform working data is
        # handled by $hardPlatformContent above and never needs a reference.
        $hasPlatformContent = [bool](
            $children | Where-Object { $_.Name -match '^Infrastructure_\d+$' }
        )

        # Scope the active-CSV check to THIS root. A live CSV mounted under a
        # DIFFERENT numbered root says nothing about this one, and treating it as
        # platform-referenced would tell the operator to open a support case for a
        # ghost that is genuinely safe to remove. The root that actually hosts the
        # live CSV still classifies as PlatformReferenced on its own pass, so no
        # signal is lost.
        $activeCsvUnderThisRoot = @(
            $activeCsvUnderNumberedRoot | Where-Object {
                $_.SharedVolumeInfo.FriendlyVolumeName -match $rootBoundaryPattern
            }
        )

        if ($reparsePoints.Count -gt 0 -or $activeCsvUnderThisRoot.Count -gt 0 -or
            $hardPlatformContent.Count -gt 0 -or
            $isPlatformPath -or ($hasPlatformContent -and $rootReferences.Count -gt 0)) {
            $classification = 'PlatformReferenced'
        }
        elseif ($rootReferences.Count -gt 0) {
            $classification = 'WorkloadReferenced'
        }
        else {
            $classification = 'Unreferenced'
        }

        [PSCustomObject]@{
            GhostRoot         = $rootPath
            Classification    = $classification
            CreationTime      = $root.CreationTime
            ReferenceCount    = $rootReferences.Count
            References        = $rootReferences
            ReparsePointCount = $reparsePoints.Count
            ItemCount         = $children.Count
            IsEmpty           = ($children.Count -eq 0)
        }
    }

    $ghostDetails = @($ghostDetails)

    # A referenced root is individually actionable, so it gets its own rule.
    #
    # Unreferenced roots are different. They are cosmetic, and a real cluster
    # accumulates roughly one per solution update, so a healthy production node
    # can carry several at once. One rule per root would turn a benign condition
    # into a wall of warnings and train operators to ignore the check, so they
    # are summarised into a SINGLE rule. No detail is lost: every root still
    # appears as its own row in the property report below.
    $unreferencedRoots = @($ghostDetails | Where-Object { $_.Classification -eq 'Unreferenced' })
    $referencedRoots = @($ghostDetails | Where-Object { $_.Classification -ne 'Unreferenced' })

    foreach ($detail in $referencedRoots) {
        $insightAnalyzer.Rules += & "$PSScriptRoot\..\rules\Windows.Cluster.CSV.GhostMountPoint.ps1" -Object $detail @PSBoundParameters
    }

    if ($unreferencedRoots.Count -gt 0) {
        $aggregate = [PSCustomObject]@{
            Classification = 'Unreferenced'
            GhostRoot      = ($unreferencedRoots.GhostRoot -join ', ')
            GhostRoots     = @($unreferencedRoots.GhostRoot)
            GhostRootCount = $unreferencedRoots.Count
            ItemCount      = @($unreferencedRoots | Measure-Object -Property ItemCount -Sum).Sum
            ReferenceCount = 0
            Details        = $unreferencedRoots
        }

        $insightAnalyzer.Rules += & "$PSScriptRoot\..\rules\Windows.Cluster.CSV.GhostMountPoint.ps1" -Object $aggregate @PSBoundParameters
    }

    # Carry any orphaned-VM notes onto the rule the operator is already reading for this
    # node, so the finding is visible in the report rather than only in the log. It is
    # informational and does not change any rule's status.
    if ($orphanNotes.Count -gt 0 -and $insightAnalyzer.Rules.Count -gt 0) {
        $orphanText = 'Also seen while checking these ghost roots: ' + ($orphanNotes -join ' ')
        $insightAnalyzer.Rules[0].ErrorMessage = "$($insightAnalyzer.Rules[0].ErrorMessage) $orphanText".Trim()
    }

    $reportObject = Initialize-InsightAnalyzerPropertyReport -Name 'Ghost CSV Mount Points'
    foreach ($detail in $ghostDetails) {
        $reportObject.Data += [PSCustomObject]@{
            GhostRoot      = $detail.GhostRoot
            Classification = $detail.Classification
            CreationTime   = $detail.CreationTime
            ReferenceCount = $detail.ReferenceCount
            ItemCount      = $detail.ItemCount
            ReparsePoints  = $detail.ReparsePointCount
            References     = ($detail.References | ForEach-Object { "$($_.Kind): $($_.Object).$($_.Property) = $($_.Value)" }) -join '; '
        }
    }
    $insightAnalyzer.Properties = $reportObject
}
catch {
    $_ | Write-AzsSupportLog -Level 'Exception'
    $insightAnalyzer.ScriptStackTrace = Get-FormattedException -Exception $_
    $insightAnalyzer.Status = [InsightStatus]::UNKNOWN
}

<# END ANALYZER LOGIC #>

if ($insightAnalyzer.Rules.Status -icontains [InsightStatus]::FAILURE) {
    $insightAnalyzer.Status = [InsightStatus]::FAILURE
}
elseif ($insightAnalyzer.Rules.Status -icontains [InsightStatus]::WARNING) {
    $insightAnalyzer.Status = [InsightStatus]::WARNING
}

$insightAnalyzer.Duration = New-TimeSpan -Start $insightAnalyzer.OccurrenceTimeUTC -End $([System.DateTime]::UtcNow)

Set-InsightCache -Type 'Analyzer' -Data $insightAnalyzer
Write-InsightEvent -Insight $insightAnalyzer
return $insightAnalyzer

# SIG # Begin signature block
# MIInQQYJKoZIhvcNAQcCoIInMjCCJy4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD8f4nALcHoXODL
# PxEEZdnffo/Ui0KQ9xlQKda88aoypqCCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# 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
# 1cY2L4A7GTQG1h32HHAvfQESWP0xghndMIIZ2QIBATBuMFcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv
# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w
# DQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJ
# KoZIhvcNAQkEMSIEIBjaXEkpRY0bPgwzIQjDoRWdJWZklrGLI1Qp6ND9nxOaMEIG
# CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAbqXGev8q5aIaQ3tZ
# bvcswOp4VvwgjyyUEk/Pvg4LxzEmFiCdVxEOX09/6+0MKFsk/f+hhcxXrfC2n7sa
# qlLjEPzyb6MDTF0bp8cpSxQQGLBc92OF0LTf64EEzJG72UDqOdSlPM2xkoEsQZk9
# mtCHUz4LrNhlGs0r1Dywhc62Fr8qkbDYgZKWtGI9Rlwes8hafyNCYHr5l7pB0CPk
# 0pJ76FqJDi8YWB3VFr3c5rHWyprJEfUmBzWZS9Uc5yrRrIsMQTAKOeUm8rxjbNFB
# LZ6YITCuWQ8q+Fk73fBG62nWvL3dzS6RkU6oauo+a87+yjI1Lp0LwwNLemxQhSH+
# G0qnEKGCF60wghepBgorBgEEAYI3AwMBMYIXmTCCF5UGCSqGSIb3DQEHAqCCF4Yw
# gheCAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFF
# MIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCA2DVueGCHAmXO/
# u8NSYaX9TAYYhxPUkNNqc8if5u1QTwIGaojbL7hoGBMyMDI2MDkwMjE1MjU0MC4x
# NDhaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0MDFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEfswggcoMIIF
# EKADAgECAhMzAAACGV6y2FR19LGNAAEAAAIZMA0GCSqGSIb3DQEBCwUAMHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgyNloXDTI2MTEx
# MzE4NDgyNlowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjQwMUEtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEApqFIyUkzIyxpL3Q03WmLuy4G9YIUScznhKr+cHOT+/u7
# ParxI96gxxb1WrWuAxB8qjGLfsbImx8V3ouK1nUcf+R/nsnXas5/iTgV/Tl3QTRG
# T0DeuXBNbpHqc+wC1NiTyA76gLnirvSBEoBzlrpNQFEnuwdbPLCLpTS3KWSCu5J0
# 2b+RFWR/kcFzVxnhoE3gIaeURtrGKGBZGKLBXvqggkDENtKkvtvRT32xLvAvL/Rp
# Reu5z18ZojCs72ZSoa74Dy8YbaWsDm3OZOpJRZxZsPKCHZ6xNqgFKf0xNHj0t9v0
# Q3W+2z5gAVaasJJCvR52Sl0XJ2AOf3l0LSetXgUA5gD5IQ1RvEslTmNnSouTrGID
# 3D1njY7mBu0puiIdPK2jK/1Weef2+YR4cQpWQkeBZmXidh9AuWdlwxKQL15LJ6K2
# dw8y/t/PBhmLyt6QAf0CepWRdgZnMytVAUuWHwlZRV9JLY7aX8D55eL9+cOLpX3b
# GNOmN24UpIW8qtZaqXaesFvIOW23JNLhaaQVvObr1eu7GE/5Mn43e+/DbtdYl/bL
# P2IQ1xYEJdSbcUkDFfW3KlZEh+nBKDtaRnNRkbgIgxIbKdT38OKQwZ/aA4uSsiAg
# 6nEPiWBHGuytIo5wU75M5VdjhEqqTHfXYu8BJi6GTzvWT+9ekfMXezqCkksxaG8C
# AwEAAaOCAUkwggFFMB0GA1UdDgQWBBSAaOo5HWatNzqZn1IF1fcD6nr3ITAfBgNV
# HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU
# aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG
# CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV
# HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIH
# gDANBgkqhkiG9w0BAQsFAAOCAgEAXxzVZLLXBFfoCCCTiY7MHXdb7civJSTfrHYJ
# C5Ok2NN75NpzTMT9V2TcIQjfQ3AFUbh1NBAYtMUuwxC6D4ceEXG5lXAnbvkC9Yje
# LVDRyImXYYmft7z+Qpl9t3C/8a0tiqnOz8Ue8/DYLtMTgvWMnsqLNjILDaImOfnH
# I36TLCjGFe8RYLXGdCUdOLlfAdMGePxSTA3TAAOc+GQbmPWjrguLWbxvnl3NVjRv
# rBZVkxFMoVZH0f7qGwDOShjpnv5nYnQ48ufL0uBz52RbPGdX4Fv9+UGOrBprmcHz
# mIutFtJec2Y4kujNtTK2wBGgWscEOVhFiaVdje8VLJ7MVNKE5TmsuGM3jTLr1nuR
# 5AFGs3UKkP7g3cQD4cHK7XdLiTm7e606QJ+WqeQsADYE9dvU9wIUbI9Dl4UcIErF
# w+FHaWSTrkfJ4SvLmhKnl5khhpJ1sF3z6e1BxepUliXHqzRLiHWihWIWESF8IHEl
# F3POxbP4VJqHBiYvaXMV0SyRgwoD6zXddbUnX9WR6JL2BlqAjjHxINwelsp/VhxA
# WThzuMA58LxvE/VAzjfFF4Wm7a1ZALmJVw3oL/s/uxo1Op4tcT+hfZ9uN1htC1JN
# 4DuRqFfLttjuoAmUQobO5zUFRzvCn8Ck/hiO+bzR15sqkjlxLMyMjpkc/ef4SUUi
# kD468vUwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3
# DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw
# MAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAx
# MDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l
# LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# 5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/
# XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1
# hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7
# M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3K
# Ni1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy
# 1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF80
# 3RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQc
# NIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahha
# YQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkL
# iWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV
# 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIG
# CSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUp
# zxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBT
# MFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI
# KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186a
# GMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br
# aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsG
# AQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN
# AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1
# OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYA
# A7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbz
# aN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6L
# GYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3m
# Sj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0
# SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxko
# JLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFm
# PWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC482
# 2rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7
# vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDVjCC
# Aj4CAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0MDFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsO
# AwIaAxUAMXYp/Wqqdyb0enigrLfxl0InAz6ggYMwgYCkfjB8MQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt
# ZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO5CgcIwIhgPMjAyNjA5
# MDIxMTA2NDJaGA8yMDI2MDkwMzExMDY0MlowdDA6BgorBgEEAYRZCgQBMSwwKjAK
# AgUA7kKBwgIBADAHAgEAAgIfIzAHAgEAAgITLjAKAgUA7kPTQgIBADA2BgorBgEE
# AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag
# MA0GCSqGSIb3DQEBCwUAA4IBAQATpMi6NkORxUmNeurEPk1vMy0E2aG9H5pF7ePi
# N8pwwzspYW7lCkUNNGDWsg6YLmfwlVh64Zff/WmSAx1WBOltft0iZYnsYZGxYuxV
# gt0JJC+vRZYSFhVLu1Ip1dopVCnBRZpONvx5GD/Q2Ifx4RJwoS7UPgpTzXvUxmqy
# 842ZBtaExfJKj02u3rzCNQ+p9g/c1JXuB7iDuWurQTaTKGs3b5utOJRW4NuudFF2
# FiVlwj0DZdY4WY9auHHHghaKsymhfdJ4/cSb+uPN+zaM8Ec1lgJjlmRKRe56jFQg
# KTs1KbTtoYLqCrpGJQOxckZ30J3VtlPa8klyxm/RCX6g082RMYIEDTCCBAkCAQEw
# gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIZXrLYVHX0sY0A
# AQAAAhkwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B
# CRABBDAvBgkqhkiG9w0BCQQxIgQgfnEu5TWVBGuL1vK82075hdT3PcKfvcH8R5tj
# XN87haUwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDckX633E1y1EF32V18
# zQcrsgjzI9+3Le7mlvk2OebthjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwAhMzAAACGV6y2FR19LGNAAEAAAIZMCIEIGaJHek+aBceiVPdcT8s
# Uusgl+VktD3XyE1aQ4LjVaNnMA0GCSqGSIb3DQEBCwUABIICAAfdSi68/MThM/JE
# k8Pe98uopmfJvQgySdEKhgLBPQjABoXiwzqz6r+DROngk+96ZMMB7Pg+cSF5Ly4K
# UBuEzC8c/7T2UZgMLx0ovHJEyl+cI6z8BBeC9MCaiRBcE6nJOh1UM9eZK7Od7big
# TJAQ1uKPzGP2K7NIqtR9zREm1yVYV5De3Xx+4QTfF+8Ok04SDSfAholOirX/UeyF
# k9eFhIZSbCb71Vi2I8Bfg7YCXknYI1uH2A47uEdeNPofJd028NDizRBim3s8WDq4
# jRFTgt2aQmDyGf2BizhNWSzLYZqPxyrroTHvyYTWlEPlQp4PYaqYRkezwadvqm7K
# REm+8bVnLygzY6R22pvIrxLkNzULMgeCD/lkGbwLNDYBbfjmkb8YK2ygOOP10nC1
# zcAKGwnfH+ZjoXY+lh16JtrMQVSVl+AAUqvXd3hn79s9GAv9BE/3VHtmVdcTkIAv
# EvRYQ2aca+zItfbRMycwUmb70iJRr9cWoI1wKa/NQUMu1mkTNOFIs/8KJMfTyuQy
# HcYwgkBLE9YYVSe1+aKUC7HI+lfBmnbcq9cO2tLgflHdZYx+uGORLc1HUrEIgPo3
# 9f/A2uPMvqiTlZmnsB+HFtaoNH/VFhdtF3m2BbEwqApy7clCOI6L5to8Hbe6VF3p
# gQ/YzSSj9xrnpIdVNMAX03X13Dgq
# SIG # End signature block