Obs/bin/ObsAgent/lib/Artifacts/StampTools.psm1

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


<#
.SYNOPSIS
Evaluates the result of Test-AzureStack
 
.DESCRIPTION
Runs Test-AzureStack on the provided endpoint using the current session's credentials
 
.PARAMETER EndpointName
The name of the JEA endpoint to access
 
.PARAMETER ComputerName
The name of the computer on which to invoke the endpoint
 
.OUTPUTS
$True if Test-AzureStack was successful, $False otherwise
#>


function Get-TestAzureStackResult{
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $EndpointName,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ComputerName
    )

    $testAzureStackResult = $false

    Import-Module $PSScriptRoot\SessionTools.psm1

    try
    {
        $jeaSession = Get-JEASession -EndpointName $EndpointName -ComputerName $ComputerName

        # Run Test Azure Stack to generate MAS logs
        Write-Information "Invoking Test-AzureStack on $ComputerName using session $($jeaSession.InstanceId)"

        $testAzureStackResult = Invoke-Command -Session $jeaSession -ScriptBlock { Test-AzureStack -Ignore AzsStampBMCSummary,AzsHostingInfraFWSummary,AzsGpuConfigurationSummary }

        Write-Information  "Test-AzureStack completed"
    }
    catch
    {
        Write-Error  "Test-AzureStack failed with $_" -Verbose
    }
    finally
    {
        if($jeaSession -ne $null) {
            Write-Information "Removing JEA session $($jeaSession.InstanceId)"
            Remove-PSSession $jeaSession
        }
    }

    return $testAzureStackResult
}

<#
.SYNOPSIS
Executes Get-AzureStackLog
 
.DESCRIPTION
Runs Get-AzureStackLog on the provided endpoint using the current session's credentials
 
.PARAMETER EndpointName
The name of the JEA endpoint to access
 
.PARAMETER ComputerName
The name of the computer on which to invoke the endpoint
 
.PARAMETER ErcsNames
The comma seperated names of ERCS computer
 
.PARAMETER OutputPath
The output path for the logs
 
.PARAMETER FromDate
The starting window from which to start collecting logs
 
.PARAMETER ToDate
The ending time window for collecting logs
 
.PARAMETER $BlobServiceSasUri
Blob Service URI for Azure Storage
 
.PARAMETER $OutputSharePath
Local File Share path
 
.PARAMETER $OutputSharePathUserName
Local File Share path user name
 
.PARAMETER $OutputSharePathPassword
Local File Share path User Password
 
.PARAMETER FilterRoles
The roles to filter on
 
.PARAMETER FilterNodes
The nodes to filter on
 
.PARAMETER FilterLogTypes
The log types to collect
 
.PARAMETER FilterResourceProviders
The resource providers to filter on
 
.PARAMETER AlertBased
Whether the log collection is alert based
 
.OUTPUTS
A result object containing the output path of the logs
#>

