Automation/Generate-VaaSReport.ps1

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

 <#
.SYNOPSIS
 
Launches a VaaS Test Pass based off of the supplied parameters.
 
.DESCRIPTION
 
Launches a VaaS Test Pass based off of the supplied parameters using the AzureStackVaaS PowerShell cmdlets.
 
.PARAMETER VaaSSolutionName
 
VaaSSolutionName
 
.PARAMETER VaaSTestPassName
 
VaaSTestPassName
 
.PARAMETER VaaSAccountCreds
 
VaaSAccountCreds - PSCredential type
 
.PARAMETER VaaSApplicationId
 
VaaSApplicationId
 
.PARAMETER VaaSAccountTenantId
 
VaaSAccountTenantId
 
.PARAMETER ProcessTestLogsFromPath
 
Specifies directory containing zipped log files (Results) to be processed when generating the VaaS Report.
If left empty, logs will be downloaded to a subdirectory named "Logs" for processing.
 
.PARAMETER LogFilePath
 
Path to log files from tests.
 
.PARAMETER VaaSPackageUri
 
Optional. Overrides default VaaS package storage location
 
.PARAMETER VaaSPortalUri
 
Optional. Specifies VaaS Portal Endpoint.
 
.PARAMETER VaaSBaseServiceUri
 
Optional. Specifies VaaS Service Endpoint.
 
.PARAMETER VaaSServiceResourceId
 
VaaSServiceResourceId
 
.PARAMETER ParseLocalLogs
 
ParseLocalLogs switch is used in conjunction with parameters
 
.PARAMETER UseVaaSSPNAuth
 
The UseVaaSSPNAuth switch specifies that SPN credentials are supplied via parameter VaaSAccountCreds.
Note that parameters VaaSApplicationId and VaaSApplicationUri are not applicable when specifying the UseVaaSSPNAuth switch.
 
.EXAMPLE
 
$secpasswd = ConvertTo-SecureString “*****************” -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential (“******************”, $secpasswd)
.\Generate-VaaSReport.ps1 -VaaSAccountCreds $creds -VaaSAccountTenantId <TenantId> -VaaSSolutionName <SolutionName> -VaaSTestPassName <TestPassName>
 
.LINK
 
https://www.powershellgallery.com/packages/AzureStackVaaS
 
#>


param(
[Parameter(Mandatory=$true)][string]$VaaSSolutionName=$null,
[Parameter(Mandatory=$true)][string]$VaaSTestPassName=$null,
[Parameter(Mandatory=$true)][ValidateNotNull()][PSCredential]$VaaSAccountCreds,
[Parameter(Mandatory=$false)][string]$VaaSApplicationId=$null,
[Parameter(Mandatory=$true)][ValidateNotNull()][string]$VaaSAccountTenantId,
[Parameter(Mandatory=$false)][string]$ProcessTestLogsFromPath=$null,
[Parameter(Mandatory=$false)][string]$LogFilePath=$null,
[Parameter(Mandatory=$false)][string]$VaaSPackageUri=$null,
[Parameter(Mandatory=$false)][string]$VaaSPortalUri=$null,
[Parameter(Mandatory=$false)][string]$VaaSApplicationUri=$null,
[Parameter(Mandatory=$false)][string]$VaaSBaseServiceUri=$null,
[Parameter(Mandatory=$false)][string]$VaaSServiceResourceId=$null,
[Parameter(Mandatory=$false)][switch]$ParseLocalLogs,
[Parameter(Mandatory=$false)][switch]$UseVaaSSPNAuth
)

[System.Reflection.Assembly]::LoadWithPartialName("mscorlib") | Out-Null
Add-Type -TypeDefinition @"
public struct TestData
{
public string testName;
public System.TimeSpan duration;
public int testsPassed;
public int testsFailed;
public int testsTotal;
}
"@


New-Variable -Name BaseURIVaaSStorage -Value "https://vaastestpacksprodeastus.blob.core.windows.net" -Option Constant
New-Variable -Name BaseURIVaaSPortal -Value "https://azurestackvalidation.com" -Option Constant
New-Variable -Name VaaSPackageDirectory -Value "$env:Temp\VaaS"
New-Variable -Name ProductionRun -Value $true

function CreateVaaSLogDirectory
{
    if (!(Test-Path -Path "$LogFilePath" -PathType Container))
    {
        Write-Verbose "Creating VaaS Log Directory ..."
        New-Item -ItemType Directory -Path $LogFilePath | Out-Null  
    }
}

function InstallVaaSPSModules
{
    Write-Host "Installing Azure Stack VaaS PowerShell Module"
    if($script:ProductionRun -eq $true)
    {
        Write-Host "Installing Azure Stack VaaS PowerShell Module from PowerShell Gallery ..."
        Install-Module -Name AzureStackVaaS -Scope AllUsers -AllowClobber -Force -ErrorVariable InstallError -ErrorAction SilentlyContinue
        if($InstallError)
        {
            Write-Warning "Couldn't install Azure Stack VaaS PowerShell module..."
        }
    }
    else
    {
        $vaasPSModulesUri = "$VaaSPackageUri/AzureStackVaaS.zip"
        Write-Host "Downloading latest Azure Stack VaaS PowerShell Module from:"
        Write-Host $vaasPSModulesUri
        try
        {
            Invoke-WebRequest -Uri $vaasPSModulesUri -outfile "$VaaSPackageDirectory\AzureStackVaaS.zip"
        }
        catch
        {
            Write-Warning "Unable to download file from $vaasPSModulesUri"
            Write-Warning "Attempting retry in 30 seconds ..."
            Start-Sleep -s 30 | Out-Null
            try
            {
                Invoke-WebRequest -Uri $vaasPSModulesUri -outfile "$VaaSPackageDirectory\AzureStackVaaS.zip"
            }
            catch
            {
                Write-Warning "Unable to download file on retry from:`r`n$vaasPSModulesUri"
                return
            }        
        }
        
        $psModulePath = $env:ProgramFiles + '\WindowsPowerShell\Modules'    
        Write-Host "Extracting latest Azure Stack VaaS PowerShell Module to:`r`n$psModulePath"
        Expand-Archive -Path "$VaaSPackageDirectory\AzureStackVaaS.zip" -DestinationPath "$psModulePath" -Force -ErrorAction SilentlyContinue -ErrorVariable ExpandError
        if($ExpandError)
        {
            if($ExpandError[0].CategoryInfo.Reason -imatch 'UnauthorizedAccessException')
            {
                Write-Warning "A PowerShell lock currently exists on the Azure Stack VaaS PowerShell."
                Write-Warning "To update binaries, please close the applicable PowerShell window or ISE."
            }        
        }
    }
}

