AzStackHciSBEHealth/AzStackHci.SBEHealth.Helpers.psm1

Import-LocalizedData -BindingVariable locSbeTxt -FileName AzStackHci.SBEHealth.Strings.psd1

function Get-ASArtifactPathLite
{
    <#
    .SYNOPSIS
    Returns the nuget content path. Same as normal Get-ArtifactPath except it doesn't use Trace-Execution or try to locate on ProductVHD.
 
    .DESCRIPTION
    Calculates and returns the path to the nuget content folder for the specified nuget on the current infrastructure vm environment. All product artifacts are
    exposed to the infrastructure vms, however the location is not fixed, this method is used to find the desired content.
 
    .EXAMPLE
    $Path = Get-ASArtifactPathLite -NugetName "Microsoft.Diagnostics.Tracing.EventSource.Redist"
 
    .PARAMETER NugetName
    The full name of the nuget without version information.
 
    .PARAMETER Version
    The optional version number of the package.
 
    #>

    [CmdletBinding()]
    PARAM
    (
        [Parameter(Position=0, Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $NugetName,

        [Parameter(Mandatory=$false)]
        [System.String]
        $Version = $null
    )
    PROCESS
    {
        $VerbosePreference = [System.Management.Automation.ActionPreference]::Continue
        Import-Module PackageManagement -DisableNameChecking -Verbose:$false | Out-Null

        $nugetProvider = Get-PackageProvider | Where-Object { $_.Name -eq "Nuget" }

        if ($nugetProvider -eq $null)
        {
            Log-Info -Message "Attempting to install nuget package provider."
            Install-PackageProvider nuget -Force -ForceBootstrap
        }

        $drivePath = "$env:SystemDrive\NugetStore"

        if (Test-Path -Path $drivePath)
        {
            if ($Version)
            {
                $package = Get-Package -Name $NugetName -Destination $drivePath -ErrorAction Stop -RequiredVersion $Version -ProviderName Nuget
            }
            else
            {
                $package = Get-Package -Name $NugetName -Destination $drivePath -ErrorAction Stop -ProviderName Nuget
            }

            Log-Info -Message "Get-Package returned with Success:$($?)"
        }

        if ($package -eq $null)
        {
            throw "Could not find package $NugetName on source $drivePath."
        }

        Log-Info -Message  "Found package $($package.Name) with version $($package.Version) at $($package.Source)."

        return [System.IO.Path]::GetDirectoryName($package.Source);
    }
}

function Copy-SBEContentToSession
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$PackagePath,

        [Parameter(Mandatory=$true)]
        [System.Management.Automation.Runspaces.PSSession]$Session,

        [Parameter(Mandatory=$true)]
        [string]$DestPath,

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

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

        [Parameter(Mandatory=$false)]
        [switch]$SkipNugetCopy
    )

    $copyItems = @()

    if ($false -eq $SkipNugetCopy.IsPresent)
    {
        try
        {
            # Note - this function only works on the seed node as only it will have NugetStore bootstrapped.
            $sbeConfig = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.SBEConfiguration"
            $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE"
        }
        catch
        {
            Log-Info -Message $locSbeTxt.SkipNoNugetPath
        }
        $sbeConfigDest = $sbeConfig
        $sbeRoleDest = $sbeRoleNuget
    }

    $targetIsCurrentNode = $false
    [array]$myIP = (Get-NetIPAddress -AddressFamily IPv4).IPAddress
    if (($Session.ComputerName -in $myIP) -or (($Session.ComputerName -eq $env:ComputerName)))
    {
        $targetIsCurrentNode = $true
        Log-Info -Message $locSbeTxt.TargetIsThisNode
    }

    if ($targetIsCurrentNode -and ($PackagePath.TrimEnd("\") -eq $DestPath.TrimEnd("\")))
    {
        Log-Info -Message ($locSbeTxt.SkipSameCopy -f $PackagePath,$Session.ComputerName)
    }
    else
    {
        Log-Info -Message ($locSbeTxt.WillCopyToSession -f 'SBE package content',$PackagePath,$DestPath,$Session.ComputerName)
        $copyItems += @{Source=$PackagePath;Destination=$DestPath}
    }
    if (-not([string]::IsNullOrWhitespace($sbeConfigDest)))
    {
        if ($targetIsCurrentNode -and ($sbeConfig.TrimEnd("\") -eq $sbeConfigDest.TrimEnd("\")))
        {
            Log-Info -Message ($locSbeTxt.SkipSameCopy -f $sbeConfig,$Session.ComputerName)
        }
        else
        {
            Log-Info -Message ($locSbeTxt.WillCopyToSession -f 'SBEConfiguration',$sbeConfig,$sbeConfigDest,$Session.ComputerName)
            $copyItems += @{Source=$sbeConfig;Destination=$sbeConfigDest}
        }
    }
    if (-not([string]::IsNullOrWhitespace($sbeRoleDest)))
    {
        if ($targetIsCurrentNode -and ($sbeRoleNuget.TrimEnd("\") -eq $sbeRoleDest.TrimEnd("\")))
        {
            Log-Info -Message ($locSbeTxt.SkipSameCopy -f $sbeRoleNuget,$Session.ComputerName)
        }
        else
        {
            Log-Info -Message ($locSbeTxt.WillCopyToSession -f 'SBE.Role nuget',$sbeRoleNuget,$sbeRoleDest,$Session.ComputerName)
            $copyItems += @{Source=$sbeRoleNuget;Destination=$sbeRoleDest}
        }
    }

    [array]$exclude = @()
    if ($ExcludeFiles.Count -ne 0)
    {
        $exclude += $ExcludeFiles
    }
    if ($ExcludeDirs.Count -ne 0)
    {
        $exclude += $ExcludeDirs
    }

    $copySuccess = $true
    foreach ($item in $copyItems)
    {
        Log-Info -Message ($locSbeTxt.CopyToSession -f $item.Source,$item.Destination,$Session.ComputerName) -Type Info
        if ($exclude.Count -gt 0)
        {
            [array]$filesToCopy = (Get-Item -Path "$($item.Source)\*" -Exclude $exclude).FullName
        }
        else
        {
            [array]$filesToCopy = (Get-Item -Path "$($item.Source)\*").FullName
        }

        # Make sure target folder exists on remote session
        $destFolderScript = {
            if ($false -eq (Test-Path -Path $using:item.Destination))
            {
                $null = New-Item -ItemType Directory -Force -Path $using:item.Destination
            }
        }
        $null = Invoke-Command -Session $Session -ScriptBlock $destFolderScript

        # Try to copy files over the PSSession, if this fails we will fallback to robocopy via PSDrive
        try
        {
            Copy-Item -Path $filesToCopy -Destination $item.Destination -Recurse -Force -ToSession $Session -ErrorAction Stop
        }
        catch
        {
            $copySuccess = $false
            Log-Info -Message ($locSbeTxt.CopyItemFailed -f $PSitem.Exception.Message) -Type Info
        }
    }

    return $copySuccess
}

function Copy-SBEContentLocalToNode
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$PackagePath,

        [Parameter(Mandatory=$true)]
        [string]$TargetNodeName,

        [Parameter(Mandatory=$true)]
        [string]$DestPath,

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

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

        [Parameter(Mandatory=$false)]
        [switch]$SkipNugetCopy,

        [PSCredential]$Credential
    )

    $copyItems = @()

    # Note - this function only works on the seed node as only it will have NugetStore bootstrapped.
    $sbeConfig = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.SBEConfiguration"
    $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE"
    if ($Credential)
    {
        Log-Info ("Username is '{0}'" -f $Credential.UserName)
    }
    else
    {
        # Credential is not needed if the target is the seed node (in this case it is copying to itself)
        Log-Info "Credential was not provided"
    }

    # Check if the copy destination is the current node
    $targetIsCurrentNode = $false
    if ($env:ComputerName -eq $TargetNodeName)
    {
        Log-Info "Current node ComputerName matched TargetNodeName"
        $targetIsCurrentNode = $true
    }
    else
    {
        $thisComputerName = $null
        $dnsName = ((Resolve-DnsName -Name $TargetNodeName -ErrorAction SilentlyContinue) | Select-Object -First 1)
        if ($dnsName.NameHost)
        {
            # Case when IP is resolved
            $thisComputerName = ($dnsName.NameHost).Split('.')[0]
            if ($env:ComputerName -eq $thisComputerName)
            {
                Log-Info "Current node ComputerName matched Resolve-DnsName by IP address"
                $targetIsCurrentNode = $true
            }
        }
        elseif ($dnsName.Name)
        {
            # Case when hostname is resolved
            $thisComputerName = ($dnsName.Name).Split('.')[0]
            if ($env:ComputerName -eq $thisComputerName)
            {
                Log-Info "Current node ComputerName matched Resolve-DnsName by hostname"
                $targetIsCurrentNode = $true
            }
        }
        else
        {
            # No DNS match so try IP address instead
            [array]$myIP = (Get-NetIPAddress).IPAddress
            if ($TargetNodeName -in $myIP)
            {
                Log-Info "TargetNodeName was matched in the current node myIP list"
                $targetIsCurrentNode = $true
            }
        }
    }
    if ($true -eq $targetIsCurrentNode)
    {
        $finalDestPath = $DestPath
        Log-Info "Target node is the current node, so use local path as destination: $finalDestPath"
    }
    else
    {
        Log-Info "Target node is a remote node, so need to map PSDrive(s)"
        Get-PSDrive -Name SBE -ErrorAction SilentlyContinue | Remove-PSDrive -Force
        if ($DestPath -match '^(\w):')
        {
            $destRoot = '\\' + $TargetNodeName + '\' + $Matches[1] + '$'
        }
        else
        {
            throw "Unable to determine proper path to copy SBE. Dest structure is unexpected '$DestPath'."
        }
        $systemDriveRoot = '\\' + $TargetNodeName + '\' + ($env:SystemDrive ).Replace(':','$')

        $retry = $true
        $maxRetry = 4
        $attempt = 0
        while ($true -eq $retry)
        {
            $attempt++
            Log-Info "Map New-PSDrive to '$($destRoot)', attempt '$($attempt)/$($maxRetry)'"
            try
            {
                $destDrv = New-PSDrive -Credential $Credential -Name SBECACHE -PSProvider FileSystem -Root $destRoot -ErrorAction SilentlyContinue
            }
            catch
            {
                $errMessage = $PSItem.Exception.Message
                Log-Info "New-PSDrive failed with exception: $($errMessage)"
            }
            $found = Get-PSDrive -Name SBECACHE
            if ($found -and $found.Root -eq $destRoot)
            {
                $errMessage = ''
                $retry = $false
            }
            else
            {
                if ($attempt -ge $maxRetry)
                {
                    throw "Failed to map New-PSDrive after '$($attempt)' attempts. Exception: '$($errMessage)'"
                    $retry = $false
                }
                Start-Sleep -Seconds 15
            }
        }
        # Change the destination path to use the mounted drive letter...
        $finalDestPath = $DestPath -replace '^\w:', $destDrv.Root
        Log-Info "Changing DestPath from $DestPath to $finalDestPath."

        if ($destRoot -ne $systemDriveRoot -and $false -eq $SkipNugetCopy.IsPresent)
        {
            $retry = $true
            $maxRetry = 4
            $attempt = 0
            while ($true -eq $retry)
            {
                $attempt++
                Log-Info "Map New-PSDrive to '$($systemDriveRoot)', attempt '$($attempt)/$($maxRetry)'"
                try
                {
                    $sysDrv = New-PSDrive -Credential $Credential -Name SBESYSROOT -PSProvider FileSystem -Root $systemDriveRoot -ErrorAction SilentlyContinue
                }
                catch
                {
                    $errMessage = $PSItem.Exception.Message
                    Log-Info "New-PSDrive failed with exception: $($errMessage)"
                }
                $found = Get-PSDrive -Name SBESYSROOT
                if ($found -and $found.Root -eq $systemDriveRoot)
                {
                    $errMessage = ''
                    $retry = $false
                }
                else
                {
                    if ($attempt -ge $maxRetry)
                    {
                        throw "Failed to map New-PSDrive after '$($attempt)' attempts. Exception: '$($errMessage)'"
                        $retry = $false
                    }
                    Start-Sleep -Seconds 15
                }
            }
            $sbeConfigDest = $sbeConfig.Replace($env:SystemDrive,$sysDrv.Root)
            $sbeRoleDest = $sbeRoleNuget.Replace($env:SystemDrive,$sysDrv.Root)
        }
        elseif ($false -eq $SkipNugetCopy.IsPresent)
        {
            Log-Info "Using the destDrv mount to copy Config and Role Nugets."
            $sbeConfigDest = $sbeConfig.Replace($env:SystemDrive,$destDrv.Root)
            $sbeRoleDest = $sbeRoleNuget.Replace($env:SystemDrive,$destDrv.Root)
        }
        else
        {
            Log-Info "Skipping sysDrv mount - we don't need to copy Config or Role Nugets."
            # This is typical of post-deploy OperationType like "Update" where the SBE.Role nuget is already available on all nodes and the SBEConfiguration nuget is not needed.
        }
    }

    Log-Info -Message ($locSbeTxt.WillCopyToPSDrive -f 'SBE package contents',$finalDestPath,$TargetNodeName)
    $copyItems += @{Source=$PackagePath;Destination=$finalDestPath}
    if (-not([string]::IsNullOrWhitespace($sbeConfigDest)))
    {
        Log-Info -Message ($locSbeTxt.WillCopyToPSDrive -f 'SBEConfiguration',$sbeConfigDest,$TargetNodeName)
        $copyItems += @{Source=$sbeConfig;Destination=$sbeConfigDest}
    }
    if (-not([string]::IsNullOrWhitespace($sbeRoleDest)))
    {
        Log-Info -Message ($locSbeTxt.WillCopyToPSDrive -f 'SBE.Role nuget',$sbeRoleDest,$TargetNodeName)
        $copyItems += @{Source=$sbeRoleNuget;Destination=$sbeRoleDest}
    }

    [string]$exclude = ""
    if ($ExcludeFiles.Count -ne 0)
    {
        $exclude += " /XF $ExcludeFiles"
    }
    if ($ExcludeDirs.Count -ne 0)
    {
        $exclude += " /XD $ExcludeDirs"
    }

    foreach ($item in $copyItems)
    {
        Log-Info -Message ($locSbeTxt.CopySBEToNode -f $item.Source,$TargetNodeName,$item.Destination) -Type Info
        $copyCmd = "robocopy.exe $($item.Source) $($item.Destination) *.* /MIR /NP /R:2 /W:10$exclude"
        $output = Invoke-Command -ScriptBlock { cmd.exe /c $copyCmd }
        # Check for exit code. If exit code is greater than 7, an error occurred while peforming the copy operation.
        if ($LASTEXITCODE -ge 8)
        {
            Log-Info -Message ($locSbeTxt.RobocopyFailed -f $LASTEXITCODE) -ConsoleOut -Type Error
            Log-Info -Message ($output | Out-String).Trim() -ConsoleOut -Type Info
            if ($destDrv) { $destDrv | Remove-PSDrive -ErrorAction SilentlyContinue }
            if ($sysDrv) { $sysDrv | Remove-PSDrive -ErrorAction SilentlyContinue }
            return $false
        }
    }
    if ($destDrv) { $destDrv | Remove-PSDrive -ErrorAction SilentlyContinue }
    if ($sysDrv) { $sysDrv | Remove-PSDrive -ErrorAction SilentlyContinue }
    return $true
}

function Get-SBEHealthCheckParams
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $ECEParameters,

        [String]
        $Tag
    )

    $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE"
    Import-Module "$($sbeRoleNuget)\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null
    $sbePartnerProps = Get-SBEPartnerProperties -SBERoleConfig $ECEParameters.Roles["SBE"].PublicConfiguration
    $sbeCredList = Get-SBECredentialList -Parameters $ECEParameters
    $sbeHostData = Get-AllNodesData -BareMetalConfig $ECEParameters.Roles["BareMetal"].PublicConfiguration

    $params = @{
        CredentialList = $sbeCredList
        HostData = $sbeHostData
        PartnerProperties = $sbePartnerProps
        Tag = $Tag
    }
    return $params
}

function Test-SBEPropertiesValid
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $ECEParameters
    )

    $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE"
    Import-Module "$($sbeRoleNuget)\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null
    $sbePartnerProps = Get-SBEPartnerProperties -SBERoleConfig $ECEParameters.Roles["SBE"].PublicConfiguration

    Log-Info -Message "Found '$($sbePartnerProps.Count)' PartnerProperties." -Type Info
}

function Test-SBECredentialsValid
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $ECEParameters
    )

    $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE"
    Import-Module "$($sbeRoleNuget)\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null
    $sbeCredList = Get-SBECredentialList -Parameters $ECEParameters
}

function Test-SolutionExtensionModule
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $PackagePath,

        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    $solExtModule = $null
    Log-Info -Message ($locSbeTxt.SBEPackagePath -f $PackagePath) -Type Info

    if ($PSSession)
    {
        $computername = $PsSession.ComputerName
    }
    else
    {
        $computername = $env:ComputerName
    }

    # Validate the SolutionExtension module using a function from the SBE Role Helper module
    $sbValidate = {
        param(
                [String]
                [parameter(Mandatory=$true)]
                $PackagePath,

                [String]
                [parameter(Mandatory=$true)]
                $SbeRoleNuget
            )
        try
        {
            Import-Module "$SbeRoleNuget\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null
            $solExtModulePath = Join-Path -Path $PackagePath -ChildPath "Configuration\SolutionExtension"
            $solExtModule = Initialize-SolutionExtensionModule -SolExtFilePath $solExtModulePath -RequireTag "HealthServiceIntegration" -AssertCertificate
            return $solExtModule
        }
        catch
        {
            Write-Output "An exception occurred while validating the SolutionExtension module: " + ($PSItem | Format-List * | Out-String).Trim()
        }
    }
    $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE"
    $solExtModule = if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $sbValidate -ArgumentList @($PackagePath, $sbeRoleNuget)
    }
    else
    {
        Invoke-Command -ScriptBlock $sbValidate -ArgumentList @($PackagePath, $sbeRoleNuget)
    }

    if ($null -eq $solExtModule)
    {
        Log-Info -Message ($locSbeTxt.NoHeatlhChecks) -Type Info
        return $false
    }
    elseif ($solExtModule -match "An exception occurred")
    {
        throw $solExtModule
    }

    return $true
}

