Obs/scripts/ExtensionHelper.psm1

##------------------------------------------------------------------
## <copyright file="ExtensionHelper.psm1" company="Microsoft">
## Copyright (C) Microsoft. All rights reserved.
## </copyright>
##------------------------------------------------------------------

#region Imports
Import-Module PackageManagement -Global -DisableNameChecking -Verbose:$false
#endregion Imports

#region Constants
$global:extensionRootLocation = Split-Path -Parent $PSScriptRoot
$global:packageBinPath = Join-Path -Path $global:extensionRootLocation -ChildPath "bin"
## Value will be updated during Set-HandlerLogFile function execution.
$global:LogFile = $null
## Value will be updated by Set-ObsNugetStorePath function execution.
$global:ObsNugetStorePath = $null

$WrapperConstants = @{
    Exception = @{
        ## We throw the error using Name property of the error constant and than catch it in the caller function, where using the name property (same as Key) we retrieve the error message and error code.
        UnhandledException = @{
            Code = 1
            Name = "UnhandledException"
            Message = "An unhandled exception occurred."
        }
        HandlerEnvJsonDoesNotExist = @{
            Code = 2
            Name = "HandlerEnvJsonDoesNotExist"
            Message = "HandlerEnvironment.json file doesn't exist, cannot proceed."
        }
        LogFolderDoesNotExist = @{
            Code = 3
            Name = "LogFolderDoesNotExist"
            Message = "Log folder doesn't exist, cannot proceed."
        }
        StatusFolderDoesNotExist = @{
            Code = 4
            Name = "StatusFolderDoesNotExist"
            Message = "Status folder doesn't exist, cannot proceed."
        }
        ConfigFolderDoesNotExist = @{
            Code = 5
            Name = "ConfigFolderDoesNotExists"
            Message = "Config folder doesn't exist, cannot proceed."
        }
        PackageNotInstalled = @{
            Code = 6
            Name = "PackageNotInstalled"
            Message = "Package ({0}) is not installed at {1}."
        }
        GetPackageCommandNotFound = @{
            Code = 7
            Name = "GetPackageCommandNotFound"
            Message = "Get-Package command not found, cannot proceed."
        }
        HeartBeatFileValueDoesNotExist = @{
            Code = 8
            Name = "HeartBeatFileValueDoesNotExist"
            Message = "Heartbeat file value does not exist in the HandlerEnvironment.json file, cannot proceed."
        }
        NupkgVersionNotFound = @{
            Code = 9
            Name = "NupkgVersionNotFound"
            Message = "Unable to find package version for nupkg ({0}) in path ({1})."
        }
        NupkgFileNotFound = @{
            Code = 10
            Name = "NupkgFileNotFound"
            Message = "Nupkg file not found for {0} in path: {1}"
        }
        NugetStorePathDirectoryNotFound = @{
            Code = 11
            Name = "NugetStorePathDirectoryNotFound"
            Message = "NugetStorePath directory ({0}) was supposed to exist, cannot proceed."
        }
        SetupScriptPathNotFound = @{
            Code = 12
            Name = "SetupScriptPathNotFound"
            Message = "Setup script path ({0}) not found, cannot proceed."
        }
    }

    RequiredObsPackageNames = @{
        ObsExtSetupScripts = "Microsoft.AzureStack.Observability.ObsExtSetupScripts"
        GMA = "Microsoft.AzureStack.Observability.GenevaMonitoringAgent"
        TestObservability = "Microsoft.AzureStack.Observability.TestObservability"
        ObsDeployment = "Microsoft.AzureStack.Observability.ObservabilityDeployment"
        FDA = "Microsoft.AzureStack.Observability.FDA.FleetDiagnosticsAgent"
        MAWatchDog = "Microsoft.AzureStack.Solution.Diagnostics.HCIWatchdog"
        SBCClient = "Microsoft.AzureStack.Services.SupportBridgeController.Client"
        ObsAgent = "Microsoft.AzureStack.SupportBridge.LogCollector.WinService"
        UtcExporter = "Microsoft.Windows.Utc.Exporters.GenevaExporter"
        NetObs = "Microsoft.AS.Network.Observability.Extension"
    }

    NugetDetails = @{
        ProviderName = "Nuget"
    }

    ## One liner constants
    HandlerLogFileName = "ObservabilityExtension.log"
    HandlerEnvFileName = "HandlerEnvironment.json"
}
#endregion Constants


#region Functions

#region Handler Functions
function Get-ConfigSequenceNumber {
    [CmdletBinding()]
    Param()

    if ($null -eq $env:ConfigSequenceNumber) { 0 } else { $env:ConfigSequenceNumber } 
}

function Get-HandlerEnvInfo {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [System.String] $LogFile
    )


    <#
    Sample HandlerEnvironment.json content:
    [
        {
            "handlerEnvironment": {
                "configFolder": "C:\\Packages\\Plugins\\Microsoft.AzureStack.Observability.Observability\\0.0.0.4\\RuntimeSettings",
                "deploymentid": "",
                "heartbeatFile": "C:\\Packages\\Plugins\\Microsoft.AzureStack.Observability.Observability\\0.0.0.4\\status\\HeartBeat.Json",
                "hostResolverAddress": "",
                "instance": "",
                "logFolder": "C:\\ProgramData\\GuestConfig\\extension_logs\\Microsoft.AzureStack.Observability.Observability",
                "rolename": "",
                "statusFolder": "C:\\Packages\\Plugins\\Microsoft.AzureStack.Observability.Observability\\0.0.0.4\\status"
            },
            "name": "Microsoft.RecoveryServices.Test.AzureSiteRecovery",
            "version": "1"
        }
    ]
    #>


    $envFile = Join-path -Path $global:extensionRootLocation -ChildPath $WrapperConstants.HandlerEnvFileName
    if (-not (Test-Path $envFile -PathType Leaf)) {
        throw $WrapperConstants.Exception.HandlerEnvJsonDoesNotExist.Name
    }

    ## Read handler config
    $envJson = Get-Content -Path $envFile -Raw | ConvertFrom-Json
    if ($envJson -is [System.Array]) {
        $envJson = $envJson[0]
    }
    return $envJson.handlerEnvironment
    
}

function Get-HandlerHeartBeatFile {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [System.String] $LogFile
    )

    $handlerEnvInfo = Get-HandlerEnvInfo

    if ($null -eq $handlerEnvInfo.heartbeatFile) {
        throw $WrapperConstants.Exception.HeartBeatFileValueDoesNotExist.Name
    }

    return $handlerEnvInfo.heartbeatFile
}