function DownloadLogs
{
    param(
    [parameter(Mandatory=$true)][string]$testLaunchName,
    [parameter(Mandatory=$true)][string]$testName,
    [parameter(Mandatory=$true)][string]$archiveFullName
    )

    Get-AzureStackVaaSTestLaunchLogs -SolutionName $VaaSSolutionName -TestPassName $VaaSTestPassName -TestLaunchName $testLaunchName -Path $archiveFullName -ErrorAction SilentlyContinue -ErrorVariable downloadError

    if($downloadError)
    {
        if($downloadError[0].CategoryInfo.Reason -imatch 'AggregateException')
        {
            Write-Warning "Unable to download logs for test $testLaunchName to $archiveFullName"
            Write-Warning "Command used to download logs:"
            Write-Warning "Get-AzureStackVaaSTestLaunchLogs -SolutionName $VaaSSolutionName -TestPassName $VaaSTestPassName -TestLaunchName $testLaunchName -Path $archiveFullName"
            Write-Warning ""
            Start-Sleep -s 2
            return ""
        }        
    }

    Write-Host "Logs for test $testName [$testLaunchName] downloaded to $archiveFullName"
    return $archiveFullName
}

function ParseTrxLog
{
    param(
    [parameter(Mandatory=$true)][string]$trxFullName
    )
    
    try
    {
        $resultXML = [xml](Get-Content -Path $trxFullName)
    }
    catch
    {
        Write-Warning "Log file appears to be corrupt. Please check file content structure ..."
        return $null
    }

    if(!($resultXML))
    {
        Write-Error "Unable to get contents of file $trxFullName!"
        return $null
    }
    
    Write-Host "Parsing TRX log $trxFullName ..."
    $testData = New-Object TestData    
    $testData.duration = [DateTime]::Parse($($resultXML.TestRun.Times.finish)) - [DateTime]::Parse($($resultXML.TestRun.Times.start))
    $testData.testsPassed = $resultXML.TestRun.ResultSummary.Counters.passed
    $testData.testsFailed = $resultXML.TestRun.ResultSummary.Counters.failed
    $testData.testsTotal = $resultXML.TestRun.ResultSummary.Counters.total

    return $testData
}

function ParseWtlLog
{
    param(
    [parameter(Mandatory=$true)][string]$wtlFullName
    )

    try
    {
        $resultXML = [xml](Get-Content -Path $wtlFullName)
    }
    catch
    {
        Write-Warning "Log file appears to be corrupt. Please check file content structure ..."
        return $null
    }
    
    if(!($resultXML))
    {
        Write-Error "Unable to get contents of file $wtlFullName ..."
        return $null
    }
    
    Write-Host "Parsing $wtlFullName ..."
    [uint64] $OneSecond100ns = 10000000;
    $testData = New-Object TestData    
    $startDateTime = ([DateTime] ("{1:D2}/{2:D2}/{0:D4} {3:D2}:{4:D2}:{5:D2}.{6:D3}" -f $resultXML.'WTT-Logger'.RTI.BaseTime.Split(': ')))
    $frequency = [uint64]$resultXML.'WTT-Logger'.RTI.Frequency
    $lastCA = [uint64]($resultXML.'WTT-Logger'.GetElementsByTagName("EndTest") | Select-Object -Last 1).CA
    $testData.duration = $startDateTime.AddTicks( ($lastCA*$OneSecond100ns) / $frequency) - $startDateTime
    $testData.testsPassed = 0
    $testData.testsFailed = 0
    $testData.testsTotal = ($resultXML.'WTT-Logger'.GetElementsByTagName("EndTest")).Count
    $results = $resultXML.'WTT-Logger'.EndTest
    foreach($result in $results)
    {
        if($result.Result -imatch "Pass")
        {
            $testData.testsPassed++
        }
        elseif($result.Result -imatch "fail")
        {
            $testData.testsFailed++
        }
    }

    return $testData
}

function IsLogDirectoryExist
{
    param(
    [parameter(Mandatory=$true)][string]$LogFilePath
    )

    Write-Host "Verifying if $LogFilePath exists ..."
    if(Test-Path -LiteralPath $LogFilePath -PathType Container)
    {
        return $true
    }    
    return $false
}

function IsLogsExist
{
    param(
    [parameter(Mandatory=$true)][string]$testArchive
    )

    Write-Host "Verifying $testArchive exists ..."
    if(Test-Path -Path $testArchive -PathType Leaf)
    {
        return $true
    }    
    return $false
}

function IsTrxLogFormat
{
    param(
    [parameter(Mandatory=$true)][string]$logPath
    )
    
    Write-Host "Searching for TRX log file in $logPath..."
    $logFile = Get-ChildItem -Path "$logPath\*.trx" -Recurse -Force
    
    if(!($logFile))
    {
        return $false
    }

    $logFileFullName = ($logFile | Sort-Object LastWriteTime -descending)[0].FullName
    Write-Host "Found TRX log file at: $logFileFullName" -ForegroundColor Green
    return $true
}

function IsWtlLogFormat
{
    param(
    [parameter(Mandatory=$true)][string]$logPath
    )
    
    Write-Host "Searching for WTL log file in $logPath..."
    $logFile = Get-ChildItem -Path "$logPath\*.wtl" -Recurse -Force 
    
    if($logFile -eq $null)
    {
        Write-Host "Log file with WTL file extension not found ..."
        Write-Host "Searching for WTL log file with XML file extension..."
        $logFile = Get-ChildItem -Path "$logPath\*.xml" -Recurse -Force
        if($logFile -eq $null)
        {
            Write-Host "No files with XML file extension found ..."
            return $false
        }
        
        $logFileFullName = ($logFile | Sort-Object LastWriteTime -descending)[0].FullName
        Write-Host "Verifying that $logFileFullName contains WTT-Logger element ..."
        try
        {
            $resultXML = [xml](Get-Content -Path $logFileFullName)
        }
        catch
        {
            Write-Warning "Log file appears to be corrupt. Please check file content structure ..."
            return $false
        }        

        $elements = $resultXML.GetElementsByTagName("WTT-Logger")
        if($elements.Count -eq 0)
        {
            Write-Warning "WTT-Logger document element not found in $logFileFullName ..."
            return $false
        }
        Write-Host "Found WTT-Logger document element"
    }
    $logFileFullName = ($logFile | Sort-Object LastWriteTime -descending)[0].FullName
    Write-Host "Found WTL log file at: $logFileFullName" -ForegroundColor Green
    
    return $true
}

function IsTestCompleted
{
    param(
    [parameter(Mandatory=$true)][PSVaaSTestManifest]$vaasTestManifest
    )

    if(($vaasTestManifest.State -eq "Accepted") -or ($vaasTestManifest.State -eq "Running"))
    {
        return $false
    }
    
    return $true
}

function GetTrxLogFileFullName
{
    param(
    [parameter(Mandatory=$true)][string]$logPath
    )
    
    $logFile = Get-ChildItem -Path "$logPath\*.trx" -Recurse -Force
    
    if(!($logFile))
    {
        return $null
    }

    $logFileFullName = ($logFile | Sort-Object LastWriteTime -descending)[0].FullName
    return $logFileFullName
}