function Invoke-TestSBEContentIntegrity
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $SBEMetadataPath,

        [Parameter(Mandatory=$true)]
        [string]
        $SBEContentPath,

        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession
    )

    $sbIntegrity = {
        param(
            [String]
            [parameter(Mandatory=$true)]
            $SBEMetadataPath,

            [String]
            [parameter(Mandatory=$true)]
            $SBEContentPath,

            [String]
            [parameter(Mandatory=$true)]
            $SbeRoleNuget
        )

        try
        {
            if (-not(Get-Command -Name Test-SBEContentIntegrity -ErrorAction SilentlyContinue))
            {
                Import-Module "$($SbeRoleNuget)\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null
            }
            $skipDir = @("IntegratedContent")
            Test-SBEContentIntegrity -SBEMetadataDirPath $SBEMetadataPath -SBEContentPath $SBEContentPath -IgnoreTopLevelFolder $skipDir
        }
        catch
        {
            throw $PSItem
        }
    }
    $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE"
    $result = if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $sbIntegrity -ArgumentList @($SBEMetadataPath, $SBEContentPath, $sbeRoleNuget)
    }
    else
    {
        Invoke-Command -ScriptBlock $sbIntegrity -ArgumentList @($SBEMetadataPath, $SBEContentPath, $sbeRoleNuget)
    }

    return $result
}