function Get-HandlerConfigSettings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [System.String] $LogFile
    )

    <#
        Sample config json file:
        ------------------------------------------------------------
        {
            "runtimeSettings":
            [
                {
                    "handlerSettings":
                    {
                        "publicSettings":
                        {
                            "region": "eastus",
                            "cloudName": "AzureCanary"
                        }
                    }
                }
            ]
        }
        -----------------------------------------------------------
 
        Or If you don't want to pass any values, it can be empty as follows:
 
        { "runtimeSettings": [ { "handlerSettings":{ "publicSettings":{} } } ] }
    #>


    $functionName = $MyInvocation.MyCommand.Name

    $handlerEnvInfo = Get-HandlerEnvInfo

    if (-not (Test-Path $handlerEnvInfo.configFolder -PathType Container)) {
        Write-Log "[$functionName] $($WrapperConstants.Exception.ConfigFolderDoesNotExist.Message)" `
            -Level "ERROR"

        throw $WrapperConstants.Exception.ConfigFolderDoesNotExist.Name
    }

    $configFile = Get-ChildItem -Path $handlerEnvInfo.configFolder | Sort-Object CreationTime -Descending | Select-Object -First 1
    ## Parse config file to read parameters
    $configJson = Get-Content -Path $configFile.FullName -Raw | ConvertFrom-Json
        
    return $configJson.runtimeSettings[0].handlerSettings.publicSettings
}

function Get-LogFolderPath {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [System.String] $LogFile
    )

    $handlerEnvInfo = Get-HandlerEnvInfo

    if (-not (Test-Path $handlerEnvInfo.logFolder -PathType Container)) {
        throw $WrapperConstants.Exception.LogFolderDoesNotExist.Name
    }

    return $handlerEnvInfo.logFolder
}

function Get-StatusFolderPath {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [System.String] $LogFile
    )

    $handlerEnvInfo = Get-HandlerEnvInfo

    if (-not (Test-Path $handlerEnvInfo.statusFolder -PathType Container)) {
        throw $WrapperConstants.Exception.StatusFolderDoesNotExist.Name
    }

    return $handlerEnvInfo.statusFolder
}

function Get-StatusFilePath {
    [CmdletBinding()]
    Param()

    $configSeqNum = Get-ConfigSequenceNumber
    $statusFolder = Get-StatusFolderPath
    return "$statusFolder\$configSeqNum.status"
}

function Set-HandlerLogFile {
    [CmdletBinding()]
    Param()

    $functionName = $MyInvocation.MyCommand.Name

    if ($null -eq $global:LogFile) {
        $global:LogFile = Join-Path $(Get-LogFolderPath) -ChildPath $WrapperConstants.HandlerLogFileName
        Write-Log "[$functionName] Setting the global:LogFile with the log file path of $($global:LogFile)."
    }
}

function Get-HandlerLogFile {
    [CmdletBinding()]
    Param()

    Set-HandlerLogFile
    return $global:LogFile
}

function Set-Status {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [System.String] $Name,

        [Parameter(Mandatory=$True)]
        [System.String] $Operation,

        [Parameter(Mandatory=$True)]
        [System.String] $Message,

        [Parameter(Mandatory=$True)]
        [System.String] $Status,

        [Parameter(Mandatory=$True)]
        [System.Int16] $Code
    )
    
    & "$PSScriptRoot\ReportStatus.ps1" `
        -Name $Name `
        -Operation $Operation `
        -Message $Message `
        -Status $Status `
        -Code $Code
}
#endregion Handler Functions

#region Observability Functions
function Set-NugetPackageProvider {
    [CmdletBinding()]
    Param()
    
    $functionName = $MyInvocation.MyCommand.Name

    $providerName = $WrapperConstants.NugetDetails.ProviderName
    $nugetProvider = Get-PackageProvider | Where-Object { $_.Name -eq $providerName }

    if ($null -eq $nugetProvider) {
        Write-Log "[$functionName] Attempting to install $providerName package provider."
        Install-PackageProvider $providerName -Force -ForceBootstrap
    }
    else {
        Write-Log "[$functionName] Package provider $providerName already installed."
    }
}

function Set-AclsForGivenPath {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [System.String] $PathToSetAcls
    )
    
    $functionName = $MyInvocation.MyCommand.Name
    Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)"

    $requiredACLs = @{
        "BUILTIN\Administrators" = "FullControl"
        "Everyone" = "ReadAndExecute"
        # "NT AUTHORITY\SYSTEM" = "FullControl"
    }

    ## Query existing Acls
    $aclObj = Get-Acl $PathToSetAcls

    # Check if required ACLs are missing
    $missingACLs = $requiredACLs.Keys | Where-Object { $_ -notin $aclObj.Access.IdentityReference }

    if ($missingACLs.Count -gt 0) {
        Write-Log "[$functionName] Re-configuring ACLs as some are missing. Missing ACLs = $($missingACLs -join ',')"

        ## Disable inheritance for PathToSetAcls and remove inherited acls
        ## https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.objectsecurity.setaccessruleprotection?view=net-8.0
        $aclObj.SetAccessRuleProtection($True, $False)

        ## Remove all existing access rules
        $aclObj.Access | ForEach-Object { $aclObj.RemoveAccessRule($_) } | Out-Null

        ## Re-configure acls
        foreach ($acl in $requiredACLs.GetEnumerator())
        {
            $account = $acl.Name
            $access = $acl.Value

            Write-Log "[$functionName] Give '$access' access to '$account'."

            # 3,0 allows child items of Path to inhertit the acls
            $newAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($account, $access, 3, 0, "Allow")

            $aclObj.SetAccessRule($newAccessRule)
        }

        ## Finally commit the acls
        Set-Acl -Path $PathToSetAcls -AclObject $aclObj
        Write-Log "[$functionName] ACLs committed for '$PathToSetAcls'."
    }
    else {
        Write-Log "[$functionName] All required ACLs are already present."
    }

    Write-Log "[$functionName] Exiting."
}