function GetWtlLogFileFullName
{
    param(
    [parameter(Mandatory=$true)][string]$logPath
    )
    
    $logFile = Get-ChildItem -Path "$logPath\*.wtl" -Recurse -Force 
    
    if($logFile -eq $null)
    {
        $logFile = Get-ChildItem -Path "$logPath\*.xml" -Recurse -Force
        if($logFile -eq $null)
        {
            return $null
        }
        
        $logFileFullName = ($logFile | Sort-Object LastWriteTime -descending)[0].FullName
        $resultXML = [xml](Get-Content -Path $logFileFullName)
        $elements = $resultXML.GetElementsByTagName("WTT-Logger")
        if($elements.Count -eq 0)
        {
            return $null
        }
    }
    else
    {
        $logFileFullName = ($logFile | Sort-Object LastWriteTime -descending)[0].FullName
    }
    
    return $logFileFullName
}

function GetAllTestsFromTestPass
{
    $vaasTestManifests = Find-AzureStackVaaSTestLaunch -SolutionName $VaaSSolutionName -TestPassName $VaaSTestPassName -ErrorAction SilentlyContinue -ErrorVariable vaasTestManifestError
    if($vaasTestManifestError -or ($vaasTestManifests -eq $null))
    {
        Write-Warning "Did not find any test manifest data for Solution $VaaSSolutionName Test pass $VaaSTestPassName"
        $emptyArray = @()
        return ,$emptyArray
    }    
    return $vaasTestManifests
}

function GetPendingTests()
{
    param(
    [parameter(Mandatory=$true)][AllowEmptyCollection()][Object[]]$vaasTestManifests
    )

    $pendingTests = $vaasTestManifests | ? {$_.State -eq "Accepted" -or $_.State -eq "Running"} -ErrorAction SilentlyContinue -ErrorVariable vaasTestManifestError
    if($vaasTestManifestError)
    {
        Write-Verbose "No pending tests found for Solution $VaaSSolutionName Test pass $VaaSTestPassName"
        $emptyArray = @()
        return ,$emptyArray
    }
    return $pendingTests    
}

function GetCanceledTests()
{
    param(
    [parameter(Mandatory=$true)][AllowEmptyCollection()][Object[]]$vaasTestManifests
    )
    
    $cancelledTests = $vaasTestManifests | ? {$_.State -eq "Cancelled"} -ErrorAction SilentlyContinue -ErrorVariable vaasTestManifestError
    if($vaasTestManifestError)
    {
        Write-Verbose "No cancelled tests found for Solution $VaaSSolutionName Test pass $VaaSTestPassName"
        $emptyArray = @()
        return ,$emptyArray
    }
    return $cancelledTests    
}

function GetAbortedTests()
{
    param(
    [parameter(Mandatory=$true)][AllowEmptyCollection()][Object[]]$vaasTestManifests
    )
    
    $abortedTests = $vaasTestManifests | ? {$_.State -eq "Aborted"} -ErrorAction SilentlyContinue -ErrorVariable vaasTestManifestError
    if($vaasTestManifestError)
    {
        Write-Verbose "No aborted tests found for Solution $VaaSSolutionName Test pass $VaaSTestPassName"
        $emptyArray = @()
        return ,$emptyArray
    }
    return $abortedTests    
}

function GetRantoCompletionTests()
{
    param(
    [parameter(Mandatory=$true)][AllowEmptyCollection()][Object[]]$vaasTestManifests
    )

    $completedTests = $vaasTestManifests | ? {$_.State -eq "Succeeded" -or $_.State -eq "Failed"} -ErrorAction SilentlyContinue -ErrorVariable vaasTestManifestError
    if($vaasTestManifestError)
    {
        Write-Verbose "No completed tests found for Solution $VaaSSolutionName Test pass $VaaSTestPassName"
        $emptyArray = @()
        return ,$emptyArray
    }
    return $completedTests    
}

function GetTestStatus
{
    param(
    [parameter(Mandatory=$true)][PSVaaSTestManifest]$vaasTestManifest
    )

    return $($vaasTestManifest.State)
}

function ValidateAndResolveAuthMethodParameters
{
    if((($VaaSPortalUri -eq "") -or ($VaaSPortalUri -imatch $BaseURIVaaSPortal)) -and ($VaaSBaseServiceUri -eq "") -and ($VaaSServiceResourceId -eq ""))
    {
        $script:ProductionRun = $true
    }
    elseif((($VaaSPortalUri -ne "") -or (!($VaaSPortalUri -imatch $BaseURIVaaSPortal))) -and ($VaaSBaseServiceUri -ne "") -and ($VaaSServiceResourceId -ne ""))
    {
        $script:ProductionRun = $false
    }
    else
    {
        throw [System.ArgumentException] "Parameters VaaSPortalUri, VaaSBaseServiceUri, and VaaSServiceResourceId must be either all specified, or none specified ..."
    }    
    
    if(($script:ProductionRun) -or ($UseVaaSSPNAuth))
    {
        if(($VaaSApplicationId -ne "") -or ($VaaSApplicationUri -ne ""))
        {
            throw [System.ArgumentException] "Parameters VaaSApplicationId and/or VaaSApplicationUri should not be specified when launching a production level run or when using SPN credentials ..."
        }    
    }
    elseif(!$UseVaaSSPNAuth)
    {
        Write-Host "Verifying that parameters VaaSApplicationId and VaaSApplicationUri are present as`r`nthe UseVaaSSPNAuth switch was not specified..."
        if(($VaaSApplicationId -eq "") -or ($VaaSApplicationUri -eq ""))
        {
            throw [System.ArgumentException] "Parameters VaaSApplicationId and VaaSApplicationUri must both be specified when using non-SPN credentials are supplied ..."
        }    
    }
}

function ValidateAndResolveTestParameters
{
    if($VaaSPackageUri -eq "")
    {
        $script:VaaSPackageUri = "$BaseURIVaaSStorage/packages"
        Write-Host "VaaS Storage endpoint updated to:`r`n$VaaSPackageUri"
    }
    
    if($VaaSPortalUri -eq "")
    {
        $script:VaaSPortalUri = $BaseURIVaaSPortal
        Write-Host "VaaS Portal endpoint updated to:`r`n$VaaSPortalUri"
    }
    
    if($ProcessTestLogsFromPath -ne "")
    {
        if(!(IsLogDirectoryExist -LogFilePath $ProcessTestLogsFromPath))
        {
            throw [System.ArgumentException] "Directory $ProcessTestLogsFromPath cannot be found or is not accessible. Please check and try again ..."
        }
    }
}

function ValidateAndResolveParameters
{
    Write-Host "Validating and Resolving Parameters"
    ValidateAndResolveAuthMethodParameters
    ValidateAndResolveTestParameters    
}