function Import-SolutionExtensionModule
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $PackagePath,

        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    # Import the SolutionExtension module
    $solExtModule = (Join-Path -Path $PackagePath -ChildPath "Configuration\SolutionExtension\SolutionExtension.psd1")
    Log-Info -Message ($locSbeTxt.ModuleToImport -f $solExtModule) -Type Info
    $sbImport = {
        param(
            [String]
            [parameter(Mandatory=$true)]
            $SolExtModule
        )
        try
        {
            Import-Module $SolExtModule -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null
        }
        catch
        {
            Write-Output "An error occurred while importing the SolutionExtension module: " + ($PSItem | Format-List * | Out-String).Trim()
        }
    }
    $result = if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $sbImport -ArgumentList @($solExtModule)
    }
    else
    {
        Invoke-Command -ScriptBlock $sbImport -ArgumentList @($solExtModule)
    }

    if ($result -match "An exception occurred")
    {
        throw $solExtModule
    }

    return $true
}

function New-SBEHealthResultObject
{
    param (
        [Parameter(Mandatory=$true)]
        [string]$TargetName,

        [Parameter()]
        [string]$TestName,

        [Parameter()]
        [ValidateSet('CRITICAL','WARNING','INFORMATIONAL')]
        [string]$Severity = 'INFORMATIONAL',

        [Parameter()]
        [ValidateSet('SUCCESS', 'FAILURE', 'ERROR')]
        [string]$Status,

        [Parameter()]
        [string]$Description,

        [Parameter()]
        [string]$Detail,

        [Parameter()]
        [bool]$PopulateAdditionalData = $true
    )

    $name = 'AzStackHci_SBEHealth'
    $title = 'SBE'
    if (-not([string]::IsNullOrWhiteSpace($TestName)))
    {
        $name += "_$TestName"
        $title += (" " + $TestName.Replace("-", " "))
    }
    $name += "_$TargetName"
    if (-not($title.EndsWith(" Health Check")))
    {
        $title += " Health Check"
    }

    $params = @{
        Name               = $name
        Title              = $title
        DisplayName        = $title
        Severity           = $Severity
        Description        = $Description
        Tags               = @{}
        Remediation        = ''
        TargetResourceID   = $TargetName
        TargetResourceName = $TargetName
        TargetResourceType = 'SBEHealth'
        Timestamp          = "$([datetime]::UtcNow)"
        Status             = $Status
        AdditionalData     = @{
            Source    = $TargetName
            Resource  = 'SBEHealth'
            Detail    = $Detail
            Status    = $Status
            Timestamp = "$([datetime]::UtcNow)"
        }
        HealthCheckSource  = $ENV:EnvChkrId
    }
    $resultObj = New-AzStackHciResultObject @params
    return $resultObj
}