function Set-ObsStoreRootFolderPath {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [System.Management.Automation.SwitchParameter] $SetAcls
    )
    
    $functionName = $MyInvocation.MyCommand.Name
    Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)"
    
    $obsRootFolderRegKeyPath = "HKLM:\SOFTWARE\Microsoft\AzureStack\Observability"
    $obsRootFolderRegKeyName = "ObsRootFolderPath"
    
    $obsRootFolderPath = $null

    ## Check if ObsStorePath is present in registry
    if (Get-ItemProperty -Path $obsRootFolderRegKeyPath -Name $obsRootFolderRegKeyName -ErrorAction Ignore) {
        $pathFromRegKey = Get-ItemPropertyValue -Path $obsRootFolderRegKeyPath -Name $obsRootFolderRegKeyName -ErrorAction Ignore
        Write-Log "[$functionName] ObsRootFolderPath key found in registry - $pathFromRegKey"

        ## If key is present then check if the directory exists, if not, create a new directory and overwrite the new path in registry.
        if (Test-Path -Path $pathFromRegKey -PathType Container -ErrorAction Ignore) {
            $obsRootFolderPath = $pathFromRegKey
        }
        else {
            Write-Log "[$functionName] Directory not found for path - $pathFromRegKey."
        }
    }
    
    if (-not $obsRootFolderPath) {
        Write-Log "[$functionName] Either ObsRootFolderPath key not found in registry or the path doesn't exists. Create new path and store in registry - $obsRootFolderRegKeyPath."
        
        ## Create the registry key path if it does not exist.
        if (-not (Test-Path -Path $obsRootFolderRegKeyPath -ErrorAction Ignore)) {
            Write-Log "[$functionName] Registry key path not found. Creating registry key path - $obsRootFolderRegKeyPath"
            $out = New-Item -Path $obsRootFolderRegKeyPath -Force
            Write-Log "[$functionName] Registry key path created - $out"
        }

        ## Generate a unique ObsStorePath using last 4 characters of GUID.
        $guidStr = [System.Guid]::NewGuid().ToString()
        $last_4_chars = $guidStr.Substring($guidStr.Length - 4)
        $obsRootFolderPath = "$($env:SystemDrive)\Obs_$last_4_chars"

        ## Create the directory for the path.
        $out = New-Item -Path $obsRootFolderPath -ItemType Directory -Force
        Write-Log "[$functionName] Created directory for path - $out"

        ## Store the path in registry.
        $out = Set-ItemProperty -Path $obsRootFolderRegKeyPath -Name $obsRootFolderRegKeyName -Value $obsRootFolderPath
        Write-Log "[$functionName] Created registry key ($obsRootFolderRegKeyPath\$obsRootFolderRegKeyName) and stored ObsRootFolderPath value ($obsRootFolderPath) - $out."
    }

    if ($SetAcls) {
        ## Set ACLs for the Obs Store Path if it doesn't have the required permissions.
        Set-AclsForGivenPath -PathToSetAcls $obsRootFolderPath
    }

    Write-Log "[$functionName] Exiting. Returning { ObsRootFolderPath = $obsRootFolderPath }"
    return $obsRootFolderPath
}

function Get-ExtVersion {
    [CmdletBinding()]
    Param()
    
    $functionName = $MyInvocation.MyCommand.Name
    Write-Log "[$functionName] Entering."
    
    ## Figure out the extension version from env variable.
    $ExtVersion = [System.Environment]::GetEnvironmentVariable("AZURE_GUEST_AGENT_EXTENSION_VERSION")
    if (-not $ExtVersion) {
        ## As a fallback, get the extension version from the extension folder path.
        Write-Log "[$functionName] AZURE_GUEST_AGENT_EXTENSION_VERSION environment variable not found. Setting it from extension folder path."
        $ExtVersion = Split-Path -Leaf $global:extensionRootLocation
        Write-Log "[$functionName] Extension version from extension folder path = $ExtVersion."
    }

    Write-Log "[$functionName] Exiting. Returning { ExtVersion = $ExtVersion }"
    return $ExtVersion
}

function Set-ObsNugetStorePath {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [System.String] $ObsStoreRootPath,

        [Parameter(Mandatory=$False)]
        [System.String] $ExtVersion,

        [Parameter(Mandatory=$False)]
        [System.String] $WorkloadName,

        [Parameter(Mandatory=$False)]
        [System.Management.Automation.SwitchParameter] $CreatePathIfNotExists
    )
    
    $functionName = $MyInvocation.MyCommand.Name
    Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)"

    if (($null -ne $ExtVersion -and $ExtVersion -in $global:ObsNugetStorePath) -or
        ($null -ne $global:ObsNugetStorePath)) {
        Write-Log "[$functionName] ObsNugetStorePath already set. Exiting."
        return
    }

    $partialNugetStorePath = Join-Path -Path $ObsStoreRootPath -Child "Nugets"

    ## If ExtVersion is not passed, then figure out the extension version that is getting used.
    if (-not $ExtVersion) {
        $ExtVersion = Get-ExtVersion
    }

    ## Set the ObsNugetStorePath with the ext version specific nuget store.
    $global:ObsNugetStorePath = Join-Path -Path $partialNugetStorePath -ChildPath $ExtVersion
    Write-Log "[$functionName] Setting ObsNugetStorePath = $($global:ObsNugetStorePath)."
    
    ## Create the obsNugetStorePath directory if it does not exist.
    if ($CreatePathIfNotExists) {    
        if (-not (Test-Path -Path $global:ObsNugetStorePath -PathType Container)) {
            $out = New-Item -Path $global:ObsNugetStorePath -ItemType Directory -Force
            Write-Log "[$functionName] Created directory for path - $out"
        }
        else {
            Write-Log "[$functionName] Directory already exists for path - $($global:ObsNugetStorePath)."
        }
    }
    else {
        ## Directory must exist for the path.
        Write-Log "[$functionName] Let's confirm whether directory for path exists or not. Path = $($global:ObsNugetStorePath)."
        if (Test-Path -Path $global:ObsNugetStorePath -PathType Container) {
            Write-Log "[$functionName] Confirmed directory exists - $($global:ObsNugetStorePath)."
        }
        elseif ($WorkloadName -eq "Update") {
            ## For Update script, if nuget store based path is not found, then we can fallback to the extension folder path. So for this case, we don't throw an error if NugetStorePath directory is not found.
            Write-Log "[$functionName] Expected directory for workload ($WorkloadName) not found for path - $($global:ObsNugetStorePath)."
        }
        else {
            ## Error message is updated with the path where the directory was supposed to be present.
            $WrapperConstants.Exception.NugetStorePathDirectoryNotFound.Message = $WrapperConstants.Exception.NugetStorePathDirectoryNotFound.Message -f $global:ObsNugetStorePath
            Write-Log "[$functionName] $($WrapperConstants.Exception.NugetStorePathDirectoryNotFound.Message)."
            throw $WrapperConstants.Exception.NugetStorePathDirectoryNotFound.Name
        }
    }
    
    Write-Log "[$functionName] Exiting."
}

function Confirm-PackageExists {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [System.String] $PackageName,

        [Parameter(Mandatory=$True)]
        [System.String] $SourcePath,

        [Parameter(Mandatory=$False)]
        [System.String] $ProviderName = $WrapperConstants.NugetDetails.ProviderName,

        [Parameter(Mandatory=$False)]
        [System.Management.Automation.SwitchParameter] $ThrowIfNotExists
    )

    $functionName = $MyInvocation.MyCommand.Name
    Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)"
    
    $packageExists = Find-Package -Name $PackageName -ProviderName $ProviderName -Source $SourcePath -ErrorAction Ignore

    if (-not $packageExists) {
        if ($ThrowIfNotExists) {
            ## Error message is updated with the package name and path where the nupkg file supposed to be present.
            $WrapperConstants.Exception.NupkgFileNotFound.Message = $WrapperConstants.Exception.NupkgFileNotFound.Message -f $PackageName, $SourcePath
            Write-Log "[$functionName] $($WrapperConstants.Exception.NupkgFileNotFound.Message)" -Level "ERROR"
            throw $WrapperConstants.Exception.NupkgFileNotFound.Name
        }
        else {
            Write-Log "[$functionName] Nupkg file for ($PackageName) not found in source ($SourcePath)."
            return $false
        }
    }

    Write-Log "[$functionName] Exiting. Nupkg file with version $($packageExists.PackageFilename) found at path $($packageExists.Source)."
    return $true
}