function AddAzureAccount
{
    Write-Host "Adding AzureStack VaaS Account"
    if($script:ProductionRun -eq $true)
    {
        Write-Host "Production Run ..."
        if($UseVaaSSPNAuth)
        {
            Write-Host "Using SPN Authentication"
            Add-AzureStackVaaSAccount -Credential $VaaSAccountCreds -ServicePrincipal -TenantId $VaaSAccountTenantId -ErrorAction Stop
        }
        else
        {
            Write-Host "Using User Authentication"
            Add-AzureStackVaaSAccount -Credential $VaaSAccountCreds -TenantId $VaaSAccountTenantId -ErrorAction Stop
        }            
    }
    else
    {
        Write-Host "Non-Production Run ..."
        if($UseVaaSSPNAuth)
        {
            Write-Host "Using SPN Authentication"
            Add-AzureStackVaaSAccount -Credential $VaaSAccountCreds -ServicePrincipal -TenantId $VaaSAccountTenantId -TenantServiceBaseUri $VaaSBaseServiceUri -TenantServiceResourceId $VaaSServiceResourceId -ErrorAction Stop
        }
        else
        {
            Write-Host "Using User Authentication"
            Add-AzureStackVaaSAccount -Credential $VaaSAccountCreds -ApplicationId $VaaSApplicationId -ApplicationUri $VaaSApplicationUri -TenantId $VaaSAccountTenantId -TenantServiceBaseUri $VaaSBaseServiceUri -TenantServiceResourceId $VaaSServiceResourceId -ErrorAction Stop
        }        
    }
}

function GenerateWarningBlock
{
    param(
    [parameter(Mandatory=$true)][ValidateNotNull()][string]$warningTitle,
    [parameter(Mandatory=$true)][Object[]]$vaasTestManifests
    )

    [string]$warningData = ""    
    $warningData += "<li>$warningTitle</li>`n"
    $warningData += " <ul>`n"
    foreach($test in $vaasTestManifests)
    {
        $warningData += "<li>$($test.TestName) [$($test.TestLaunchName)]</li>`n"
    }
    $warningData += " </ul>`n"    
    return $warningData
}

function GeneratePreContent
{
    $testPass = Get-AzureStackVaaSTestPass -SolutionName $VaaSSolutionName -Name $VaaSTestPassName -ErrorAction Stop
    
    [string]$preContent = "<p class='body'><b>Tenant Id:</b> $VaaSAccountTenantId</p>`n"
    $preContent += "<p class='body'><b>Solution Name:</b> $VaaSSolutionName</p>`n"
    $preContent += "<p class='body'><b>Test Pass Name:</b> $VaaSTestPassName</p>`n"
    $preContent += "<p class='body'><b>Test Pass Date Created:</b> $($testPass.CreatedTimeUtc) (UTC)</p>`n"
    $preContent += "<p class='body'><b>Test Pass Tags</b><br/>`n"    
    foreach ($tg in $testPass.Tags.GetEnumerator())
    {
        $preContent += "$($tg.Name.PadLeft(16, " ")) : $($tg.Value)<br/>`n"
    }
    $preContent += "</p>`n"
    $preContent += "<p class='body'><b>Test Details</b></p>`n"
    
    return $preContent
}

function GeneratePostContent
{
    param(
    [parameter(Mandatory=$true)][AllowEmptyCollection()][Object[]]$vaasTestManifests,
    [parameter(Mandatory=$true)][AllowEmptyCollection()][Object[]]$vaasTestsWOLogs,
    [parameter(Mandatory=$true)][AllowEmptyCollection()][Object[]]$vaasTestsWOSupportedLogs,
    [parameter(Mandatory=$true)][AllowEmptyCollection()][Object[]]$vaasTestsLogError
    )
    
    [bool]$Notes = $false
    [string]$noteData = ""
    [string]$postContent = "<p class='body'><b>Warning(s):</b><br/>`n"
    if($vaasTestManifests)
    {
        $cancelledTests = GetCanceledTests -vaasTestManifests $vaasTestManifests
        if($cancelledTests.Count -gt 0)
        {
            $noteData += GenerateWarningBlock -warningTitle "Test pass has one or more tests in canceled state:" -vaasTestManifests $cancelledTests
        }

        $abortedTests = GetAbortedTests -vaasTestManifests $vaasTestManifests
        if($abortedTests.Count -gt 0)
        {
            $noteData += GenerateWarningBlock -warningTitle "Test pass has one or more tests in aborted state:" -vaasTestManifests $abortedTests
        }

        $pendingTests = GetPendingTests -vaasTestManifests $vaasTestManifests
        if($pendingTests.Count -gt 0)
        {
            $noteData += GenerateWarningBlock -warningTitle "Test pass has one or more tests in a pending state:" -vaasTestManifests $pendingTests
        }

        if($vaasTestsWOLogs.Count -gt 0)
        {
            $noteData += GenerateWarningBlock -warningTitle "Test pass has one or more tests without logs:" -vaasTestManifests $vaasTestsWOLogs
        }
        
        if($vaasTestsWOSupportedLogs.Count -gt 0)
        {
            $noteData += GenerateWarningBlock -warningTitle "Test pass has one or more tests without supported log files (TRX, WTL):" -vaasTestManifests $vaasTestsWOSupportedLogs
        }

        if($vaasTestsLogError.Count -gt 0)
        {
            $noteData += GenerateWarningBlock -warningTitle "Test pass has one or more tests where the log archive could not be extracted:" -vaasTestManifests $vaasTestsLogError
        }
        
        if($noteData -eq "")
        {
            $noteData += "<i>N/A</i><br/>`n"
        }
        else
        {
            $postContent += "<div>`n"
            $postContent += " <ul>`n"
            $noteData += " </ul>`n"
            $noteData += "</div>`n"
        }
    }
    else
    {
        $noteData += "<i>No tests were found for this test pass.</i><br/>`n"
    }

    $postContent += $noteData
    $postContent += "</p>"
    $postContent += "<p class='body'><b>Portal URL:</b><br/>`n"
    $postContent += "<a href='$VaaSPortalUri/TestPass/Manage/Summary?solutionName=$VaaSSolutionName&testPassName=$VaaSTestPassName'>"
    $postContent += "$VaaSPortalUri/TestPass/Manage/Summary?solutionName=$VaaSSolutionName&testPassName=$VaaSTestPassName</a></p>`n"
    $postContent += "<p class='body'><b>Contact:</b><br/>`n"
    $postContent += " <a href=""mailto:vaashelp@microsoft.com"">vaashelp@microsoft.com</a></p>`n"
    $postContent += "<p class='body'><font size=""-1"">Generated using VaaS Reporting Module version 1.1.4</font></p>"

    return $postContent
}

function GetTestDataFromManifest
{
    param(
    [parameter(Mandatory=$true)][AllowEmptyCollection()][Object]$TestManifest
    )

    $testData = New-Object TestData
    $testData.testName = $TestManifest.TestName
    $testData.duration = 0
    $testData.testsPassed = $TestManifest.Passed
    $testData.testsFailed = $TestManifest.Failed
    $testData.testsTotal = $TestManifest.Total
    return $testData
}