function Get-ResultObject
{
    $resultObject = @{
        "Name" = ""
        "DisplayName" = ""
        "Title"= ""
        "Description" = ""
        "Status" = ""
        "Severity" = ""
        "Timestamp" = ""
        "TargetResourceID" = ""
        "TargetResourceName" = ""
        "TargetResourceType" = ""
        "Tags" = @{}
        "AdditionalData" = @{}
        "HealthCheckSource" = ""
        "Remediation" = ""
    }
    return $resultObject
}

function Assert-ResponseSchemaValid
{
    [CmdletBinding()]
    param (
        [PSObject[]]$ResultObject
    )

    $expectedSchema = Get-ResultObject
    foreach ($item in $ResultObject)
    {
        # Assert Name or Title must contain information
        if ([string]::IsNullOrWhiteSpace($item.Name) -and [string]::IsNullOrWhiteSpace($item.Title))
        {
            $msg = "Both Name and Title properties of this result object are empty"
            Log-Info -Message $msg -Type Error
            $item.AdditionalData.NameTitleEmpty = $msg
            $item.Severity = 'CRITICAL'
            $item.Status = "Error"
        }
        elseif ([string]::IsNullOrWhiteSpace($item.Name))
        {
            $item.Name = $item.Title
        }
        elseif ([string]::IsNullOrWhiteSpace($item.Title))
        {
            $item.Title = $item.Name
        }

        # Assert response contains expected schema properties
        foreach ($expectedKey in $expectedSchema.Keys)
        {
            if (-not($item.ContainsKey($expectedKey)))
            {
                # TODO : Temporary special case to add DisplayName if missing due to this being added after partner communication
                if ($key -eq "DisplayName")
                {
                    $item.DisplayName = $item.Title
                }
                else
                {
                    Log-Info -Message "Expected result property '$($expectedKey)' was not found" -Type Warning
                    $item.$expectedKey = ""
                    # TODO : In the future, we should decide how to better handle these cases of missing properties
                }
            }
        }

        # Assert Status values
        if ($item.Status -notin @("Success", "Failure", "Error"))
        {
            $msg = "Unexpected Status: '$($item.Status)'"
            Log-Info -Message $msg
            $item.AdditionalData.StatusDiscrepancy = $msg
            $item.Status = "Error"
        }

        # Assert Severity values
        if ($item.Severity -notin @('CRITICAL', 'WARNING', 'INFORMATIONAL'))
        {
            $msg = "Unexpected Severity: '$($item.Severity)'"
            $item.AdditionalData.SeverityDiscrepancy = $msg
            if ($item.Status -eq "Success")
            {
                $item.Severity = 'WARNING'
                Log-Info -Message $msg -Type Warning
            }
            else
            {
                $item.Severity = 'CRITICAL'
                Log-Info -Message $msg -Type Error
            }
        }

        # Assert Timestamp is valid
        if (-not [string]::IsNullOrWhiteSpace($item.Timestamp))
        {
            try
            {
                $null = [DateTime]$item.Timestamp
            }
            catch
            {
                Log-Info -Message "Invalid Timestamp: '$($item.Timestamp)'" -Type Warning
                if (-not [string]::IsNullOrWhiteSpace($item.AdditionalData.Timestamp))
                {
                    try
                    {
                        $null = [DateTime]$item.AdditionalData.Timestamp
                        # AdditionalData.Timestamp is valid, so use it
                        $item.Timestamp = $item.AdditionalData.Timestamp
                    }
                    catch
                    {
                        # Use current time
                        $item.Timestamp = "$([datetime]::UtcNow)"
                    }
                }
                else
                {
                    # Use current time
                    $item.Timestamp = "$([datetime]::UtcNow)"
                }
            }
        }
        if (-not [string]::IsNullOrWhiteSpace($item.AdditionalData.Timestamp))
        {
            try
            {
                $null = [DateTime]$item.AdditionalData.Timestamp
            }
            catch
            {
                Log-Info -Message "Invalid Timestamp: '$($item.AdditionalData.Timestamp)'" -Type Warning
                # Timestamp must be valid now, so use it
                $item.AdditionalData.Timestamp = $item.Timestamp
            }
        }
    }

    return $ResultObject
}

