Public/Remove-AzureDevOpsAccelerator.ps1

function Remove-AzureDevOpsAccelerator {
    <#
    .SYNOPSIS
        Removes Azure DevOps resources created by the Azure Landing Zone accelerator bootstrap modules.
 
    .DESCRIPTION
        The Remove-AzureDevOpsAccelerator function performs cleanup of Azure DevOps resources that were created
        by the Azure Landing Zone accelerator bootstrap process (https://github.com/Azure/accelerator-bootstrap-modules).
        This includes projects and optionally agent pools.
 
        The function operates in the following sequence:
        1. Validates Azure CLI and Azure DevOps extension authentication
        2. Prompts for confirmation (unless bypassed or in plan mode)
        3. Discovers and deletes projects matching the specified patterns
        4. Optionally discovers and deletes agent pools matching the specified patterns
 
        CRITICAL WARNING: This is a highly destructive operation that will permanently delete Azure DevOps resources.
        Use with extreme caution and ensure you have appropriate backups and authorization before executing.
 
    .PARAMETER Organization
        The Azure DevOps organization URL or name. Can be provided as either the full URL
        (e.g., https://dev.azure.com/my-org) or just the organization name (e.g., my-org).
        This parameter is required.
 
    .PARAMETER ProjectNamePatterns
        An array of regex patterns to match against project names. Projects matching any of these
        patterns will be deleted. If empty, no projects will be deleted.
        Default: Empty array (no projects deleted)
 
    .PARAMETER AgentPoolNamePatterns
        An array of regex patterns to match against agent pool names. Agent pools matching any of
        these patterns will be deleted. If empty, no agent pools will be deleted.
        Default: Empty array (no agent pools deleted)
 
    .PARAMETER BypassConfirmation
        A switch parameter that bypasses the interactive confirmation prompts. When specified, the function
        waits for the duration specified in -BypassConfirmationTimeoutSeconds before proceeding, allowing
        time to cancel. During this timeout, pressing any key will cancel the operation.
        WARNING: Use this parameter with extreme caution as it reduces safety checks.
        Default: $false (confirmation required)
 
    .PARAMETER BypassConfirmationTimeoutSeconds
        The number of seconds to wait before proceeding when -BypassConfirmation is used. During this
        timeout, pressing any key will cancel the operation. This provides a safety window to prevent
        accidental deletions.
        Default: 30 seconds
 
    .PARAMETER ThrottleLimit
        The maximum number of parallel operations to execute simultaneously. This controls the degree
        of parallelism when processing resources. Higher values may improve performance but increase
        API throttling risk.
        Default: 11
 
    .PARAMETER PlanMode
        A switch parameter that enables "dry run" mode. When specified, the function displays what
        actions would be taken without actually making any changes. This is useful for validating
        the scope of operations before executing the actual cleanup.
        Default: $false (execute actual deletions)
 
    .EXAMPLE
        Remove-AzureDevOpsAccelerator -Organization "my-org" -ProjectNamePatterns @("^alz-.*") -PlanMode
 
        Shows what projects matching the pattern "^alz-.*" would be deleted from the "my-org"
        organization without making any changes.
 
    .EXAMPLE
        Remove-AzureDevOpsAccelerator -Organization "https://dev.azure.com/my-org" -ProjectNamePatterns @("^alz-.*")
 
        Deletes all projects matching the pattern "^alz-.*" from the "my-org" organization.
 
    .EXAMPLE
        Remove-AzureDevOpsAccelerator -Organization "my-org" -ProjectNamePatterns @("^alz-.*") -AgentPoolNamePatterns @("^alz-.*")
 
        Deletes projects and self-hosted agent pools matching the pattern "^alz-.*" from the
        "my-org" organization.
 
    .EXAMPLE
        Remove-AzureDevOpsAccelerator -Organization "my-org" -ProjectNamePatterns @("^test-alz$") -BypassConfirmation -BypassConfirmationTimeoutSeconds 10
 
        Deletes the project named exactly "test-alz" with a 10-second confirmation bypass timeout.
 
    .NOTES
        This function requires the Azure CLI with the Azure DevOps extension to be installed and authenticated.
        Install Azure CLI: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli
        Install Azure DevOps extension: az extension add --name azure-devops
        Authenticate: az devops login (supports PAT authentication, az login is not required)
 
        Required permissions:
        - Project Collection Administrator or equivalent permissions to delete projects
        - Agent Pool Administrator permissions to delete agent pools
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "[REQUIRED] The Azure DevOps organization URL or name.")]
        [Alias("org", "AzureDevOpsOrganization")]
        [string]$Organization,

        [Parameter(Mandatory = $false, HelpMessage = "[OPTIONAL] Regex patterns to match project names for deletion.")]
        [Alias("projects")]
        [string[]]$ProjectNamePatterns = @(),

        [Parameter(Mandatory = $false, HelpMessage = "[OPTIONAL] Regex patterns to match agent pool names for deletion.")]
        [Alias("pools")]
        [string[]]$AgentPoolNamePatterns = @(),

        [Parameter(Mandatory = $false, HelpMessage = "[OPTIONAL] Bypass interactive confirmation prompts.")]
        [switch]$BypassConfirmation,

        [Parameter(Mandatory = $false, HelpMessage = "[OPTIONAL] Seconds to wait when bypassing confirmation.")]
        [int]$BypassConfirmationTimeoutSeconds = 30,

        [Parameter(Mandatory = $false, HelpMessage = "[OPTIONAL] Maximum parallel operations.")]
        [int]$ThrottleLimit = 11,

        [Parameter(Mandatory = $false, HelpMessage = "[OPTIONAL] Enable dry run mode - no changes will be made.")]
        [switch]$PlanMode
    )

    function Get-NormalizedOrganizationUrl {
        param (
            [string]$Organization
        )

        # If it's already a URL, return it
        if ($Organization -match "^https?://") {
            return $Organization.TrimEnd('/')
        }

        # Otherwise, construct the URL
        return "https://dev.azure.com/$Organization"
    }

    # Main execution starts here
    if ($PSCmdlet.ShouldProcess("Delete Azure DevOps Resources", "delete")) {

        Test-Tooling -Checks @("AzureDevOpsCli")

        $TempLogFileForPlan = ""
        if($PlanMode) {
            Write-ToConsoleLog "Plan Mode enabled, no changes will be made. All actions will be logged as what would be performed." -IsWarning
            $TempLogFileForPlan = (New-TemporaryFile).FullName
        }

        $funcWriteToConsoleLog = ${function:Write-ToConsoleLog}.ToString()

        # Normalize organization URL
        $organizationUrl = Get-NormalizedOrganizationUrl -Organization $Organization
        Write-ToConsoleLog "Using Azure DevOps organization: $organizationUrl"

        # Configure Azure DevOps CLI defaults
        az devops configure --defaults organization=$organizationUrl 2>&1 | Out-Null

        if($BypassConfirmation) {
            Write-ToConsoleLog "Bypass confirmation enabled, proceeding without prompts..." -IsWarning
            Write-ToConsoleLog "This is a highly destructive operation that will permanently delete Azure DevOps resources!" -IsWarning
            Write-ToConsoleLog "We are waiting $BypassConfirmationTimeoutSeconds seconds to allow for cancellation. Press any key to cancel..." -IsWarning

            $keyPressed = $false
            $secondsRunning = 0

            while((-not $keyPressed) -and ($secondsRunning -lt $BypassConfirmationTimeoutSeconds)){
                $keyPressed = [Console]::KeyAvailable
                Write-ToConsoleLog ("Waiting for: $($BypassConfirmationTimeoutSeconds-$secondsRunning) seconds. Press any key to cancel...") -IsWarning -Overwrite
                Start-Sleep -Seconds 1
                $secondsRunning++
            }

            if($keyPressed) {
                Write-ToConsoleLog "Cancellation key pressed, exiting without making any changes..." -IsError
                return
            }
        }

        Write-ToConsoleLog "Thanks for providing the inputs, getting started..." -IsSuccess

        $hasProjectPatterns = $ProjectNamePatterns.Count -gt 0
        $hasAgentPoolPatterns = $AgentPoolNamePatterns.Count -gt 0

        if(-not $hasProjectPatterns -and -not $hasAgentPoolPatterns) {
            Write-ToConsoleLog "No patterns provided for projects or agent pools. Nothing to do. Exiting..." -IsError
            return
        }

        # Discover resources to delete
        $projectsToDelete = @()
        $agentPoolsToDelete = @()

        # Discover projects
        if($hasProjectPatterns) {
            Write-ToConsoleLog "Discovering projects in organization: $organizationUrl"

            $allProjects = (az devops project list --org $organizationUrl -o json 2>$null) | ConvertFrom-Json
            if($null -eq $allProjects -or $null -eq $allProjects.value) {
                Write-ToConsoleLog "Failed to list projects in organization: $organizationUrl" -IsError
                return
            }

            $projectList = $allProjects.value
            Write-ToConsoleLog "Found $($projectList.Count) total projects in organization: $organizationUrl"

            foreach($project in $projectList) {
                foreach($pattern in $ProjectNamePatterns) {
                    if($project.name -match $pattern) {
                        Write-ToConsoleLog "Project matches pattern '$pattern': $($project.name)"
                        $projectsToDelete += @{
                            Name = $project.name
                            Id   = $project.id
                        }
                        break
                    }
                }
            }

            Write-ToConsoleLog "Found $($projectsToDelete.Count) projects matching patterns for deletion"
        }

        # Discover agent pools
        if($hasAgentPoolPatterns) {
            Write-ToConsoleLog "Discovering agent pools in organization: $organizationUrl"

            $allAgentPools = (az pipelines pool list --org $organizationUrl -o json 2>$null) | ConvertFrom-Json
            if($null -eq $allAgentPools) {
                Write-ToConsoleLog "Failed to list agent pools in organization: $organizationUrl" -IsWarning
                $allAgentPools = @()
            }

            Write-ToConsoleLog "Found $($allAgentPools.Count) total agent pools in organization: $organizationUrl"

            foreach($pool in $allAgentPools) {
                # Skip hosted pools (Microsoft-hosted Azure Pipelines)
                if($pool.isHosted) {
                    Write-ToConsoleLog "Skipping hosted pool: $($pool.name)"
                    continue
                }

                foreach($pattern in $AgentPoolNamePatterns) {
                    if($pool.name -match $pattern) {
                        Write-ToConsoleLog "Agent pool matches pattern '$pattern': $($pool.name)"
                        $agentPoolsToDelete += @{
                            Name = $pool.name
                            Id   = $pool.id
                        }
                        break
                    }
                }
            }

            Write-ToConsoleLog "Found $($agentPoolsToDelete.Count) agent pools matching patterns for deletion"
        }

        # Confirm deletion
        $totalResourcesToDelete = $projectsToDelete.Count + $agentPoolsToDelete.Count
        if($totalResourcesToDelete -eq 0) {
            Write-ToConsoleLog "No resources found matching the provided patterns. Nothing to delete." -IsWarning
            return
        }

        if(-not $BypassConfirmation) {
            Write-ToConsoleLog "The following Azure DevOps resources will be deleted:"

            if($projectsToDelete.Count -gt 0) {
                Write-ToConsoleLog "Projects ($($projectsToDelete.Count)):"
                $projectsToDelete | ForEach-Object { Write-ToConsoleLog " - $($_.Name)"  }
            }

            if($agentPoolsToDelete.Count -gt 0) {
                Write-ToConsoleLog "Agent Pools ($($agentPoolsToDelete.Count)):"
                $agentPoolsToDelete | ForEach-Object { Write-ToConsoleLog " - $($_.Name)"  }
            }

            if($PlanMode) {
                Write-ToConsoleLog "Skipping confirmation for plan mode"
            } else {
                $continue = Invoke-PromptForConfirmation -message "ALL LISTED AZURE DEVOPS RESOURCES WILL BE PERMANENTLY DELETED"
                if(-not $continue) {
                    Write-ToConsoleLog "Exiting..."
                    return
                }
            }
        }

        # Delete projects
        if($projectsToDelete.Count -gt 0) {
            Write-ToConsoleLog "Deleting projects..."

            $projectsToDelete | ForEach-Object -Parallel {
                $funcWriteToConsoleLog = $using:funcWriteToConsoleLog
                ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog
                $TempLogFileForPlan = $using:TempLogFileForPlan
                $orgUrl = $using:organizationUrl

                $project = $_

                if($using:PlanMode) {
                    Write-ToConsoleLog `
                        "Would delete project: $($project.Name)", `
                        "Would run: az devops project delete --id $($project.Id) --org $orgUrl --yes" `
                        -IsPlan -LogFilePath $TempLogFileForPlan
                } else {
                    Write-ToConsoleLog "Deleting project: $($project.Name)"
                    $result = az devops project delete --id $project.Id --org $orgUrl --yes 2>&1
                    if($LASTEXITCODE -ne 0) {
                        Write-ToConsoleLog "Failed to delete project: $($project.Name)", "Full error: $result" -IsWarning
                    } else {
                        Write-ToConsoleLog "Deleted project: $($project.Name)"
                    }
                }
            } -ThrottleLimit $ThrottleLimit
        }

        # Delete agent pools
        if($agentPoolsToDelete.Count -gt 0) {
            Write-ToConsoleLog "Deleting agent pools..."

            $agentPoolsToDelete | ForEach-Object -Parallel {
                $funcWriteToConsoleLog = $using:funcWriteToConsoleLog
                ${function:Write-ToConsoleLog} = $funcWriteToConsoleLog
                $TempLogFileForPlan = $using:TempLogFileForPlan
                $orgUrl = $using:organizationUrl

                $pool = $_

                if($using:PlanMode) {
                    Write-ToConsoleLog `
                        "Would delete agent pool: $($pool.Name)", `
                        "Would run: az devops invoke --org $orgUrl --area distributedtask --resource pools --route-parameters poolId=$($pool.Id) --http-method DELETE --api-version 7.1" `
                        -IsPlan -LogFilePath $TempLogFileForPlan
                } else {
                    Write-ToConsoleLog "Deleting agent pool: $($pool.Name)"
                    $result = az devops invoke --org $orgUrl --area distributedtask --resource pools --route-parameters poolId=$($pool.Id) --http-method DELETE --api-version 7.1 2>&1
                    if($LASTEXITCODE -ne 0) {
                        Write-ToConsoleLog "Failed to delete agent pool: $($pool.Name)", "Full error: $result" -IsWarning
                    } else {
                        Write-ToConsoleLog "Deleted agent pool: $($pool.Name)"
                    }
                }
            } -ThrottleLimit $ThrottleLimit
        }

        Write-ToConsoleLog "Cleanup completed." -IsSuccess

        if($PlanMode) {
            Write-ToConsoleLog "Plan mode enabled, no changes were made." -IsWarning
            $planLogContents = Get-Content -Path $TempLogFileForPlan -Raw
            Write-ToConsoleLog @("Plan mode log contents:", $planLogContents) -Color Gray
            Remove-Item -Path $TempLogFileForPlan -Force
        }
    }
}