function Main
{
    $Container = @()
    $totalTests = 0
    $totalPassed = 0
    $totalFailed = 0
    $totalDuration = New-TimeSpan
    $style = Get-Content -Path ".\VaaSStyle.css"
    $vaasTestsWOLogs = @()
    $vaasTestsWOSupportedLogs = @()
    $vaasTestsLogError = @()
    [bool]$downloadArchive = $true
    $jsonResults = @()    

    if($ProcessTestLogsFromPath -eq "")
    {
        $testArchivePath = $LogFilePath
    }
    else
    {
        $testArchivePath = $ProcessTestLogsFromPath
        $downloadArchive = $false
    }
    
    $vaasTestManifests = GetAllTestsFromTestPass    

    foreach($vaasTestManifest in $vaasTestManifests)
    {
        if($ParseLocalLogs)
        {
            $testArchive = $testArchivePath + "\" + $vaasTestManifest.TestLaunchName + ".zip"
            if($downloadArchive)
            {
                $testArchive = DownloadLogs -testLaunchName $vaasTestManifest.TestLaunchName -testName $vaasTestManifest.TestName -archiveFullName $testArchive
                if($testArchive -eq "")
                {
                    Write-Warning "Skipping test $($vaasTestManifest.TestName)"
                    $vaasTestsWOLogs += $vaasTestManifest
                    continue
                }
            }
            
            if(!(IsLogsExist -testArchive $testArchive))
            {
                Write-Warning "Skipping test $($vaasTestManifest.TestName), as log archive at $testArchive does not exist ..."
                $vaasTestsWOLogs += $vaasTestManifest
                continue
            }
        
            $outputPath = "$testArchivePath\$($vaasTestManifest.TestLaunchName)"
            Write-Host "Expanding Archive $testArchive to path $outputPath ..."
            
            try
            {
                Expand-Archive -LiteralPath "$testArchive" -DestinationPath "$outputPath" -Force -ErrorAction Ignore
            }
            catch
            {
                Write-Error "PowerShell cmdlet Expand-Archive failed to extract log files from $testArchive"
                $vaasTestsLogError += $vaasTestManifest
                continue
            }

            if(IsTrxLogFormat -logPath $outputPath)
            {
                $logFileFullName = GetTrxLogFileFullName -logPath $outputPath
                $testData = ParseTrxLog -trxFullName $logFileFullName
                if($testData -eq $null)
                {
                    $vaasTestsWOSupportedLogs += $vaasTestManifest
                    continue
                }
            }
            elseif(IsWtlLogFormat -logPath $outputPath)
            {
                $logFileFullName = GetWtlLogFileFullName -logPath $outputPath
                $testData = ParseWtlLog -wtlFullName $logFileFullName
                if($testData -eq $null)
                {
                    $vaasTestsWOSupportedLogs += $vaasTestManifest
                    continue
                }
            }
            else
            {
                $vaasTestsWOSupportedLogs += $vaasTestManifest
                continue
            }
        }
        else
        {
            $testData = GetTestDataFromManifest -TestManifest $vaasTestManifest
        }
            
        $jsonResults += $testData
        $totalDuration += $testData.duration
        $totalPassed += $testData.testsPassed
        $totalFailed += $testData.testsFailed
        $totalTests += $testData.testsTotal            

        if ($testData.testsTotal -gt 0)
        {
            if ($testData.testsPassed -gt 0)
            {
                $passedString = 'Green' + $testData.testsPassed.ToString()
            }
            else
            {
                $passedString = 'Red' + $testData.testsPassed.ToString()
            }
            
            $passedPerc = [math]::round(($testData.testsPassed/$testData.testsTotal)*100,2)
            if($passedPerc -gt 0)
            {
                $passedPercString = 'Green' + $passedPerc.ToString()
            }
            else
            {
                $passedPercString = 'Red' + $passedPerc.ToString()
            }            
            
            $failedPerc = [math]::round(($testData.testsFailed/$testData.testsTotal)*100,2)
            if (($failedPerc -eq 0) -and ($passedPerc -gt 0))
            {
                $failedPercString= 'Green' + $failedPerc.ToString()
            }
            else
            {
                $failedPercString= 'Red' + $failedPerc.ToString()
            }
            
            if (($testData.testsFailed -eq 0) -and ($testData.testsPassed -gt 0))
            {
                $failedString = 'Green' + $testData.testsFailed.ToString()
            }
            else
            {
                $failedString = 'Red' + $testData.testsFailed.ToString()
            }
        }
        else
        {
            $passedString = $failedString = $passedPercString = $failedPercString = 'Red0';
        }
        $totalString = "$($testData.testsTotal)"
        $testLaunchNameString = $vaasTestManifest.TestLaunchName.ToString()
        $logsLink = "<a href='$VaaSPortalUri/TestPass/Manage/DownloadLog?solutionName=$VaaSSolutionName&testPassName=$VaaSTestPassName&testManifestName=$testLaunchNameString'>Download</a>"
                        
        $blob = New-Object System.Object
        $blob | Add-Member -type NoteProperty -name "Test Name" -value "$($vaasTestManifest.TestName)"
        $blob | Add-Member -type NoteProperty -name Total -value $totalString
        $blob | Add-Member -type NoteProperty -name Passed -value $passedString
        $blob | Add-Member -type NoteProperty -name Failed -value $failedString
        $blob | Add-Member -type NoteProperty -name Passed% -value "$passedPercString%"
        $blob | Add-Member -type NoteProperty -name Failed% -value "$failedPercString%"
        $blob | Add-Member -type NoteProperty -name Duration -Value $testData.duration.ToString();
        $blob | Add-Member -type NoteProperty -name Logs -Value "$logsLink"
        $Container += $blob
    }

    if ($totalTests -gt 0)
    {
        $totalPassedPerc = 'Green' + [math]::round(($totalPassed/$totalTests)*100,2)
        $totalFailedPercVal = [math]::round(($totalFailed/$totalTests)*100,2)
    }
    else
    {
        $totalPassedPerc = 'Red0';
        $totalFailedPercVal = 0
    }
    
    $totalPassedString = "Green$totalPassed"
    if ($totalFailedPercVal -eq 0)
    {
        $totalFailedPerc = 'Green' + $totalFailedPercVal.ToString();
    }
    else
    {
        $totalFailedPerc = 'Red' + $totalFailedPercVal.ToString();
    }
    
    if ($totalFailed -eq 0) {
        $totalFailedString = "Green$totalFailed"
    }
    else
    {
        $totalFailedString = "Red$totalFailed"
    }
    $totalTestsString = "$totalTests"

    $blob = New-Object System.Object
    $blob | Add-Member -type NoteProperty -name "Test Name" -value "Total"
    $blob | Add-Member -type NoteProperty -name Total -value $totalTestsString
    $blob | Add-Member -type NoteProperty -name Passed -value $totalPassedString
    $blob | Add-Member -type NoteProperty -name Failed -value $totalFailedString
    $blob | Add-Member -type NoteProperty -name Passed% -value "$totalPassedPerc%"
    $blob | Add-Member -type NoteProperty -name Failed% -value "$totalFailedPerc%"
    $blob | Add-Member -type NoteProperty -name Duration -Value $totalDuration.ToString()
    $Container += $blob

    $preContent = GeneratePreContent
    $postContent = GeneratePostContent -vaasTestManifests $vaasTestManifests -vaasTestsWOLogs $vaasTestsWOLogs -vaasTestsWOSupportedLogs $vaasTestsWOSupportedLogs -vaasTestsLogError $vaasTestsLogError
    $results = $Container | ConvertTo-Html -Head $style -PreContent $preContent -PostContent $postContent
    $results = $results -Replace "<td>Green","<td class='pass'>"
    $results = $results -Replace "<td>Red","<td class='fail'>"
    $results = $results -Replace "&lt;","<"
    $results = $results -Replace "&gt;",">"
    $results = $results -Replace "&#39;","'"
    $results | Out-File -FilePath ".\VaaSResults-$VaaSSolutionName-$VaaSTestPassName.html" -Encoding ASCII
    $jsonResults | ConvertTo-Json | Out-File -FilePath ".\VaaSResults-$VaaSSolutionName-$VaaSTestPassName.json"
}