function Get-AzureStackLogResult
{
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $EndpointName,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ComputerName,

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

        [Parameter(Mandatory=$false)]
        [nullable[DateTime]]
        $FromDate,

        [Parameter(Mandatory=$false)]
        [nullable[DateTime]]
        $ToDate,

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

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

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

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

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

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

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

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

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

        [Parameter(Mandatory=$false)]
        [bool]
        $AlertBased
    )

    $getAzureStackLogResult = $false;
    $uploadErrors = @()
    $uploadSizeInMb = 0
    $uploadNumberOfFiles = 0
    $uploadDirectory = $null

    Import-Module $PSScriptRoot\SessionTools.psm1

    $customerConfigPath = "$env:SystemDrive\ProgramData\Microsoft\AzureStack\config.xml"

    try {

        # Default value for log collection window start, if not specified, should be 4 hours ago
        if ($FromDate -eq $null)
        {
            $FromDate = (Get-Date).AddHours(-4)
            Write-Information "FromDate parameter not specified. Setting to default value $FromDate"
        }

        # Default value for log collection window end, if not specified, should be now
        if ($ToDate -eq $null)
        {
            $ToDate = Get-Date
            Write-Information "ToDate parameter not specified. Setting to default value $ToDate"
        }

        $getAzureStackLogParameters = @{
            FromDate = $FromDate
            ToDate = $ToDate
            FilterByRole = $FilterRoles
            FilterByNode = $FilterNodes
            FilterByLogType = $FilterLogTypes
            FilterByResourceProvider = $FilterResourceProviders
            AlertBased = $AlertBased
        }

        # Provide the customer configuration file to Get-AzureStackLog if it exists
        Write-Information "Checking if config.xml exists: $customerConfigPath"

        if (Test-Path $customerConfigPath)
        {
            Write-Information "$customerConfigPath exists. Adding to Get-AzureStackLog parameters"
            $getAzureStackLogParameters.Add("CustomerConfigurationFilePath", $customerConfigPath)
        }
        else {
            # Considering this a non-terminating error
            Write-Error "$customerConfigPath does not exist. Continuing without a configuration file"
        }

        if (![string]::IsNullOrEmpty($BlobServiceSasUri))
        {
            Write-Information "Using Blob Service Uri for upload $BlobServiceSasUri"
            $getAzureStackLogParameters.Add("OutputSasUri", $BlobServiceSasUri)
            $jeaSession = Get-JEASession -EndpointName $EndpointName -ComputerName $ComputerName
        }
        elseif (![string]::IsNullOrEmpty($OutputSharePath))
        {
            Write-Information "Using Share Path for upload $OutputSharePath"
            $getAzureStackLogParameters.Add("OutputSharePath", $OutputSharePath)

            Write-Information "Adding share creds"
            $password = ConvertTo-SecureString $OutputSharePathPassword -AsPlainText -Force
            $cred = New-Object -TypeName System.Management.Automation.PSCredential ($OutputSharePathUserName, $password)
            $getAzureStackLogParameters.Add("OutputShareCredential", $cred)

            Write-Information "Checking share access locally first"
            $checkStatus = Test-SharePathAccess -SharePath $OutputSharePath -ShareCreds $cred
            Write-Information "Checking share access locally returned: $checkStatus"
            if(!$checkStatus)
            {
                Write-Information "Getting PEP session from ERCS: $($ErcsNames)"
                $jeaSession = Get-EcePepSession -ErcsComputerNames $ErcsNames
            }
            else
            {
                Write-Information "Getting PEP session from SRNG: $ComputerName"
                $jeaSession = Get-JEASession -EndpointName $EndpointName -ComputerName $ComputerName
            }
        }
        else 
        {
            Write-Information "Using Autonomous Log Archival Location for upload $OutputPath"
             $jeaSession = Get-JEASession -EndpointName $EndpointName -ComputerName $ComputerName
             $getAzureStackLogParameters.Add("OutputPath", $OutputPath)
        }

        $getAzureStackLogCommand = 
        {
            Get-AzureStackLog 6>&1 @using:getAzureStackLogParameters
        }


        Write-Information "Invoking Get-AzureStackLog using session $($jeaSession.InstanceId)"
        $azureStackLog = Invoke-Command -Session $jeaSession -ScriptBlock $getAzureStackLogCommand

        Write-Information "Completed Get-AzureStackLog"
        Write-Information "Collecting diagnostic data from Get-AzureStackLog"

        if($azureStackLog -ne $null) 
        {
            $uploadDetails = Get-AzureStackLogDetails $azureStackLog

            $uploadErrors = $uploadDetails.UploadErrorDetails
            $uploadSizeInMb = $uploadDetails.UploadSize
            $uploadNumberOfFiles = $uploadDetails.UploadNumberOfFiles
            $uploadDirectory = $uploadDetails.UploadDirectory
        }
        else 
        {
            Write-Error "Get-AzureStackLog failed to produce an output stream. Diagnostic data will not be collected."
        }
    }
    catch {
        Write-Error "Get-AzureStackLog failed on $ComputerName with $_"
    }
    finally {
        if($jeaSession -ne $null) {
            Write-Information "Removing JEA session $($jeaSession.InstanceId)"
            Remove-PSSession $jeaSession
        }
    }

    # If we have successfully uploaded files to the storage account consider this run a succes
    if (($uploadSizeInMb -gt 0) -and ($uploadNumberOfFiles -gt 0))
    {
        Write-Information "Get-AzureStackLog successfully uploaded logs to $uploadDirectory"
        $getAzureStackLogResult = $true
    }

    # TODO: remove output path key
    return ConvertTo-Json @{ 
        LogResult = $getAzureStackLogResult
        OutputPath = $uploadDirectory
        UploadDirectory = $uploadDirectory
        UploadSizeInMb = $uploadSizeInMb
        UploadNumberOfFiles = $uploadNumberOfFiles
        UploadErrors = $uploadErrors
    }
}