function Extract-Package {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [System.String] $PackageName,

        [Parameter(Mandatory=$True)]
        [System.String] $SourcePath,

        [Parameter(Mandatory=$True)]
        [System.String] $DestinationPath,

        [Parameter(Mandatory=$False)]
        [System.String] $ProviderName = $WrapperConstants.NugetDetails.ProviderName
    )
    
    $functionName = $MyInvocation.MyCommand.Name
    Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)"

    $output = Install-Package `
        -Name $PackageName `
        -Source $SourcePath `
        -Destination $DestinationPath `
        -ProviderName $ProviderName `
        -Force

    Write-Log "[$functionName] Exiting. Successfully installed package. Result = $($output | Select-Object Name, Status, Source, FullPath)."
}

function Install-ObsPackages {
    [CmdletBinding()]
    Param()
    
    $functionName = $MyInvocation.MyCommand.Name
    Write-Log "[$functionName] Entering."

    Set-NugetPackageProvider

    ## Check if ZDP package path exists or not.
    $obsZDPPackagePath = "$env:SystemDrive\ObsZDP"
    $zdpPathExists = Test-Path $obsZDPPackagePath -PathType Container -ErrorAction Ignore

    ## Install the nuget packages
    foreach($packageName in $WrapperConstants.RequiredObsPackageNames.Values) {
        Write-Log "[$functionName] Installing package: $packageName from source: $global:packageBinPath."

        if (Confirm-PackageExists -PackageName $packageName -SourcePath $global:packageBinPath -ThrowIfNotExists) {
            Extract-Package `
                -PackageName $packageName `
                -SourcePath $global:packageBinPath `
                -DestinationPath $global:ObsNugetStorePath
        }

        if ($zdpPathExists) {
            Write-Log "[$functionName] Installing package: $packageName from source: $obsZDPPackagePath."
            ## Extract the ZDPd nugets to ObsNugetStorePath.
            ## Note: SourcePath value is different for ZDPd nugets.
            if (Confirm-PackageExists -PackageName $packageName -SourcePath $obsZDPPackagePath) {
                Extract-Package `
                    -PackageName $packageName `
                    -SourcePath $obsZDPPackagePath `
                    -DestinationPath $global:ObsNugetStorePath
            }
        }
    }
    
    Write-Log "[$functionName] Exiting. Successfully installed observability nuget packages."
}

function Uninstall-ObsPackages {
    [CmdletBinding()]
    Param()
    
    $functionName = $MyInvocation.MyCommand.Name
    Write-Log "[$functionName] Entering."

    ## Navigate to the parent directory of ObsNugetStorePath
    $nugetStorePathParent = Split-Path -Parent $global:ObsNugetStorePath

    ## Get all ext versions specific nuget store paths. Sort them in descending order.
    ## For e.g. List all the version folders in the ObsNugetStorePathParent directory (i.e. C:\Obs_XXXX\Nugets\), like 1.0.6.0, 1.0.6.1, 1.0.7.0, etc.
    $allVersionsOfNugetStorePaths = Get-ChildItem -Path $nugetStorePathParent -Directory -Name | ForEach-Object { [System.Version] $_ } | Sort-Object -Descending
    Write-Log "[$functionName] AllVersionsOfNugetStorePaths = `n$($allVersionsOfNugetStorePaths | Out-String) ."

    ## Keep the latest two versions of the nuget store paths.
    $latestTwoVersions = $allVersionsOfNugetStorePaths | Select-Object -First 2
    Write-Log "[$functionName] LatestTwoVersions = `n$($latestTwoVersions | Out-String) ."
    
    ## Delete N-2 and older versions of the nuget store paths.
    foreach ($version in $allVersionsOfNugetStorePaths) {
        if ($version -notin $latestTwoVersions) {
            $pathToDelete = Join-Path -Path $nugetStorePathParent -ChildPath $version.ToString()
            Write-Log "[$functionName] Deleting directory: $pathToDelete"
            try {
                ## Only when ErrorAction is set to SilentlyContinue, the error is saved in the ErrorVariable. For ErrorAction Ignore, the error is not saved in the ErrorVariable.
                Remove-Item -Path $pathToDelete -Recurse -Force `
                    -ErrorAction SilentlyContinue -ErrorVariable removeItemError
                if ($removeItemError) {
                    ## Just log the error and continue with the next directory deletion.
                    Write-Log "[$functionName] Error occured while deleting directory ($pathToDelete). ErrorDetails: $removeItemError"
                }
                else {
                    Write-Log "[$functionName] Successfully deleted directory: $pathToDelete"
                }
            }
            catch {
                $exceptionDetails = Get-ExceptionDetails -ErrorObject $_
                Write-Log "[$functionName] Failed to delete directory: $pathToDelete. Error: $exceptionDetails" -Level "ERROR"
            }
        }
        else {
            Write-Log "[$functionName] Skipping version ($version) as it is one of the latest two versions."
        }
    }
    
    Write-Log "[$functionName] Exiting. Successfully uninstalled observability nuget packages."
}

function Get-SetupScriptPath {
    [CmdletBinding()]
    Param()

    $functionName = $MyInvocation.MyCommand.Name

    if (-not (Get-Command Get-Package -ErrorAction Ignore)) {
        Write-Log "[$functionName] $($WrapperConstants.Exception.GetPackageCommandNotFound.Message)" -Level "ERROR"
        throw $WrapperConstants.Exception.GetPackageCommandNotFound.Name
    }

    $setupScriptsPackageObj = Get-Package `
                            -Name $WrapperConstants.RequiredObsPackageNames.ObsExtSetupScripts `
                            -Destination $global:ObsNugetStorePath `
                            -ProviderName $WrapperConstants.NugetDetails.ProviderName

    Write-Log "[$functionName] SetupScriptsPackageObj = $($setupScriptsPackageObj)"
    $setupScriptsPackageContentPath = Join-Path -Path ([System.IO.Path]::GetDirectoryName($setupScriptsPackageObj.Source)) -ChildPath "content"
    Write-Log "[$functionName] SetupScriptsPackageContentPath = $($setupScriptsPackageContentPath)"
    $scriptPath = Join-Path -Path $setupScriptsPackageContentPath -ChildPath "Setup-Extension.ps1"
    
    if (-not (Test-Path $scriptPath)) {
        $WrapperConstants.Exception.SetupScriptPathNotFound.Message = $WrapperConstants.Exception.SetupScriptPathNotFound.Message -f $scriptPath
        Write-Log "[$functionName] $($WrapperConstants.Exception.SetupScriptPathNotFound.Message)" -Level "ERROR"
        throw $WrapperConstants.Exception.SetupScriptPathNotFound.Name
    }

    Write-Log "[$functionName] Returning. Successfully found setup scripts path at = $scriptPath"
    return $scriptPath
}

function Get-FileLockProcess {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [System.String] $FilePath,

        [Parameter(Mandatory=$False)]
        [System.String] $LogFile
    )

    $functionName = $MyInvocation.MyCommand.Name

    ##### BEGIN Variable/Parameter Transforms and PreRun Prep #####

    if (! $(Test-Path $FilePath)) {
        Write-Log "[$functionName] The path $FilePath was not found! Halting!" -Level "WARNING"
        return
    }

    ##### END Variable/Parameter Transforms and PreRun Prep #####


    ##### BEGIN Main Body #####

    if ($PSVersionTable.PSEdition -eq "Desktop" -or $PSVersionTable.Platform -eq "Win32NT" -or 
    $($PSVersionTable.PSVersion.Major -le 5 -and $PSVersionTable.PSVersion.Major -ge 3)) {
        $CurrentlyLoadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies()
    
        $AssembliesFullInfo = $CurrentlyLoadedAssemblies | Where-Object {
            $_.GetName().Name -eq "Microsoft.CSharp" -or
            $_.GetName().Name -eq "mscorlib" -or
            $_.GetName().Name -eq "System" -or
            $_.GetName().Name -eq "System.Collections" -or
            $_.GetName().Name -eq "System.Core" -or
            $_.GetName().Name -eq "System.IO" -or
            $_.GetName().Name -eq "System.Linq" -or
            $_.GetName().Name -eq "System.Runtime" -or
            $_.GetName().Name -eq "System.Runtime.Extensions" -or
            $_.GetName().Name -eq "System.Runtime.InteropServices"
        }
        $AssembliesFullInfo = $AssembliesFullInfo | Where-Object {$_.IsDynamic -eq $False}
  
        $ReferencedAssemblies = $AssembliesFullInfo.FullName | Sort-Object | Get-Unique

        $usingStatementsAsString = @"
        using Microsoft.CSharp;
        using System.Collections.Generic;
        using System.Collections;
        using System.IO;
        using System.Linq;
        using System.Runtime.InteropServices;
        using System.Runtime;
        using System;
        using System.Diagnostics;
"@

        
        $TypeDefinition = @"
        $usingStatementsAsString
         
        namespace MyCore.Utils
        {
            static public class FileLockUtil
            {
                [StructLayout(LayoutKind.Sequential)]
                struct RM_UNIQUE_PROCESS
                {
                    public int dwProcessId;
                    public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
                }
         
                const int RmRebootReasonNone = 0;
                const int CCH_RM_MAX_APP_NAME = 255;
                const int CCH_RM_MAX_SVC_NAME = 63;
         
                enum RM_APP_TYPE
                {
                    RmUnknownApp = 0,
                    RmMainWindow = 1,
                    RmOtherWindow = 2,
                    RmService = 3,
                    RmExplorer = 4,
                    RmConsole = 5,
                    RmCritical = 1000
                }
         
                [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
                struct RM_PROCESS_INFO
                {
                    public RM_UNIQUE_PROCESS Process;
         
                    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)]
                    public string strAppName;
         
                    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)]
                    public string strServiceShortName;
         
                    public RM_APP_TYPE ApplicationType;
                    public uint AppStatus;
                    public uint TSSessionId;
                    [MarshalAs(UnmanagedType.Bool)]
                    public bool bRestartable;
                }
         
                [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
                static extern int RmRegisterResources(uint pSessionHandle,
                                                    UInt32 nFiles,
                                                    string[] rgsFilenames,
                                                    UInt32 nApplications,
                                                    [In] RM_UNIQUE_PROCESS[] rgApplications,
                                                    UInt32 nServices,
                                                    string[] rgsServiceNames);
         
                [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)]
                static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey);
         
                [DllImport("rstrtmgr.dll")]
                static extern int RmEndSession(uint pSessionHandle);
         
                [DllImport("rstrtmgr.dll")]
                static extern int RmGetList(uint dwSessionHandle,
                                            out uint pnProcInfoNeeded,
                                            ref uint pnProcInfo,
                                            [In, Out] RM_PROCESS_INFO[] rgAffectedApps,
                                            ref uint lpdwRebootReasons);
         
                /// <summary>
                /// Find out what process(es) have a lock on the specified file.
                /// </summary>
                /// <param name="path">Path of the file.</param>
                /// <returns>Processes locking the file</returns>
                /// <remarks>See also:
                /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373661(v=vs.85).aspx
                /// http://wyupdate.googlecode.com/svn-history/r401/trunk/frmFilesInUse.cs (no copyright in code at time of viewing)
                ///
                /// </remarks>
                static public List<Int32> WhoIsLocking(string path)
                {
                    // Console.WriteLine("Looking for process handles for file {0}.", path);
                    uint handle;
                    string key = Guid.NewGuid().ToString();
                    var processes = new List<Int32>();
         
                    int res = RmStartSession(out handle, 0, key);
                    if (res != 0) throw new Exception("Could not begin restart session. Unable to determine file locker.");
         
                    try
                    {
                        const int ERROR_MORE_DATA = 234;
                        uint pnProcInfoNeeded = 0,
                            pnProcInfo = 0,
                            lpdwRebootReasons = RmRebootReasonNone;
         
                        string[] resources = new string[] { path }; // Just checking on one resource.
         
                        res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null);
         
                        if (res != 0) throw new Exception("Could not register resource.");
         
                        //Note: there's a race condition here -- the first call to RmGetList() returns
                        // the total number of process. However, when we call RmGetList() again to get
                        // the actual processes this number may have increased.
                        res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons);
         
                        if (res == ERROR_MORE_DATA)
                        {
                            // Create an array to store the process results
                            RM_PROCESS_INFO[] processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded];
                            pnProcInfo = pnProcInfoNeeded;
         
                            // Get the list
                            res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons);
                            if (res == 0)
                            {
                                processes = new List<Int32>((int)pnProcInfo);
         
                                // Enumerate all of the results and add them to the
                                // list to be returned
                                for (int i = 0; i < pnProcInfo; i++)
                                {
                                    try
                                    {
                                        processes.Add(processInfo[i].Process.dwProcessId);
                                    }
                                    // catch the error -- in case the process is no longer running
                                    catch (ArgumentException) { }
                                }
                            }
                            else {
                                var exceptionMessage = String.Format("Could not list processes locking file ({0}).", path);
                                throw new Exception(exceptionMessage);
                            }
                        }
                        else if (res != 0) {
                            var exceptionMessage = String.Format("Could not list processes locking file ({0}). Failed to get size of result.", path);
                            throw new Exception(exceptionMessage);
                        }
                    }
                    finally
                    {
                        RmEndSession(handle);
                    }
         
                    return processes;
                }
            }
        }