# SIG # Begin signature block
# MIIoLwYJKoZIhvcNAQcCoIIoIDCCKBwCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA3HNyf3Am68Aym
# mTKs/wWFCQsK0HZeEn1JWSMg27M436CCDXYwggX0MIID3KADAgECAhMzAAAEhV6Z
# 7A5ZL83XAAAAAASFMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjUwNjE5MTgyMTM3WhcNMjYwNjE3MTgyMTM3WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDASkh1cpvuUqfbqxele7LCSHEamVNBfFE4uY1FkGsAdUF/vnjpE1dnAD9vMOqy
# 5ZO49ILhP4jiP/P2Pn9ao+5TDtKmcQ+pZdzbG7t43yRXJC3nXvTGQroodPi9USQi
# 9rI+0gwuXRKBII7L+k3kMkKLmFrsWUjzgXVCLYa6ZH7BCALAcJWZTwWPoiT4HpqQ
# hJcYLB7pfetAVCeBEVZD8itKQ6QA5/LQR+9X6dlSj4Vxta4JnpxvgSrkjXCz+tlJ
# 67ABZ551lw23RWU1uyfgCfEFhBfiyPR2WSjskPl9ap6qrf8fNQ1sGYun2p4JdXxe
# UAKf1hVa/3TQXjvPTiRXCnJPAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUuCZyGiCuLYE0aU7j5TFqY05kko0w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwNTM1OTAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBACjmqAp2Ci4sTHZci+qk
# tEAKsFk5HNVGKyWR2rFGXsd7cggZ04H5U4SV0fAL6fOE9dLvt4I7HBHLhpGdE5Uj
# Ly4NxLTG2bDAkeAVmxmd2uKWVGKym1aarDxXfv3GCN4mRX+Pn4c+py3S/6Kkt5eS
# DAIIsrzKw3Kh2SW1hCwXX/k1v4b+NH1Fjl+i/xPJspXCFuZB4aC5FLT5fgbRKqns
# WeAdn8DsrYQhT3QXLt6Nv3/dMzv7G/Cdpbdcoul8FYl+t3dmXM+SIClC3l2ae0wO
# lNrQ42yQEycuPU5OoqLT85jsZ7+4CaScfFINlO7l7Y7r/xauqHbSPQ1r3oIC+e71
# 5s2G3ClZa3y99aYx2lnXYe1srcrIx8NAXTViiypXVn9ZGmEkfNcfDiqGQwkml5z9
# nm3pWiBZ69adaBBbAFEjyJG4y0a76bel/4sDCVvaZzLM3TFbxVO9BQrjZRtbJZbk
# C3XArpLqZSfx53SuYdddxPX8pvcqFuEu8wcUeD05t9xNbJ4TtdAECJlEi0vvBxlm
# M5tzFXy2qZeqPMXHSQYqPgZ9jvScZ6NwznFD0+33kbzyhOSz/WuGbAu4cHZG8gKn
# lQVT4uA2Diex9DMs2WHiokNknYlLoUeWXW1QrJLpqO82TLyKTbBM/oZHAdIc0kzo
# STro9b3+vjn2809D0+SOOCVZMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGg8wghoLAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAASFXpnsDlkvzdcAAAAABIUwDQYJYIZIAWUDBAIB
# BQCggbAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIFrfx+sewb3KkHCHY42LNU6X
# ksa0dxHVM4eTbomAnhORMEQGCisGAQQBgjcCAQwxNjA0oBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEcgBpodHRwczovL3d3dy5taWNyb3NvZnQuY29tIDANBgkqhkiG9w0B
# AQEFAASCAQCZhRTS2vHo5rZ+o71zKx/M9sVUSPOILkknjpGi01wN5494Z2aJOXis
# d+xfNPE9qP/RGHEpMgUPLfcCnLKOY9Io/Hi7IMfG15R4ET/KSxZH+BSCa+eJiOI/
# mVsL60X52lW4b/o+Xpto+XkfE8rcJxJGsuNSuYJKS7NH0mijBABsqZ7hrUQSgHFb
# 2fxmVT0YQaTdpnnWFFJ52aje8ruE8Aasa3dXFxhMTFZMGs7D/++zFXXxoi6KFzw0
# FCCl5zSyaKmX/t8VKYr/htCaz46C3AAVn9UfIoL9xejB04HLcoWfGQe9HN5uc5XU
# sqiuRE0UMP0zLYNg5ijSrl/qQFJW8FlCoYIXlzCCF5MGCisGAQQBgjcDAwExgheD
# MIIXfwYJKoZIhvcNAQcCoIIXcDCCF2wCAQMxDzANBglghkgBZQMEAgEFADCCAVIG
# CyqGSIb3DQEJEAEEoIIBQQSCAT0wggE5AgEBBgorBgEEAYRZCgMBMDEwDQYJYIZI
# AWUDBAIBBQAEIJHVjPccsjfHKy1NnlJbNHmrwtTZVnOm+6ODEASg4zE2AgZpb4/O
# JUkYEzIwMjYwMTMwMjAwNjEwLjE5NVowBIACAfSggdGkgc4wgcsxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBB
# bWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo4OTAw
# LTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
# ZaCCEe0wggcgMIIFCKADAgECAhMzAAACDizLKH2VIHVjAAEAAAIOMA0GCSqGSIb3
# DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAk
# BgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDEzMDE5
# NDMwM1oXDTI2MDQyMjE5NDMwM1owgcsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlv
# bnMxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo4OTAwLTA1RTAtRDk0NzElMCMG
# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAKzm3uJG1e3SFt6j0wTvxliMijexpC5YwEVDtfiz
# 2+ihhEAFM5/amhMdq3H4TCcFQYHVXa38TozCxA2Zjlekz/vloKtl3ECetX2jhO7m
# wF6ltt96Gys5ZEEgagkTo+1ah3UKsV6GbV2LPeNjcfyIWuHuep5+eJnVKdtxY8zI
# 0jG4YXOlCIMD4TlhLfeZ4yppfCF1vTUKW7KaH/cQq+SePh0ilBkRY48zePFtFUBg
# 3kna06tiQlx0PHSXTZX81h3WqS9QGA/Gsq+KrmTPsToBs6J8INIwGByf9ftFDDrf
# RCTOqGnSQNTap6L9qj0gea65F5cSOeOmBOyvgBvfcgIAoxjE5B76fnCoRVwT05PK
# GZZklLkCdZROeKiTiaDA40FZDUMs4YWRnBdPffgg8Kp3j/f8t38i2LOKy3JRliya
# X8LhmF0Atu99jDO/fU7F/w1OZXkgbFZ0eeTYeGHhufNMqiwRoOsm9AyJD6WiiMzt
# /luB3IEGdhAGbn7+ImzHDyTbbvMXaNs0j47Czwct5ka3y3q4nZ5WM0PUHRi2CwE/
# RywGWecj7j528thG3RwCrDo+JhLPkVJlxumLTF0Af+N3d3PIYCtvIu6jr0e6B8YQ
# Rv+wzTutyg/Wjdxnx5Yxvj4wgHx645vkNU8OcRwWLg0O6Rgz3WDUO3+oh6T6u0Tz
# xVLxAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQUhXFEaVIRkT7URIrpQYjtg1wQiNsw
# HwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKg
# UIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0
# JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAw
# XjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# ZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQw
# DAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8E
# BAMCB4AwDQYJKoZIhvcNAQELBQADggIBAHXgvZMv4vw5cvGcZJqXaHfSZnsWwEWi
# iJiRRU5jTkX2mfA9NW58QMZYSk03LY59pQdYg6hD/3+uPA7SFLZKkHQHMwCTaDLP
# 3Y0ZY6lZukF0y+utEOmJZmL+4tLhkZ1Gfc/YNxxiaWQ0/69pEBe+e/6anbsqAjv2
# Yn2EbIJBu+0wiORtiguoruwXtZqGf2suNfXLlAkviW8TLdCYD0pEGPnpwS/+UC/M
# Ort5KKpGr+kLKrJzy7OZDxJ4pbJa7oONQD2+LzhCyYuOo8YKcfhw/KD633lGlb7v
# eyeF7DWIJX7Be7ZHEydaDsSwPl4uQkcuzNQg935cKUP4VO9XTcZ+sMN+T7jl+Uf9
# 4pFlzcxRm2eEsmM/C/cqgoNJxbiJXpJsJHJxg+SqhYGsd/tK8MDsasfZQ63PVZrW
# Tbux1mCkbr9z0KoojwJfm+Bpr4DuhgdvhkGPtLy7yyDHBYrseBYNEHI4fcKIm7gs
# nyHdOJGRECuYdGnSVs1/WIAq4vzzogoT3Xa/TKrnm3yMzGMFTu6guythUigqTOH6
# wCSCSkY6hkvXj52XFUz3UFq/NriQ4NNSXDNv5KlexKpXHye4HqqFTLumqmDDDWrh
# I2EDEWcXGzGJOVqgvvkY3E9HrTmUnZZd6G0SLv/5h8mq8f6+epymoKPJD2E1pXO4
# 4QdfgzK6pyPCMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJmQAAAAAAFTANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQg
# VGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+
# F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU
# 88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqY
# O7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzp
# cGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka97aSueik3rMvrg0Xn
# Rm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1
# zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZN
# N3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLR
# vWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTY
# uVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUX
# k8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB
# 2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKR
# PEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0g
# BFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5t
# aWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQM
# MAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQE
# AwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQ
# W9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNv
# bS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBa
# BggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0
# LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqG
# SIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOX
# PTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6c
# qYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/z
# jj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz
# /AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyR
# gNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdU
# bZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo
# 3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4K
# u+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10Cga
# iQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9
# vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGC
# A1AwggI4AgEBMIH5oYHRpIHOMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODkwMC0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMV
# AErodj9lYuc5wwRCyOQMCgH8llYIoIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh
# bXAgUENBIDIwMTAwDQYJKoZIhvcNAQELBQACBQDtJztsMCIYDzIwMjYwMTMwMTQx
# NTA4WhgPMjAyNjAxMzExNDE1MDhaMHcwPQYKKwYBBAGEWQoEATEvMC0wCgIFAO0n
# O2wCAQAwCgIBAAICCycCAf8wBwIBAAICE1UwCgIFAO0ojOwCAQAwNgYKKwYBBAGE
# WQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDAN
# BgkqhkiG9w0BAQsFAAOCAQEAhEZuuihnex2TGtOyRlyFC3Bd1BnanC8TrlrP/NzT
# bkapxk3DTVE73Ft3CF/U149UehemfCpfCWwJOXQXBedL6c61Ipa5A4LXMk/r8Y9F
# VVnLvxEntfmrAkCCXKpr4LF+pVYSDDRr7UYP/AdHjvjr21OIA7xOiMOApkHbRYm0
# FJ+XRB4MhQCUtDJaZ+urLtlGZKGu5ncH21ejTmxqTq9liUsFaZkEJiPcc/7DL/pG
# SquODiSb8srxI2HRXZDIkBtcaAyz9J3TuPDtZ8n96jVFchKUwAYwYP+LMZkI7v7/
# Dug0scL+9FK5xhgiO8Xo6paF2OsZLGpAnN0t11LNT9/D9TGCBA0wggQJAgEBMIGT
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACDizLKH2VIHVjAAEA
# AAIOMA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQ
# AQQwLwYJKoZIhvcNAQkEMSIEIJne6V7byKygqAVZIfEJOPZnHaIwQbXH1Ola1ptq
# xr+zMIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB5DCBvQQgAXQdcyXw6YGQrbrubGhs
# pKKHA50/R5Q1dAzKk/NPEoYwgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ
# Q0EgMjAxMAITMwAAAg4syyh9lSB1YwABAAACDjAiBCCq4TyTWOtfIv3FPamud3CS
# s8Epj2AkNI1MZjRpp+osIzANBgkqhkiG9w0BAQsFAASCAgBO8Lk79zgyXZEbQ0wp
# SuE4Fzq+LtCHgqkhSV5RAKfsIJCiJv/g7FdoXpJRtkhfURS0ELqFQJwsrsedQn3U
# RsetA0JBRO5m6Ies6bFpmRU71o6Hup8Tf8sF5JNyGYd/nSSoh/ERNg8ULuomNTWU
# yDBllyHGgPCuGtCnHwQKBhnuJH3YhVMnK/8HkRz2GAuHg/B/N5D3xaFEalVt1E9P
# 73FDj359ETRHuzfrI6y6ZkeoYQzq5kF/hnLgMoCP3sKAWJt1SIxE0Rfzrm1IubCB
# JZQmpASv6m/2bcyI6fmvwGYjMOiArIr3cB3Bmy8JlBUU/TMKecF3G5+FfFwsLUCm
# CgICIb8b6D8y54iSAlgTgZsWlbZY24uLtbCx7JReSOswS0WaH3ZqrkfxKCYtNsU9
# 0IFG2oLMtz9wqe1y6wSOXleA80QT+5uFUvoIM/CStIBpJJ81DqbsnJuxHxzu94gh
# NsGTwgNoaTKMJk/2iaIu5LFrwpK+8h2O+jjfDnAJ4jR1TKUfP7sCP/fuCDaupF9y
# ylPeLay7iFyFzeNovAK7ilqOX87tUN5u6s13dKGmWsJQMmK/nmc0c8grBQPXY+Y0
# SNOxKa+xlidysqL6973FKTeWQRUP5ALKh491+66OvAk0ENdBmMfKd3QjKtvsRZLi
# C9eB3V3Lr7gOdfPJg1WyuphYTg==
# SIG # End signature block