# Extracts a valid folder name from an InformationRecord
function Find-FolderNameMatch {
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $LogDetail,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $LogDetailsLabel
    )

    Write-Information "Finding match for $LogDetailsLabel"

    $folderMatch = "[a-zA-Z0-9](?:[a-zA-Z0-9_ -]*[a-zA-Z0-9]){1}$"
    $folderName = ($LogDetail | Select-String -pattern $folderMatch).Matches[0].Value

    if($folderName -eq $null)
    {
        Write-Error "Unable to find a valid folder name"
    }

    return $folderName
}

# Extracts the latest numerical match from an InformationRecord
function Find-LatestNumericalMatch {
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [PSObject[]]
        $LogDetails,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $LogDetailsLabel
    )

    $measureMatch = "\d+(\.\d+)?$"
    $maximum = 0

    Write-Information "Finding the latest match for $LogDetailsLabel"

    $measureMatches = $LogDetails `
    | Select-String -pattern $measureMatch `
    | ForEach {$_.Matches } `
    | ForEach {[decimal]$_.Value}

    #report the maximum in case multiple log messages exist
    $latestDetail = $measureMatches | Measure-Object -Maximum
    if ($latestDetail -ne $null) {
    $maximum = $latestDetail.Maximum
    }
    else {
        Write-Error "Unable to find a maximum for $LogDetailsLabel"
    }

    return $maximum
}

# Retrieves diagnostic data from a Get-AzureStackLog output stream
function Get-AzureStackLogDetails{
    Param(
       [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [PSObject[]]
        $azureStackLog
    )

    # Get-AzureStackLog output tags
    $sizeTag = "LogCollectionUploadFileSizeInMb"
    $numberOfFilesTag = "LogCollectionUploadNumberOfFiles"
    $errorDetailsTag = "LogCollectionUploadErrorDetails"
    $directoryTag = "LogCollectionUploadDirectory"

    # Data capture matches
    $errorDetailsMatch = "$errorDetailsTag.*$"
    $sizeMatch = "FileSizeInMb.*";
    $numberOfFilesMatch = "NumberOfFiles.*"
    $directoryMatch = "$directoryTag.*"

    $logCollectionMatch = "($sizeTag|$numberOfFilesTag|$errorDetailsTag|$directoryTag).*"

    # Default values
    $errorDetails = @()
    $uploadSize = 0
    $uploadNumberOfFiles = 0
    $uploadDirectory = $null

    try 
    {
        # Search the output stream for the output tags
        $uploadDetails = $azureStackLog | Where-Object { $_.MessageData -match $logCollectionMatch }

        if ($uploadDetails -ne $null) 
        {
            # Find all upload error details if present
            $errorDetails += $uploadDetails `
            | Select-String -pattern $errorDetailsMatch  `
            | ForEach { $_.Matches } `
            | ForEach { $_.Value }

            # Find the latest record for upload size
            $uploadSizeMatches = $uploadDetails | Where-Object { $_.MessageData -match $sizeMatch }
            if ($uploadSizeMatches -ne $null)
            {
                $uploadSize = [int](Find-LatestNumericalMatch $uploadSizeMatches $sizeTag)
                Write-Information "${sizeTag}: $uploadSize"
            }
            else 
            {
                Write-Error "$sizeTag details were not present"
            }

            # Find the latest record for the number of files uploaded
            $uploadNumberOfFilesMatches = $uploadDetails | Where-Object { $_.MessageData -match $numberOfFilesMatch }
            if ($uploadNumberOfFilesMatches -ne $null)
            {
                $uploadNumberOfFiles = Find-LatestNumericalMatch $uploadNumberOfFilesMatches $numberOfFilesTag
                Write-Information "${numberOfFilesTag}: $uploadNumberOfFiles"
            }
            else
            {
                Write-Error "$numberOfFilesTag details were not present"
            }

            # Find the upload directory
            $uploadDirectoryMatches = $uploadDetails | Where-Object { $_.MessageData -match $directoryMatch }
            if ($uploadDirectoryMatches -ne $null)
            {
                 $uploadDirectory = Find-FolderNameMatch $uploadDirectoryMatches[0] $directoryTag
                Write-Information "${directoryTag}: $uploadDirectory"
            }
            else{
                Write-Error "$uploadDirectoryTag details were not present"
            }
        }
        else {
            Write-Error "Log collection upload details were not present in the output provided."
        }
    }
    catch
    {
        Write-Error "An error occured while attempting to collect log collection upload details: $_"
    }

    return @{ 
        UploadErrorDetails = $errorDetails
        UploadSize = $uploadSize
        UploadNumberOfFiles = $uploadNumberOfFiles
        UploadDirectory = $uploadDirectory
    }
}