Export-ModuleMember -Function Test-*
Export-ModuleMember -Function New-SBEHealthResultObject
Export-ModuleMember -Function Get-SBEHealthCheckParams
Export-ModuleMember -Function Copy-SBEContentLocalToNode
Export-ModuleMember -Function Copy-SBEContentToSession
Export-ModuleMember -Function Import-SolutionExtensionModule
Export-ModuleMember -Function Assert-ResponseSchemaValid
Export-ModuleMember -Function Invoke-TestSBEContentIntegrity
# SIG # Begin signature block
# MIIoRgYJKoZIhvcNAQcCoIIoNzCCKDMCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB4bEelPvI0MR0f
# Vdg3s9G3J+QpLXPltULxS+a9dQk9rqCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0
# 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
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIB97B1ADFg+UlmeGCkl9Lf1v
# t2ExsUSkr8hlkTz6hwRpMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAnE9URHVio9V3MvNdSooy5SYgjmVfSPo1GdqYMRu0cg4m7qZgINiXOOSy
# 1oKsZdhQPyyjdVCrkPx3fF/CXJJn9WPJ0gqdVjy+gL8W3Z/wYZT6cpv763SQQJBq
# ENcRL34pkvNluSxY1A8xZiQwSzXxj2emSjsrlh81V/IYiHZHYmHKw7aKMxvzrY68
# QNwipPUcNZylhX5fzoLqxQSwVUTHa8D6WKnv9PleZYYdSOgV931utb3DwUboVDeU
# JxTRR+tA+D6K4O2LYCGLyrrrMoxa5w74MLGkLZ9v+/HRoUaEKNiMZw9IFxSO2rHm
# EaNuli4NXItHf+lujCIaGU8uxxqQhKGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCC
# F5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq
# hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCCKC6TvM1DnsFDrodfWxr0b3Jf7kbzSfUpSAjLCNBkMggIGZ0nWNWzc
# GBMyMDI0MTIwNDE1MDQwNC4yNDdaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# TjozNjA1LTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAAB91ggdQTK+8L0AAEAAAH3MA0G
# CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0
# MDcyNTE4MzEwNloXDTI1MTAyMjE4MzEwNlowgdMxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w
# ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjM2MDUt
# MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl
# MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0OdHTBNom6/uXKaEKP9r
# PITkT6QxF11tjzB0Nk1byDpPrFTHha3hxwSdTcr8Y0a3k6EQlwqy6ROz42e0R5eD
# W+dCoQapipDIFUOYp3oNuqwX/xepATEkY17MyXFx6rQW2NcWUJW3Qo2AuJ0HOtbl
# SpItQZPGmHnGqkt/DB45Fwxk6VoSvxNcQKhKETkuzrt8U6DRccQm1FdhmPKgDzgc
# fDPM5o+GnzbiMu6y069A4EHmLMmkecSkVvBmcZ8VnzFHTDkGLdpnDV5FXjVObAgb
# SM0cnqYSGfRp7VGHBRqyoscvR4bcQ+CV9pDjbJ6S5rZn1uA8hRhj09Hs33HRevt4
# oWAVYGItgEsG+BrCYbpgWMDEIVnAgPZEiPAaI8wBGemE4feEkuz7TAwgkRBcUzLg
# Q4uvPqRD1A+Jkt26+pDqWYSn0MA8j0zacQk9q/AvciPXD9It2ez+mqEzgFRRsJGL
# tcf9HksvK8Jsd6I5zFShlqi5bpzf1Y4NOiNOh5QwW1pIvA5irlal7qFhkAeeeZqm
# op8+uNxZXxFCQG3R3s5pXW89FiCh9rmXrVqOCwgcXFIJQAQkllKsI+UJqGq9rmRA
# BJz5lHKTFYmFwcM52KWWjNx3z6odwz2h+sxaxewToe9GqtDx3/aU+yqNRcB8w0tS
# XUf+ylN4uk5xHEpLpx+ZNNsCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBTfRqQzP3m9
# PZWuLf1p8/meFfkmmDAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf
# BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz
# L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww
# bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El
# MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF
# BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAN0ajafILeL6S
# QIMIMAXM1Qd6xaoci2mOrpR8vKWyyTsL3b83A7XGLiAbQxTrqnXvVWWeNst5YQD8
# saO+UTgOLJdTdfUADhLXoK+RlwjfndimIJT9MH9tUYXLzJXKhZM09ouPwNsrn8YO
# LIpdAi5TPyN8Cl11OGZSlP9r8JnvomW00AoJ4Pl9rlg0G5lcQknAXqHa9nQdWp1Z
# xXqNd+0JsKmlR8tcANX33ClM9NnaClJExLQHiKeHUUWtqyLMl65TW6wRM7XlF7Y+
# PTnC8duNWn4uLng+ON/Z39GO6qBj7IEZxoq4o3avEh9ba43UU6TgzVZaBm8VaA0w
# SwUe/pqpTOYFWN62XL3gl/JC2pzfIPxP66XfRLIxafjBVXm8KVDn2cML9IvRK02s
# 941Y5+RR4gSAOhLiQQ6A03VNRup+spMa0k+XTPAi+2aMH5xa1Zjb/K8u9f9M05U0
# /bUMJXJDP++ysWpJbVRDiHG7szaca+r3HiUPjQJyQl2NiOcYTGV/DcLrLCBK2zG5
# 03FGb04N5Kf10XgAwFaXlod5B9eKh95PnXKx2LNBgLwG85anlhhGxxBQ5mFsJGkB
# n0PZPtAzZyfr96qxzpp2pH9DJJcjKCDrMmZziXazpa5VVN36CO1kDU4ABkSYTXOM
# 8RmJXuQm7mUF3bWmj+hjAJb4pz6hT5UwggdxMIIFWaADAgECAhMzAAAAFcXna54C
# 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
# TjozNjA1LTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAb28KDG/xXbNBjmM7/nqw3bgrEOaggYMw
# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF
# AAIFAOr67AMwIhgPMjAyNDEyMDQxNDU2MDNaGA8yMDI0MTIwNTE0NTYwM1owdzA9
# BgorBgEEAYRZCgQBMS8wLTAKAgUA6vrsAwIBADAKAgEAAgIL7AIB/zAHAgEAAgIS
# bjAKAgUA6vw9gwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow
# CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQA8u4NRbeCo
# xhCkd0pG5OpIpdaSE8PwtSLa/X1PHoB+Lo4A+BNUx5VI5H1+0AGMn61ho9i+uHJg
# w9iiXBsx9XEjAEC9Yk36p/HN/k6hiWYdhK0rHyaYIc7EYFz34RvBUmL7c5M4vEE3
# agEMeASPRbFq/X0W4XGZB3T8kVyoHR0gBwCgLsrUfSyp86fHvQULcwgTsQjR+Fg1
# xe1tU5qAtPcKaTKY+/sDonajMMWDcFoplxCTC4unOnet23wygGB+zAQZLF0MogtF
# bArhxOzXJpK0r9H/pKgG7PLIOTWBY5lMvSB9QguCdVCjhS4T9he/INO3SsyGefbT
# wzFTs2sAVbV/MYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAH3WCB1BMr7wvQAAQAAAfcwDQYJYIZIAWUDBAIBBQCgggFKMBoG
# CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgoDZDkzhK
# 6ELE30LlRV0PmklkaWsDKH0DziQp2GtcuRwwgfoGCyqGSIb3DQEJEAIvMYHqMIHn
# MIHkMIG9BCAh2pjaa3ca0ecYuhu60uYHP/IKnPbedbVQJ5SoIH5Z4jCBmDCBgKR+
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB91ggdQTK+8L0AAEA
# AAH3MCIEICHPFvVYs5oZoAOL51irkJfC3AbDaLOurx/mm0cPAqaxMA0GCSqGSIb3
# DQEBCwUABIICAMtyQgn6+EoLE+npN2MlLIhQr42mpym1w80lSzMNwvbzupiqZG01
# rVg5paOC5V4wEltX8VYkE2ktIBYkKEX+KJYwwG5wUFLKbslev0oPe8vwffu94b5j
# ZnEya85mfnnqS6Ht7HwxdTZw3JnKwxy3N+GKkDu6VkgMOH8Xvb6G/N42Gvqc08ST
# cdK4z8/4QRCSmkI1gBvHguOm81GBz44K5v+2senMRV1Cnq5geE61/Xe70p6Ipudy
# jZAQGl10bngq0uWq6Un8PR8NtR+R9UTLdIsdSzgpB6SieNmlDTrOCRyHTyDHSOLh
# 7PUv3+L4/dpz5/xMg92gFXstAwcKH+QGEtTew5He4xCq2IrF5sXvtMopsBD8KQOM
# sW8hkA5kMgeU61monPV5Sv23K1YAT3V41Z0+UVymnW7nirbSCOYIPWmjYXsms1Ud
# TBP2hl/3fjTVmhpsCOgt7QmoYygDGSI/hhgSrQ69WME1teHyIpr2fWtm8k9KcBwD
# JpBimoPO3SI5XmvcEW1VQDJjKOOKGWWw+sCDS6wsO2w9keCZU2Ie9JqmSK855jE5
# abATt73SDu1M36gMy5dK8joobOKbMoPmTvQ8G8sLJZCsVbrY2ZZDztwCmyk9UJFk
# v6CD4gpJkBkJnoI/cTjWgiCOo6oXSKC487uEVoXfQeDvS+vs3c26U4HB
# SIG # End signature block