"@


            $CheckMyCoreUtilsFileLockUtilLoaded = $CurrentlyLoadedAssemblies | Where-Object {$_.ExportedTypes -like "MyCore.Utils.FileLockUtil*"}
            if ($null -eq $CheckMyCoreUtilsFileLockUtilLoaded) {
                Add-Type -ReferencedAssemblies $ReferencedAssemblies -TypeDefinition $TypeDefinition
            }

            $Result = [MyCore.Utils.FileLockUtil]::WhoIsLocking($FilePath)
        }
        if ($null -ne $PSVersionTable.Platform -and $PSVersionTable.Platform -ne "Win32NT") {
            $lsofOutput = lsof $FilePath

            function Parse-lsofStrings ($lsofOutput, $Index) {
                $($lsofOutput[$Index] -split " " | foreach {
                    if (![String]::IsNullOrWhiteSpace($_)) {
                        $_
                    }
                }).Trim()
            }

            $lsofOutputHeaders = Parse-lsofStrings -lsofOutput $lsofOutput -Index 0
            $lsofOutputValues = Parse-lsofStrings -lsofOutput $lsofOutput -Index 1

            $Result = [pscustomobject]@{}
            for ($i=0; $i -lt $lsofOutputHeaders.Count; $i++) {
                $Result | Add-Member -MemberType NoteProperty -Name $lsofOutputHeaders[$i] -Value $lsofOutputValues[$i]
            }
        }

        return $Result
    
    ##### END Main Body #####

}