Set-PSDebug -Strict
if($LogFilePath -eq "")
{
    $LogFilePath = (Get-Item -Path ".\" -Verbose).FullName + '\Logs'
    Write-Host "Defaulting to VaaS log file path $LogFilePath, as a log file path was not provided ..."
}
CreateVaaSLogDirectory

$logFileName = "$LogFilePath\Generate-VaaSReport_$(Get-Date -Format yyyyMMdd-HHmmss).log"
Start-Transcript -Path $logFileName
Write-Output "`r`n"
[int]$reportResult = 0

try
{
    Write-Output "PsBoundParameters: "
    foreach($keyValue in $PsBoundParameters.GetEnumerator())
    {
        Write-Output "$($keyValue.Key)=$($keyValue.Value)"
    }    
    Write-Output ""

    ValidateAndResolveParameters
    InstallVaaSPSModules
    Import-Module AzureStackVaaS -Force -ErrorAction Stop
    Write-Host "`r`nAzureStackVaaS module version loaded: $((Get-Module -Name AzureStackVaaS).Version.ToString())`r`n"
    AddAzureAccount
    Main
}
catch
{
    $_ | Format-List * -Force | Out-Default;
    $_.InvocationInfo | Format-List * | Out-Default
    $Exception = $_.Exception
    for ($i = 0; $Exception; $i++, ($Exception = $_.InnerException))
    {
      "$i" * 80
      $Exception | Format-List * -Force | Out-Default
    }
    $reportResult = 1
}
finally
{
    Remove-Module AzureStackVaaS -Force -ErrorAction Ignore
    Stop-Transcript
}

return $reportResult
# SIG # Begin signature block
# MIIdpQYJKoZIhvcNAQcCoIIdljCCHZICAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUfGAqLnFZvxl76FV9B2b0jwY3
# aGygghhlMIIEwzCCA6ugAwIBAgITMwAAALWsfW2HayYRRwAAAAAAtTANBgkqhkiG
# 9w0BAQUFADB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEw
# HwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwHhcNMTYwOTA3MTc1ODQ0
# WhcNMTgwOTA3MTc1ODQ0WjCBszELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjENMAsGA1UECxMETU9QUjEnMCUGA1UECxMebkNpcGhlciBEU0UgRVNO
# OkI4RUMtMzBBNC03MTQ0MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApXwz2j7k2rDl
# 2QO9eyz1qUm3FyqD7dksbP5M3NCOq/j95vpOeHG2w0S1SyNmN8VEqjiHSeopO5b+
# VbOIbpqqG9PyfyDc0WdzIilufZOuwyZI15hI3uRgZ78E/cbljXUW5Me75jGGEOlr
# Gek41eOyGRUxkejFapqkiHCLxHSMHEpPdT95ylPhuLz7Bq01fsQSbclDoQye3EzO
# YFlqcFMYb3s61siEbpvKgf0qcQjPzAh3vsySXqzeeLc3Kzss74E9HDduQGO1ZZTZ
# FadL4bzwlgVhux25DZr0zqybZIBiy8/J9oyKCi2OuWLqxf+YgSWp0YMY9ktvKwGr
# VW7W8/UJVwIDAQABo4IBCTCCAQUwHQYDVR0OBBYEFIMd6iA083bzGHST2k2O6R6l
# XnyFMB8GA1UdIwQYMBaAFCM0+NlSRnAK7UD7dvuzK7DDNbMPMFQGA1UdHwRNMEsw
# SaBHoEWGQ2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3Rz
# L01pY3Jvc29mdFRpbWVTdGFtcFBDQS5jcmwwWAYIKwYBBQUHAQEETDBKMEgGCCsG
# AQUFBzAChjxodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY3Jv
# c29mdFRpbWVTdGFtcFBDQS5jcnQwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZI
# hvcNAQEFBQADggEBAAez+vxJWgDsgMtouMLKUcbt+zRbXcxWm2HmTU7rhIVVyh2E
# IFS5ebVknSGsKoR1/xlEmnMo3fHtvWaDRo/2qXIg1jMnOQp1d4wqFh9hKfnDeCQA
# 9tCnM8C/mYu3axXxKmyxJXDOm2MqcoZ9CBlmk96o/hzV9QWo5c+Y94j7qEYpGRPG
# 6Adqoc/HNxnce3Ik0ZlpbD8TbmbIjDORxQ3Jjbn3AGXBQ+smsInwWFzut2EwpGPC
# 2xWhLjXLdzJReIM1geh3oM/wti4zZ4w7hr4CvedMnU29OkcnoyMEUAQnZfB7PsXm
# adKxnklsJCsr1UOu7g/nwX5/mcw7R9G3RSvrI0EwggYHMIID76ADAgECAgphFmg0
# AAAAAAAcMA0GCSqGSIb3DQEBBQUAMF8xEzARBgoJkiaJk/IsZAEZFgNjb20xGTAX
# BgoJkiaJk/IsZAEZFgltaWNyb3NvZnQxLTArBgNVBAMTJE1pY3Jvc29mdCBSb290
# IENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0wNzA0MDMxMjUzMDlaFw0yMTA0MDMx
# MzAzMDlaMHcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xITAf
# BgNVBAMTGE1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQTCCASIwDQYJKoZIhvcNAQEB
# BQADggEPADCCAQoCggEBAJ+hbLHf20iSKnxrLhnhveLjxZlRI1Ctzt0YTiQP7tGn
# 0UytdDAgEesH1VSVFUmUG0KSrphcMCbaAGvoe73siQcP9w4EmPCJzB/LMySHnfL0
# Zxws/HvniB3q506jocEjU8qN+kXPCdBer9CwQgSi+aZsk2fXKNxGU7CG0OUoRi4n
# rIZPVVIM5AMs+2qQkDBuh/NZMJ36ftaXs+ghl3740hPzCLdTbVK0RZCfSABKR2YR
# JylmqJfk0waBSqL5hKcRRxQJgp+E7VV4/gGaHVAIhQAQMEbtt94jRrvELVSfrx54
# QTF3zJvfO4OToWECtR0Nsfz3m7IBziJLVP/5BcPCIAsCAwEAAaOCAaswggGnMA8G
# A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCM0+NlSRnAK7UD7dvuzK7DDNbMPMAsG
# A1UdDwQEAwIBhjAQBgkrBgEEAYI3FQEEAwIBADCBmAYDVR0jBIGQMIGNgBQOrIJg
# QFYnl+UlE/wq4QpTlVnkpKFjpGEwXzETMBEGCgmSJomT8ixkARkWA2NvbTEZMBcG
# CgmSJomT8ixkARkWCW1pY3Jvc29mdDEtMCsGA1UEAxMkTWljcm9zb2Z0IFJvb3Qg
# Q2VydGlmaWNhdGUgQXV0aG9yaXR5ghB5rRahSqClrUxzWPQHEy5lMFAGA1UdHwRJ
# MEcwRaBDoEGGP2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1
# Y3RzL21pY3Jvc29mdHJvb3RjZXJ0LmNybDBUBggrBgEFBQcBAQRIMEYwRAYIKwYB
# BQUHMAKGOGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljcm9z
# b2Z0Um9vdENlcnQuY3J0MBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEB
# BQUAA4ICAQAQl4rDXANENt3ptK132855UU0BsS50cVttDBOrzr57j7gu1BKijG1i
# uFcCy04gE1CZ3XpA4le7r1iaHOEdAYasu3jyi9DsOwHu4r6PCgXIjUji8FMV3U+r
# kuTnjWrVgMHmlPIGL4UD6ZEqJCJw+/b85HiZLg33B+JwvBhOnY5rCnKVuKE5nGct
# xVEO6mJcPxaYiyA/4gcaMvnMMUp2MT0rcgvI6nA9/4UKE9/CCmGO8Ne4F+tOi3/F
# NSteo7/rvH0LQnvUU3Ih7jDKu3hlXFsBFwoUDtLaFJj1PLlmWLMtL+f5hYbMUVbo
# nXCUbKw5TNT2eb+qGHpiKe+imyk0BncaYsk9Hm0fgvALxyy7z0Oz5fnsfbXjpKh0
# NbhOxXEjEiZ2CzxSjHFaRkMUvLOzsE1nyJ9C/4B5IYCeFTBm6EISXhrIniIh0EPp
# K+m79EjMLNTYMoBMJipIJF9a6lbvpt6Znco6b72BJ3QGEe52Ib+bgsEnVLaxaj2J
# oXZhtG6hE6a/qkfwEm/9ijJssv7fUciMI8lmvZ0dhxJkAj0tr1mPuOQh5bWwymO0
# eFQF1EEuUKyUsKV4q7OglnUa2ZKHE3UiLzKoCG6gW4wlv6DvhMoh1useT8ma7kng
# 9wFlb4kLfchpyOZu6qeXzjEp/w7FW1zYTRuh2Povnj8uVRZryROj/TCCBhEwggP5
# oAMCAQICEzMAAACOh5GkVxpfyj4AAAAAAI4wDQYJKoZIhvcNAQELBQAwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMTAeFw0xNjExMTcyMjA5MjFaFw0xODAy
# MTcyMjA5MjFaMIGDMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ
# MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u
# MQ0wCwYDVQQLEwRNT1BSMR4wHAYDVQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24w
# ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQh9RCK36d2cZ61KLD4xWS
# 0lOdlRfJUjb6VL+rEK/pyefMJlPDwnO/bdYA5QDc6WpnNDD2Fhe0AaWVfIu5pCzm
# izt59iMMeY/zUt9AARzCxgOd61nPc+nYcTmb8M4lWS3SyVsK737WMg5ddBIE7J4E
# U6ZrAmf4TVmLd+ArIeDvwKRFEs8DewPGOcPUItxVXHdC/5yy5VVnaLotdmp/ZlNH
# 1UcKzDjejXuXGX2C0Cb4pY7lofBeZBDk+esnxvLgCNAN8mfA2PIv+4naFfmuDz4A
# lwfRCz5w1HercnhBmAe4F8yisV/svfNQZ6PXlPDSi1WPU6aVk+ayZs/JN2jkY8fP
# AgMBAAGjggGAMIIBfDAfBgNVHSUEGDAWBgorBgEEAYI3TAgBBggrBgEFBQcDAzAd
# BgNVHQ4EFgQUq8jW7bIV0qqO8cztbDj3RUrQirswUgYDVR0RBEswSaRHMEUxDTAL
# BgNVBAsTBE1PUFIxNDAyBgNVBAUTKzIzMDAxMitiMDUwYzZlNy03NjQxLTQ0MWYt
# YmM0YS00MzQ4MWU0MTVkMDgwHwYDVR0jBBgwFoAUSG5k5VAF04KqFzc3IrVtqMp1
# ApUwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br
# aW9wcy9jcmwvTWljQ29kU2lnUENBMjAxMV8yMDExLTA3LTA4LmNybDBhBggrBgEF
# BQcBAQRVMFMwUQYIKwYBBQUHMAKGRWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w
# a2lvcHMvY2VydHMvTWljQ29kU2lnUENBMjAxMV8yMDExLTA3LTA4LmNydDAMBgNV
# HRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBEiQKsaVPzxLa71IxgU+fKbKhJ
# aWa+pZpBmTrYndJXAlFq+r+bltumJn0JVujc7SV1eqVHUqgeSxZT8+4PmsMElSnB
# goSkVjH8oIqRlbW/Ws6pAR9kRqHmyvHXdHu/kghRXnwzAl5RO5vl2C5fAkwJnBpD
# 2nHt5Nnnotp0LBet5Qy1GPVUCdS+HHPNIHuk+sjb2Ns6rvqQxaO9lWWuRi1XKVjW
# kvBs2mPxjzOifjh2Xt3zNe2smjtigdBOGXxIfLALjzjMLbzVOWWplcED4pLJuavS
# Vwqq3FILLlYno+KYl1eOvKlZbiSSjoLiCXOC2TWDzJ9/0QSOiLjimoNYsNSa5jH6
# lEeOfabiTnnz2NNqMxZQcPFCu5gJ6f/MlVVbCL+SUqgIxPHo8f9A1/maNp39upCF
# 0lU+UK1GH+8lDLieOkgEY+94mKJdAw0C2Nwgq+ZWtd7vFmbD11WCHk+CeMmeVBoQ
# YLcXq0ATka6wGcGaM53uMnLNZcxPRpgtD1FgHnz7/tvoB3kH96EzOP4JmtuPe7Y6
# vYWGuMy8fQEwt3sdqV0bvcxNF/duRzPVQN9qyi5RuLW5z8ME0zvl4+kQjOunut6k
# LjNqKS8USuoewSI4NQWF78IEAA1rwdiWFEgVr35SsLhgxFK1SoK3hSoASSomgyda
# Qd691WZJvAuceHAJvDCCB3owggVioAMCAQICCmEOkNIAAAAAAAMwDQYJKoZIhvcN
# AQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAw
# BgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEx
# MB4XDTExMDcwODIwNTkwOVoXDTI2MDcwODIxMDkwOVowfjELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUg
# U2lnbmluZyBQQ0EgMjAxMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AKvw+nIQHC6t2G6qghBNNLrytlghn0IbKmvpWlCquAY4GgRJun/DDB7dN2vGEtgL
# 8DjCmQawyDnVARQxQtOJDXlkh36UYCRsr55JnOloXtLfm1OyCizDr9mpK656Ca/X
# llnKYBoF6WZ26DJSJhIv56sIUM+zRLdd2MQuA3WraPPLbfM6XKEW9Ea64DhkrG5k
# NXimoGMPLdNAk/jj3gcN1Vx5pUkp5w2+oBN3vpQ97/vjK1oQH01WKKJ6cuASOrdJ
# Xtjt7UORg9l7snuGG9k+sYxd6IlPhBryoS9Z5JA7La4zWMW3Pv4y07MDPbGyr5I4
# ftKdgCz1TlaRITUlwzluZH9TupwPrRkjhMv0ugOGjfdf8NBSv4yUh7zAIXQlXxgo
# tswnKDglmDlKNs98sZKuHCOnqWbsYR9q4ShJnV+I4iVd0yFLPlLEtVc/JAPw0Xpb
# L9Uj43BdD1FGd7P4AOG8rAKCX9vAFbO9G9RVS+c5oQ/pI0m8GLhEfEXkwcNyeuBy
# 5yTfv0aZxe/CHFfbg43sTUkwp6uO3+xbn6/83bBm4sGXgXvt1u1L50kppxMopqd9
# Z4DmimJ4X7IvhNdXnFy/dygo8e1twyiPLI9AN0/B4YVEicQJTMXUpUMvdJX3bvh4
# IFgsE11glZo+TzOE2rCIF96eTvSWsLxGoGyY0uDWiIwLAgMBAAGjggHtMIIB6TAQ
# BgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQUSG5k5VAF04KqFzc3IrVtqMp1ApUw
# GQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB
# /wQFMAMBAf8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0f
# BFMwUTBPoE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJv
# ZHVjdHMvTWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcB
# AQRSMFAwTgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kv
# Y2VydHMvTWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDCBnwYDVR0gBIGX
# MIGUMIGRBgkrBgEEAYI3LgMwgYMwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvZG9jcy9wcmltYXJ5Y3BzLmh0bTBABggrBgEFBQcC
# AjA0HjIgHQBMAGUAZwBhAGwAXwBwAG8AbABpAGMAeQBfAHMAdABhAHQAZQBtAGUA
# bgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAZ/KGpZjgVHkaLtPYdGcimwuWEeFj
# kplCln3SeQyQwWVfLiw++MNy0W2D/r4/6ArKO79HqaPzadtjvyI1pZddZYSQfYtG
# UFXYDJJ80hpLHPM8QotS0LD9a+M+By4pm+Y9G6XUtR13lDni6WTJRD14eiPzE32m
# kHSDjfTLJgJGKsKKELukqQUMm+1o+mgulaAqPyprWEljHwlpblqYluSD9MCP80Yr
# 3vw70L01724lruWvJ+3Q3fMOr5kol5hNDj0L8giJ1h/DMhji8MUtzluetEk5CsYK
# wsatruWy2dsViFFFWDgycScaf7H0J/jeLDogaZiyWYlobm+nt3TDQAUGpgEqKD6C
# PxNNZgvAs0314Y9/HG8VfUWnduVAKmWjw11SYobDHWM2l4bf2vP48hahmifhzaWX
# 0O5dY0HjWwechz4GdwbRBrF1HxS+YWG18NzGGwS+30HHDiju3mUv7Jf2oVyW2ADW
# oUa9WfOXpQlLSBCZgB/QACnFsZulP0V3HjXG0qKin3p6IvpIlR+r+0cjgPWe+L9r
# t0uX4ut1eBrs6jeZeRhL/9azI2h15q/6/IvrC4DqaTuv/DDtBEyO3991bWORPdGd
# Vk5Pv4BXIqF4ETIheu9BCrE/+6jMpF3BoYibV3FWTkhFwELJm3ZbCoBIa/15n8G9
# bW1qyVJzEw16UM0xggSqMIIEpgIBATCBlTB+MQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5n
# IFBDQSAyMDExAhMzAAAAjoeRpFcaX8o+AAAAAACOMAkGBSsOAwIaBQCggb4wGQYJ
# KoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQB
# gjcCARUwIwYJKoZIhvcNAQkEMRYEFFEH/hJJCMan0bhDrcyak1SaHw0bMF4GCisG
# AQQBgjcCAQwxUDBOoDCALgBHAGUAbgBlAHIAYQB0AGUALQBWAGEAYQBTAFIAZQBw
# AG8AcgB0AC4AcABzADGhGoAYaHR0cDovL3d3dy5taWNyb3NvZnQuY29tMA0GCSqG
# SIb3DQEBAQUABIIBAEDpqiyz7w3+x/R+h/BPecum8lsv66wc1dgV3YdgyI2YWzCH
# +vAqL8RXvBeKhliinaxtWRGaKRfljBOi3Zodv6jtYhyFcXiEVqKsY9Vc8Is6AtD3
# T9I11IpXENA/VuQY3dGjUorSOyGoB/Pk3mpHwv1QpFe2Pa0A2ztIOfGKBYQW7sM4
# 8/2jeEiFVw4ePixbUFrnHtz06R472WOWVd0weNoxxzD3f8RUtohi8QMdMdgpzmi8
# 7kyBhnPpq3Vi0OMWVqzZbLEl4PsuZQgBQav9I2dshFYjwjPexobQuEr8q3lKZaZ1
# iyPdZg322zUP10u9AdCqIJjhRi2peOTe+KvfWEKhggIoMIICJAYJKoZIhvcNAQkG
# MYICFTCCAhECAQEwgY4wdzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBAhMzAAAAtax9
# bYdrJhFHAAAAAAC1MAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
# AQcBMBwGCSqGSIb3DQEJBTEPFw0xNzAzMjkyMDIyMDNaMCMGCSqGSIb3DQEJBDEW
# BBQmYDYYJcpFtgrw1KrVaSMsmhGljDANBgkqhkiG9w0BAQUFAASCAQBBy3Cj4KoE
# zjUfa0pLcZkeA920nnYAF48iGqknNnLz/lbV3dSiL1HWdf1winKyEMwOpUiLg5Y6
# WpEmsILAEsjrzNuBuY2l+E+2YZkrfaUpxcFmow+GkyAxlMBgVplv6xXaoFbQ8d48
# 977fU4okUo7NJwrvoiu/40QXyE8PAWFjOGd4P0EWBZkpsQXrjlBBg1ZmVCk/6yzn
# meoAv1IFgIErIhfMejzXXqV2swVnP/ROiHEe18JIppyRWfa1y3RxeLGU5aZMxKNj
# 6r5RHM5FMaxS2A785Wn/3kLp+EL9N17FvKwoHYO8w43Z+kUzgZrILcqg8LHv4GaV
# HSu5Ni+MCNOr
# SIG # End signature block