# Checks if share is reachable from given node
function Test-SharePathAccess {
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SharePath,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $ShareCreds
    )

    Write-Information "Test-ShareAccessFromNode: Testing SharePath $SharePath access locally"
    $status = $true
    try
    {
        $drive = Get-PSDrive | Where-Object DisplayRoot -eq $SharePath
        if ($null -eq $drive)
        {
            Write-Information "Creating new PS drive: $SharePath."
            $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ShareCreds.Password)
            $password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
            $null = net use * $SharePath /user:$($ShareCreds.UserName) $password
            $drive = Get-PSDrive | Where-Object DisplayRoot -eq $SharePath
            if(!$drive)
            {
                throw "net use is not successful"
            }
        }

        Write-Information "Net Use did not return null. So considering success: $($netuseResult)"
    }
    catch
    {
        Write-Information "Failed to access the share. $_"
        $status = $false
    }
    finally
    {
        Write-Information "Test-ShareAccessFromNode : Cleanup mapped drive"
        if($drive)
        {
            $driveName = $drive.Root -replace "\\",""
            Write-Information "Test-ShareAccessFromNode : Cleaning mapped drive: $driveName"
            $null = net use $driveName /delete /yes
            Write-Information "Test-ShareAccessFromNode : Successfully delete mapped drive"
        }
    }

    $status
}