Function Close-ProcessHandles {
    [CmdletBinding()]
    Param (        
        [Parameter(Mandatory=$True)]
        [System.String] $FolderPathToClean
    )

    $functionName = $MyInvocation.MyCommand.Name
    Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)"

    ## Get the process handles that are locking files of lower extension version and save the fileName and corresponding ProcessIDs.
    $filesLockedByProcessesDict = @{}
    foreach ($directory in (Get-ChildItem $FolderPathToClean)) {
        Write-Log "[$functionName] Checking process handles inside directory: $($directory.FullName)"
        Get-ChildItem -Path $directory.FullName -Recurse | Where-Object { ! $_.PSIsContainer } | ForEach-Object {
            $filePath = $_.FullName
            try {
                $processHandles = Get-FileLockProcess -FilePath $filePath
                if ($null -ne $processHandles -and $processHandles.Count -gt 0) {
                    $filesLockedByProcessesDict[$filePath] = $processHandles
                }
            }
            catch {
                $exceptionDetails = Get-ExceptionDetails -ErrorObject $_
                Write-Log "[$functionName] Exception occurred for file ($filePath). Exception is as follows: $exceptionDetails"
            }
        }
    }

    if ($filesLockedByProcessesDict.Keys.Count -eq 0) {
        Write-Log "[$functionName] No files found with locked process handles."
    }
    else {
        Write-Log "[$functionName] Files locked by Processes are as follows = $($filesLockedByProcessesDict | ConvertTo-Json -Compress)."

        $currentPID = [System.Diagnostics.Process]::GetCurrentProcess().Id
        Write-Log "[$functionName] Current PID is $currentPID"

        $returnStatusMessage = [System.String]::Empty

        ## Loop through the ProcessIDs and force stop them accordingly (if needed).
        Write-Log "[$functionName] Looping through locked file processes and force stop them accordingly (if needed)."
        ## Maintain a set of stopped processes so we don't stop the same one again
        $stoppedPID = [System.Collections.Generic.HashSet[string]]@()
        foreach ($currentFile in $filesLockedByProcessesDict.Keys) {
            foreach ($procId in $filesLockedByProcessesDict[$currentFile]) {
                if ($procId -eq $currentPID) {
                    ## We do not want to stop the current process, so if the file is locked by the current process, we hope that the process will finish successfully and release the handle.
                    Write-Log "[$functionName] Ignoring file $currentFile as it is used by PID ($procId) which is running the current script."
                } 
                elseif ($stoppedPID.Contains($procId)) {
                    ## We do not want to stop the same process again (and get "PID not found" error)
                    Write-Log "[$functionName] Ignoring PID ($procId) as it was already stopped"
                }
                else {
                    $procDetails = gwmi win32_process | Where-Object {$_.ProcessId -eq $procId} | Select-Object ProcessName, ExecutablePath, CommandLine
                    $fileLockingProcDetails = @{
                        FilePath = $currentFile
                        ProcId = $procId
                        ProcessName = $procDetails.ProcessName
                        ExecutablePath = $procDetails.ExecutablePath
                        CommandLine = $procDetails.CommandLine
                    }
                    Write-Log "[$functionName] Details of file and its locking process = $($fileLockingProcDetails | ConvertTo-Json -Compress)"
                    try {
                        Write-Log "[$functionName] Stopping process $procId = $(Stop-Process -Id $procId -Force -PassThru | Out-String)"
                        $stoppedPID.Add($procId) | Out-Null
                    }
                    catch{
                        $returnStatusMessage += "[$functionName] Cannot stop process $procId due to error $_"
                    }
                }
            }
        }
    }

    Write-Log "[$functionName] Exiting. Return status message = $returnStatusMessage"
    return $returnStatusMessage
}
#endregion Observability Functions

#region Misc Functions
function Get-ExceptionDetails {
    [CmdLetBinding()]
    Param (
        [Parameter(Mandatory=$True, ValueFromPipeline)]
        [System.Management.Automation.ErrorRecord] $ErrorObject
    )

    return @{
        Errormsg = $ErrorObject.ToString()
        Exception = $ErrorObject.Exception.ToString()
        Stacktrace = $ErrorObject.ScriptStackTrace
        Failingline = $ErrorObject.InvocationInfo.Line
        Positionmsg = $ErrorObject.InvocationInfo.PositionMessage
        PScommandpath = $ErrorObject.InvocationInfo.PSCommandPath
        Failinglinenumber = $ErrorObject.InvocationInfo.ScriptLineNumber
        Scriptname = $ErrorObject.InvocationInfo.ScriptName
    } | ConvertTo-Json ## The ConvertTo-Json will return the entire hashtable as string.
}

function Save-Exception {
    [CmdLetBinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [System.Management.Automation.ErrorRecord] $ErrorObject,
        
        [Parameter(Mandatory=$False)]
        [System.String] $CustomErrorMessage
    )
    
    $functionName = $MyInvocation.MyCommand.Name

    $errorKey = $ErrorObject.ToString()
    $exceptionConstant = $null
    
    if ($global:ErrorConstants -and $global:ErrorConstants.Keys -contains $errorKey) {
        $exceptionConstant = $global:ErrorConstants[$errorKey]
    }
    elseif ($WrapperConstants.Exception.Keys -contains $errorKey) {
        $exceptionConstant = $WrapperConstants.Exception[$errorKey]
    }

    if ($exceptionConstant) {
        $customErrorMessage += $exceptionConstant.Message
        $errorCode = $exceptionConstant.Code
    }
    else {
        $customErrorMessage += "$($WrapperConstants.Exception.UnhandledException.Message) $errorKey"
        $errorCode = $WrapperConstants.Exception.UnhandledException.Code
    }

    $exceptionDetailsAsString = Get-ExceptionDetails -ErrorObject $ErrorObject
    ## Exception details are for logging only.
    Write-Log "[$functionName] $customErrorMessage : $exceptionDetailsAsString" -Level "ERROR"

    return $errorCode, $customErrorMessage
}

function Write-Log {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$True, ValueFromPipeline)]
        [System.String] $Message,

        [Parameter(Mandatory=$False)]
        [ValidateSet("INFO","WARNING","ERROR","FATAL","DEBUG","VERBOSE")]
        [System.String] $Level = "INFO",

        [Parameter(Mandatory=$False)]
        [System.String] $LogFile,

        [Parameter(Mandatory=$false)]
        [System.Management.Automation.SwitchParameter] $WriteToConsole
    )

    if ($global:LogFile -and 
        ([System.String]::IsNullOrWhiteSpace($LogFile) -or [System.String]::IsNullOrEmpty($LogFile))) {
        $LogFile = $global:LogFile
    }

    $dateTimeStamp = [System.DateTime]::UtcNow.ToString("u")
    $formattedMessage = "$dateTimeStamp : $Level : $Message"

    if ($WriteToConsole -or (-not $LogFile)) {
        if (Get-Command Trace-Execution -ErrorAction Ignore) {
            Trace-Execution $formattedMessage
        }
        switch($Level.toUpper()) {
            "INFO" {
                        Write-Host $formattedMessage
                        break;
                    }
            "DEBUG" {
                        Write-Debug $formattedMessage
                        break;
                    }
            "VERBOSE" {
                        Write-Verbose $formattedMessage
                        break;
                    }
            "WARNING" {
                        Write-Warning $formattedMessage
                        break;
                    }
            "ERROR" {
                        Write-Error $formattedMessage
                        break;
                    }
            "FATAL" {
                        Write-Error $formattedMessage
                        break;
                    }
        }
    }

    if ($LogFile) {
        Out-File -FilePath $LogFile -InputObject $formattedMessage -Append -Encoding utf8 
    }
}
#endregion Misc Functions

#region Legacy functions
# Legacy functions are needed for compatability with Action plans that still call these functions

function Get-GmaPackageContentPath { "$PSScriptRoot\Legacy" }