# Available commands
Export-ModuleMember -Function Get-TestAzureStackResult
Export-ModuleMember -Function Get-AzureStackLogResult
# SIG # Begin signature block
# MIInzgYJKoZIhvcNAQcCoIInvzCCJ7sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDYaQv7Gh3Wj9OQ
# byFBNOmQggQWNZy+m8z6kLS0crndTqCCDYUwggYDMIID66ADAgECAhMzAAADri01
# UchTj1UdAAAAAAOuMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwODU5WhcNMjQxMTE0MTkwODU5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQD0IPymNjfDEKg+YyE6SjDvJwKW1+pieqTjAY0CnOHZ1Nj5irGjNZPMlQ4HfxXG
# yAVCZcEWE4x2sZgam872R1s0+TAelOtbqFmoW4suJHAYoTHhkznNVKpscm5fZ899
# QnReZv5WtWwbD8HAFXbPPStW2JKCqPcZ54Y6wbuWV9bKtKPImqbkMcTejTgEAj82
# 6GQc6/Th66Koka8cUIvz59e/IP04DGrh9wkq2jIFvQ8EDegw1B4KyJTIs76+hmpV
# M5SwBZjRs3liOQrierkNVo11WuujB3kBf2CbPoP9MlOyyezqkMIbTRj4OHeKlamd
# WaSFhwHLJRIQpfc8sLwOSIBBAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUhx/vdKmXhwc4WiWXbsf0I53h8T8w
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwMTgzNjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AGrJYDUS7s8o0yNprGXRXuAnRcHKxSjFmW4wclcUTYsQZkhnbMwthWM6cAYb/h2W
# 5GNKtlmj/y/CThe3y/o0EH2h+jwfU/9eJ0fK1ZO/2WD0xi777qU+a7l8KjMPdwjY
# 0tk9bYEGEZfYPRHy1AGPQVuZlG4i5ymJDsMrcIcqV8pxzsw/yk/O4y/nlOjHz4oV
# APU0br5t9tgD8E08GSDi3I6H57Ftod9w26h0MlQiOr10Xqhr5iPLS7SlQwj8HW37
# ybqsmjQpKhmWul6xiXSNGGm36GarHy4Q1egYlxhlUnk3ZKSr3QtWIo1GGL03hT57
# xzjL25fKiZQX/q+II8nuG5M0Qmjvl6Egltr4hZ3e3FQRzRHfLoNPq3ELpxbWdH8t
# Nuj0j/x9Crnfwbki8n57mJKI5JVWRWTSLmbTcDDLkTZlJLg9V1BIJwXGY3i2kR9i
# 5HsADL8YlW0gMWVSlKB1eiSlK6LmFi0rVH16dde+j5T/EaQtFz6qngN7d1lvO7uk
# 6rtX+MLKG4LDRsQgBTi6sIYiKntMjoYFHMPvI/OMUip5ljtLitVbkFGfagSqmbxK
# 7rJMhC8wiTzHanBg1Rrbff1niBbnFbbV4UDmYumjs1FIpFCazk6AADXxoKCo5TsO
# zSHqr9gHgGYQC2hMyX9MGLIpowYCURx3L7kUiGbOiMwaMIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAOuLTVRyFOPVR0AAAAA
# A64wDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIE8J
# zdgMuK4CjUrE6OmRbjx1rgJDx4jk6VBAspXDkkj3MEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEACaLcj0rCEc2GcaEpk/VlsivYkMTTIbwHFWuj
# 7kkNuitrrvtme5bpjQl/XXGHJWN65mjjnnMrqny3eBYajEUHpRmdG6VEZTx5ZbGs
# aVk8jPOlS4hq9DpOExlZAawzTEfpcgPUHQMX7aJataV9ruJ+gYCUuikUqFlQuYZR
# L15YrFUZL1/IdlytNkTgdz2MG9ZoK3JrBbzUVXbp5vtI7WipL/OUprM7LDKvngnG
# 0cgxmVdDrbAtSz4URpZNGN0cyd7w65CWtupIS8+M28ACOZ+KPBU+7Wuzb/hSo5rV
# UwzENTx6FMl5p7DpxfObdW0hi9OIubXsqRTngAqIXGLQHcKKDqGCFykwghclBgor
# BgEEAYI3AwMBMYIXFTCCFxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFZBgsqhkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCDSM3Exuvo0l9t9eGteUbiDWG91iS367jlW
# 7AvNcMf4dQIGZdXm1gQ+GBMyMDI0MDMxMTE4MTcxNS4wOTRaMASAAgH0oIHYpIHV
# MIHSMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsT
# HVRoYWxlcyBUU1MgRVNOOjA4NDItNEJFNi1DMjlBMSUwIwYDVQQDExxNaWNyb3Nv
# ZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAHajtXJ
# WgDREbEAAQAAAdowDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# UENBIDIwMTAwHhcNMjMxMDEyMTkwNjU5WhcNMjUwMTEwMTkwNjU5WjCB0jELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9z
# b2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjowODQyLTRCRTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJOQ
# Bgh2tVFR1j8jQA4NDf8bcVrXSN080CNKPSQo7S57sCnPU0FKF47w2L6qHtwm4EnC
# lF2cruXFp/l7PpMQg25E7X8xDmvxr8BBE6iASAPCfrTebuvAsZWcJYhy7prgCuBf
# 7OidXpgsW1y8p6Vs7sD2aup/0uveYxeXlKtsPjMCplHkk0ba+HgLho0J68Kdji3D
# M2K59wHy9xrtsYK+X9erbDGZ2mmX3765aS5Q7/ugDxMVgzyj80yJn6ULnknD9i4k
# UQxVhqV1dc/DF6UBeuzfukkMed7trzUEZMRyla7qhvwUeQlgzCQhpZjz+zsQgpXl
# PczvGd0iqr7lACwfVGog5plIzdExvt1TA8Jmef819aTKwH1IVEIwYLA6uvS8kRdA
# 6RxvMcb//ulNjIuGceyykMAXEynVrLG9VvK4rfrCsGL3j30Lmidug+owrcCjQagY
# mrGk1hBykXilo9YB8Qyy5Q1KhGuH65V3zFy8a0kwbKBRs8VR4HtoPYw9z1DdcJfZ
# BO2dhzX3yAMipCGm6SmvmvavRsXhy805jiApDyN+s0/b7os2z8iRWGJk6M9uuT24
# 93gFV/9JLGg5YJJCJXI+yxkO/OXnZJsuGt0+zWLdHS4XIXBG17oPu5KsFfRTHREl
# oR2dI6GwaaxIyDySHYOtvIydla7u4lfnfCjY/qKTAgMBAAGjggFJMIIBRTAdBgNV
# HQ4EFgQUoXyNyVE9ZhOVizEUVwhNgL8PX0UwHwYDVR0jBBgwFoAUn6cVXQBeYl2D
# 9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3Nv
# ZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy
# MDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1l
# LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUB
# Af8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQAD
# ggIBALmDVdTtuI0jAEt41O2OM8CU237TGMyhrGr7FzKCEFaXxtoqk/IObQriq1ca
# HVh2vyuQ24nz3TdOBv7rcs/qnPjOxnXFLyZPeaWLsNuARVmUViyVYXjXYB5DwzaW
# ZgScY8GKL7yGjyWrh78WJUgh7rE1+5VD5h0/6rs9dBRqAzI9fhZz7spsjt8vnx50
# WExbBSSH7rfabHendpeqbTmW/RfcaT+GFIsT+g2ej7wRKIq/QhnsoF8mpFNPHV1q
# /WK/rF/ChovkhJMDvlqtETWi97GolOSKamZC9bYgcPKfz28ed25WJy10VtQ9P5+C
# /2dOfDaz1RmeOb27Kbegha0SfPcriTfORVvqPDSa3n9N7dhTY7+49I8evoad9hdZ
# 8CfIOPftwt3xTX2RhMZJCVoFlabHcvfb84raFM6cz5EYk+x1aVEiXtgK6R0xn1wj
# MXHf0AWlSjqRkzvSnRKzFsZwEl74VahlKVhI+Ci9RT9+6Gc0xWzJ7zQIUFE3Jiix
# 5+7KL8ArHfBY9UFLz4snboJ7Qip3IADbkU4ZL0iQ8j8Ixra7aSYfToUefmct3dM6
# 9ff4Eeh2Kh9NsKiiph589Ap/xS1jESlrfjL/g/ZboaS5d9a2fA598mubDvLD5x5P
# P37700vm/Y+PIhmp2fTvuS2sndeZBmyTqcUNHRNmCk+njV3nMIIHcTCCBVmgAwIB
# AgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UE
# BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc
# BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0
# IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1
# WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O
# 1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZn
# hUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t
# 1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxq
# D89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmP
# frVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSW
# rAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv
# 231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zb
# r17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYcten
# IPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQc
# xWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17a
# j54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQAB
# MCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQU
# n6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEw
# QTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9E
# b2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQB
# gjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/
# MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJ
# oEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01p
# Y1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYB
# BQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9v
# Q2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3h
# LB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x
# 5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74p
# y27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1A
# oL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbC
# HcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB
# 9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNt
# yo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3
# rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcV
# v7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A24
# 5oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lw
# Y1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB
# 0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMk
# TWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1U
# aGFsZXMgVFNTIEVTTjowODQyLTRCRTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0
# IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAQqIfIYljHUbNoY0/
# wjhXRn/sSA2ggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAN
# BgkqhkiG9w0BAQUFAAIFAOmZbs4wIhgPMjAyNDAzMTExOTUxNDJaGA8yMDI0MDMx
# MjE5NTE0MlowdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA6ZluzgIBADAHAgEAAgId
# CzAHAgEAAgISMjAKAgUA6ZrATgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEE
# AYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GB
# AIi6AJjK3X06yU7I1N7cNeDL7Y4KHnoV+edLLpe1LVX2wHKwkW/cS/ZYNJv5jRVs
# r94Ocx00hXLSnxs6bOI1JHYttMOwPCrwbadN6ka0G6EdPH84Qpxjbnk01kGKEBx+
# JWKnOoKnrRDLR/wrNxGgXO1fvMTP1jrdbADoa/PistERMYIEDTCCBAkCAQEwgZMw
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAHajtXJWgDREbEAAQAA
# AdowDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRAB
# BDAvBgkqhkiG9w0BCQQxIgQgzks4DChTJTF5GMGh0TICUfBNZP+Q7WD7TFHrgzyI
# XpUwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCAipaNpYsDvnqTe95Dj1C09
# 020I5ljibrW/ndICOxg9xjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD
# QSAyMDEwAhMzAAAB2o7VyVoA0RGxAAEAAAHaMCIEIOVlnbiuMr9qn8Ud96DRKf8K
# fVfppLN1O1DuKcs5MV7cMA0GCSqGSIb3DQEBCwUABIICAIX/w37sk4gaiLORKlPT
# xU+Kdpgm+vTyMGI86rWDsWkcWGn2lH5mKgMgL+sYIjCAHWIswxKANkYFu47PZPkl
# uW6ystmH0CdR6V+6XG57D4yUFszjYwLb0BY0viUeSQJtfG4PZPfSjyUFb9+e6m1b
# YbH0NXJriE+pzaUGBeHzUTMeUxfchS8hqYQVrkTE0iUseIh6hw3Ywekr44aoQdXN
# K7cMwxXxLfcyUt8wPgC1xbIH509zlHDcxBq3abzSTKzsjM9/I7r598TOFUW86eyV
# +LfbOWjW+Bj+qCTpWsCCC0Wz01Bsv/N3WPm6IfFIVDoJqS9jcEXsrceI5eiUIV/x
# l8zIwZKZ9xreoeqMQQQnQscSQxmGl4eOQmerRkt5YaYMbJfMNQeQDvpsyARmevva
# qrxqiOAt9NuEnwnHuIx48gBAZ4SJ+FRHT9wYNI+k43Qgf1GC/PWJbiQvkb/0OFYF
# YB4rhKRVmTShn2lJQndx153mgn++8BzVIFmGTYqIFl1gGg/4oNvpGnnZzInJ4ZF3
# aufdRBFFbtz8TezaQO7yMmd86zmnRj6TtQs8OT/8HcTX3SM1dwft9qZIuWvv6+It
# 9UST+8PCP7UowTqZOwsGj0PR0+RqL870O1cDDimR4Kkvu83ym5Rj5DtYes7sSxQm
# pbnS5wga6zlPsb0rfDxWaxg6
# SIG # End signature block