## This import is needed only for legacy support.
Import-Module (Join-Path -Path (Get-GmaPackageContentPath) -ChildPath 'GMATenantJsonHelper.psm1') `
    -DisableNameChecking `
    -Verbose:$false `
    -Global

#endregion Legacy functions

#endregion Functions

#region Exports
## Handler functions
Export-ModuleMember -Function Get-ConfigSequenceNumber
Export-ModuleMember -Function Get-HandlerEnvInfo
Export-ModuleMember -Function Get-HandlerHeartBeatFile
Export-ModuleMember -Function Get-HandlerConfigSettings
Export-ModuleMember -Function Get-LogFolderPath
Export-ModuleMember -Function Get-StatusFolderPath
Export-ModuleMember -Function Get-StatusFilePath
Export-ModuleMember -Function Set-HandlerLogFile
Export-ModuleMember -Function Get-HandlerLogFile
Export-ModuleMember -Function Set-Status

## Observability functions
Export-ModuleMember -Function Set-NugetPackageProvider
Export-ModuleMember -Function Set-AclsForGivenPath
Export-ModuleMember -Function Set-ObsStoreRootFolderPath
Export-ModuleMember -Function Get-ExtVersion
Export-ModuleMember -Function Set-ObsNugetStorePath
Export-ModuleMember -Function Confirm-PackageExists
Export-ModuleMember -Function Extract-Package
Export-ModuleMember -Function Install-ObsPackages
Export-ModuleMember -Function Uninstall-ObsPackages
Export-ModuleMember -Function Get-SetupScriptPath
Export-ModuleMember -Function Get-FileLockProcess
Export-ModuleMember -Function Close-ProcessHandles

## Misc functions
Export-ModuleMember -Function Get-ExceptionDetails
Export-ModuleMember -Function Save-Exception
Export-ModuleMember -Function Write-Log

## Legacy functions
Export-ModuleMember -Function Get-GmaPackageContentPath

#endregion Exports
# SIG # Begin signature block
# MIIoRgYJKoZIhvcNAQcCoIIoNzCCKDMCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAcP5CVZpyeRV3Y
# oenZuNbrhlguCV2n+caxkP4XfLq9RqCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0
# Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz
# NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo
# DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3
# a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF
# HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy
# 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC
# Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj
# L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp
# h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3
# cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X
# dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL
# E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi
# u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1
# sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq
# 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb
# DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/
# V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGiYwghoiAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIN1nqx5mAr2SrLdbDpGTgaV/
# U4/Sv+gpnmbHwBgzh6JlMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAn04/LJqGsfEmTGTMRYLgC571dlHtY/6BQ22ulSpfb0t0vB2vwEYFxnMN
# +z1OTIe/iu8WkrxxY8YEz6UOQyHHVt/auTWknIFcnsr02oV303WP25Th3jyoU9LP
# u9CZ+bTmkku0URB9odXnscawgMHgOoMk25OOUttZBtzaQ+W4/t0bgXftJq8NWgzq
# DYon/P/tuO8QcZBQYwQwNVI1OtOeN4LehxMmhgC+ymw5I7v1Q+MRKBq9DzRGgu0z
# dx97sXVf5FH86uHR9KRAdzH/WSmqgNn3jgbuM8tVdwLIsTEWTtd9Gh0SZRNiD9CM
# 8fTcU3qlSITzVEzBUbY8YuORc5K3ZKGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCC
# F5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq
# hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCCWwLn2itajAKjv+Tdmp0ryBmIjNhSnPq3W9PO4TV3B3gIGZ0oHGn7A
# GBMyMDI0MTIwNDE1MDM1NC4xNDdaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# Tjo0MDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAAB/tCowns0IQsBAAEAAAH+MA0G
# CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0
# MDcyNTE4MzExOFoXDTI1MTAyMjE4MzExOFowgdMxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w
# ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjQwMUEt
# MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl
# MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvLwhFxWlqA43olsE4PCe
# gZ4mSfsH2YTSKEYv8Gn3362Bmaycdf5T3tQxpP3NWm62YHUieIQXw+0u4qlay4AN
# 3IonI+47Npi9fo52xdAXMX0pGrc0eqW8RWN3bfzXPKv07O18i2HjDyLuywYyKA9F
# mWbePjahf9Mwd8QgygkPtwDrVQGLyOkyM3VTiHKqhGu9BCGVRdHW9lmPMrrUlPWi
# YV9LVCB5VYd+AEUtdfqAdqlzVxA53EgxSqhp6JbfEKnTdcfP6T8Mir0HrwTTtV2h
# 2yDBtjXbQIaqycKOb633GfRkn216LODBg37P/xwhodXT81ZC2aHN7exEDmmbiWss
# jGvFJkli2g6dt01eShOiGmhbonr0qXXcBeqNb6QoF8jX/uDVtY9pvL4j8aEWS49h
# KUH0mzsCucIrwUS+x8MuT0uf7VXCFNFbiCUNRTofxJ3B454eGJhL0fwUTRbgyCbp
# LgKMKDiCRub65DhaeDvUAAJT93KSCoeFCoklPavbgQyahGZDL/vWAVjX5b8Jzhly
# 9gGCdK/qi6i+cxZ0S8x6B2yjPbZfdBVfH/NBp/1Ln7xbeOETAOn7OT9D3UGt0q+K
# iWgY42HnLjyhl1bAu5HfgryAO3DCaIdV2tjvkJay2qOnF7Dgj8a60KQT9QgfJfwX
# nr3ZKibYMjaUbCNIDnxz2ykCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBRvznuJ9SU2
# g5l/5/b+5CBibbHF3TAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf
# BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz
# L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww
# bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El
# MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF
# BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAiT4NUvO2lw+0
# dDMtsBuxmX2o3lVQqnQkuITAGIGCgI+sl7ZqZOTDd8LqxsH4GWCPTztc3tr8AgBv
# sYIzWjFwioCjCQODq1oBMWNzEsKzckHxAzYo5Sze7OPkMA3DAxVq4SSR8y+TRC2G
# cOd0JReZ1lPlhlPl9XI+z8OgtOPmQnLLiP9qzpTHwFze+sbqSn8cekduMZdLyHJk
# 3Niw3AnglU/WTzGsQAdch9SVV4LHifUnmwTf0i07iKtTlNkq3bx1iyWg7N7jGZAB
# RWT2mX+YAVHlK27t9n+WtYbn6cOJNX6LsH8xPVBRYAIRVkWsMyEAdoP9dqfaZzwX
# GmjuVQ931NhzHjjG+Efw118DXjk3Vq3qUI1re34zMMTRzZZEw82FupF3viXNR3DV
# OlS9JH4x5emfINa1uuSac6F4CeJCD1GakfS7D5ayNsaZ2e+sBUh62KVTlhEsQRHZ
# RwCTxbix1Y4iJw+PDNLc0Hf19qX2XiX0u2SM9CWTTjsz9SvCjIKSxCZFCNv/zpKI
# lsHx7hQNQHSMbKh0/wwn86uiIALEjazUszE0+X6rcObDfU4h/O/0vmbF3BMR+45r
# AZMAETJsRDPxHJCo/5XGhWdg/LoJ5XWBrODL44YNrN7FRnHEAAr06sflqZ8eeV3F
# uDKdP5h19WUnGWwO1H/ZjUzOoVGiV3gwggdxMIIFWaADAgECAhMzAAAAFcXna54C
# m0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp
# Y2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMy
# MjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51
# yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY
# 6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9
# cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN
# 7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDua
# Rr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74
# kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2
# K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5
# TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk
# i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9Q
# BXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3Pmri
# Lq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUC
# BBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJl
# pxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y
# eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA
# YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw
# MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/yp
# b+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulm
# ZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM
# 9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECW
# OKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4
# FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3Uw
# xTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPX
# fx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVX
# VAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGC
# onsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU
# 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEG
# ahC0HVUzWLOhcGbyoYIDWTCCAkECAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# Tjo0MDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAhGNHD/a7Q0bQLWVG9JuGxgLRXseggYMw
# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF
# AAIFAOr6dCgwIhgPMjAyNDEyMDQwNjI0NDBaGA8yMDI0MTIwNTA2MjQ0MFowdzA9
# BgorBgEEAYRZCgQBMS8wLTAKAgUA6vp0KAIBADAKAgEAAgIQ8gIB/zAHAgEAAgIV
# 3jAKAgUA6vvFqAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow
# CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQB1bnVJedws
# V71dgEZ2I4E+NstsP2iVqt75/DZdToSxB5h9UGlR2i/bd3UROjCj3/c8hNAdK0Qy
# 3BC+X+XpnfVCxgNvAPw3AEyXEofTMj/JNKUyGSLG1HtX5K1P0VZAJsAWSRi5woTZ
# z323wCn305Ce19xl2oIMchH2rOyXNgwVZ/cPSLv2ZV4sXvStBBvdm0h6Mml00Vu1
# QENrLXrrqEAjuP5kOAFFdNM4n0WHUN+5/S4dL8emUIwquAqFXagvBNhZ1JgVcHUx
# PCjZd5DZx+xKKCsFS17PHKYqhKH7QDXCq07CWUXNvubuvINKlAkV123HcEhhvPSX
# FC4ABSYo3jjCMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAH+0KjCezQhCwEAAQAAAf4wDQYJYIZIAWUDBAIBBQCgggFKMBoG
# CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgJMd1To/D
# 1a2Dxy3vx9/Kcg+bpCQs+j76CPxm/YH56gEwgfoGCyqGSIb3DQEJEAIvMYHqMIHn
# MIHkMIG9BCARhczd/FPInxjR92m2hPWqc+vGOG1+/I0WtkCstyh0eTCBmDCBgKR+
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB/tCowns0IQsBAAEA
# AAH+MCIEIIO2hrDY4UoAvVTRqJsMmPbvGrI93qiWHaxo8TjxftSCMA0GCSqGSIb3
# DQEBCwUABIICACrjW8QEXa6fgzoURDAXcKFaNHvJwgdC5kJePb4G9AiIDFR+ypcV
# QI2sAnOuQAHZt4/J3d8NBTMpqdFKO7imvpOdU8/jGzHICre5bVWBSqvCEqoCwwvC
# CBs/4cYdiJkkditPeN9+XO97d5ygRsmv4n2KA3MqEbmrHGxATZyUSamP7ZSomTwc
# UbreleMO6aTGlmNqfOU08sED8JUqn9btnTX1YztkPbqO7eJPLJ/SwAUUiDfb/jgD
# IpNQAIltZsnbUm0t76CpVjKbKlXXM8KUKeqTPBlD5eNksGLsaE1kf2tK5a12myf0
# XbwYmOIZ4vI9ul6nJzf3JbbdIrmQN3iz7CIFc/MKstPZV9g2s+7Va4n1vm3efuJa
# 5gvwRkvg+l9DnHpDioqA492in+UiI+YSc5taMePtCfC1Rn6t0hkPpqFJN5UwUQas
# Zme7ClQSXL7Ie7mJEAXqN40gHtvS9ThuM3mwQsD7VChkyPHmqKzIdUDMK+g+31bc
# /XISfZWXAEaamHyqFUUqW6T+4ctUENbQYNV6H4FZLZfHlUgbtA2ngQkwEG7kMVl/
# 606ffZoXtIoehn04R2U6klH5zJctHmj+IxghuZPHJesFLw3gb2eZ10zn9M4veP+A
# l76P4+nC7HaI3S0ZGO2uXitr02S80/Kk3F7yvCYVO1NjZ4QLS6DkKIad
# SIG # End signature block