AzStackHci.EnvironmentChecker.Reporting.psm1
<#
.SYNOPSIS Common Reporting functions across all modules/scenarios .DESCRIPTION Logging, Reporting .INPUTS Inputs (if any) .OUTPUTS Output (if any) .NOTES General notes #> function Set-AzStackHciOutputPath { param ($Path, $Source='AzStackHciEnvironmentChecker/Diagnostic') if ([string]::IsNullOrEmpty($Path)) { $Path = Join-Path -Path $HOME -ChildPath ".AzStackHci" } $Global:AzStackHciEnvironmentLogFile = Join-Path -Path $Path -ChildPath 'AzStackHciEnvironmentChecker.log' $Global:AzStackHciEnvironmentReport = Join-Path -Path $Path -ChildPath 'AzStackHciEnvironmentReport.json' $Global:AzStackHciEnvironmentReportXml = Join-Path -Path $Path -ChildPath 'AzStackHciEnvironmentReport.xml' Assert-EventLog -source $Source Set-AzStackHciIdentifier } Import-LocalizedData -BindingVariable lTxt -FileName AzStackHci.EnvironmentChecker.Strings.psd1 function Get-AzStackHciEnvProgress { <# .SYNOPSIS Look for existing progress or create new progress. .DESCRIPTION Finds either the latest progress XML file or creates a new progress XML file .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS Clean switch, in case the user wants to start fresh Path to search for progress run. .OUTPUTS PSCustomObject of progress. .NOTES #> param ([switch]$clean, $path = $PSScriptRoot) $latestReport = Get-Item -Path $Global:AzStackHciEnvironmentReportXml -ErrorAction SilentlyContinue try { $report = Import-Clixml $latestReport.FullName } catch {} if (-not $clean -and $latestReport -and $report) { Log-Info -Message ('Found existing report: {0}' -f $report.FilePath) } else { $hash = @{ FilePath = $Global:AzStackHciEnvironmentReportXml Version = $MyInvocation.MyCommand.Module.Version.ToString() Jobs = @{} } $report = New-Object PSObject -Property $hash Log-Info -Message ('Creating new report {0}' -f $report.FilePath) } $report } function Write-AzStackHciEnvProgress { <# .SYNOPSIS Write report output to JSON .DESCRIPTION After all processing, take results object and convert to JSON report. Any file already existing will be overwritten. .EXAMPLE Write-AzStackHciEnvProgress -report $report Writes $report to JSON file .INPUTS [psobject] .OUTPUTS XML file on disk (path on disk is expected to be embedded in psobject) .NOTES General notes #> param ([psobject]$report) try { $report | Export-Clixml -Depth 10 -Path $report.FilePath -Force Log-Info -Message ('AzStackHCI progress written: {0}' -f $report.FilePath) } Catch { Log-Info -Message ('Writing XML progress to disk error {0}' -f $_.exception.message) -Type Error } } function Add-AzStackHciEnvJob { <# .SYNOPSIS Adds a 'Job' to the progress object. .DESCRIPTION If a user runs the tool multiple time to check different assets e.g. Certificates on one execution and Registration details on the next execution Those executions are added to the progress for tracking purposes. Execution/Job details include: start time, parameters, parameterset (indicating what is being checked, certificates or Azure Accounts), Placeholders for EndTime and Duration (later filled in by Close-AzStackHciEnvJob) .EXAMPLE Add-AzStackHciEnvJob -report $report Adds execution job to progress object ($report) .INPUTS Report - psobject - containing all progress to date .OUTPUTS Report - psobject - updated with execution job log. .NOTES General notes #> param ($report) $allJobs = @{} $alljobs = $report.Jobs # Index for jobs must be a string for json conversion later if ($alljobs.Count) { $jobCount = ($alljobs.Count++).tostring() } else { $jobCount = '0' } # Record current job $currentJob = @{ Index = $jobCount StartTime = (Get-Date -f 'yyyy/MM/dd HH:mm:ss') EndTime = $null Duration = $null } Log-Info -Message ('Adding current job to progress: {0}' -f $currentJob) # Add current job $allJobs += @{"$jobcount" = $currentJob } $report.Jobs = $allJobs $report } function Close-AzStackHciEnvJob { <# .SYNOPSIS Writes endtime and duration for jobs .DESCRIPTION Find latest job entry and update time and calculates duration calls function to update xml on disk and updates and returns report object .EXAMPLE Close-AzStackHciEnvJob -report $report .INPUTS Report - psobject - containing all progress to date .OUTPUTS Report - psobject - updated with finished execution job log. .NOTES General notes #> param ($report) try { $latestJob = $report.jobs.Keys -match '[0-9]' | ForEach-Object { [int]$_ } | Sort-Object -Descending | Select-Object -First 1 $report.jobs["$latestJob"].EndTime = (Get-Date -f 'yyyy/MM/dd HH:mm:ss') $duration = (([dateTime]$report.jobs["$latestJob"].EndTime) - ([dateTime]$report.jobs["$latestJob"].StartTime)).TotalSeconds $report.jobs["$latestJob"].Duration = $duration Log-Info -Message ('Updating current job to progress with endTime: {0} and duration {1}' -f $report.jobs["$latestJob"].EndTime, $duration) Write-AzStackHciEnvProgress -report $report } Catch { Log-Info -Message ('Updating current job to progress failed with exception: {0}' -f $_.exception) -Type Error } $report } function Write-AzStackHciEnvReport { <# .SYNOPSIS Writes progress to disk in JSON format .DESCRIPTION Write progress object to disk in JSON format, overwriting as neccessary. The resulting blob is intended to be a portable record of what has been checked including the results of that check .EXAMPLE Write-AzStackHciEnvReport -report $report .INPUTS Report - psobject - containing all progress to date .OUTPUTS JSON - file - named AzStackEnvReport.json .NOTES General notes #> param ([psobject]$report) try { ConvertTo-Json -InputObject $report -Depth 8 -WarningAction SilentlyContinue | Out-File $AzStackHciEnvironmentReport -Force -Encoding UTF8 Log-Info -Message ('JSON report written to {0}' -f $AzStackHciEnvironmentReport) } catch { Log-Info -Message ('Writing JSON report failed:' -f $_.exception.message) -Type Error } } function Log-Info { <# .SYNOPSIS Write verbose logging to disk .DESCRIPTION Formats and writes verbose logging to disk under scriptroot. Log type (or severity) is essentially cosmetic to the verbose log file, no action should be inferred, such as termination of the script. .EXAMPLE Write-AzStackHciEnvironmentLog -Message ('Script messaging include data {0}' -f $data) -Type 'Info|Warning|Error' -Function 'FunctionName' .INPUTS Message - a string of the body of the log entry Type - a cosmetic type or severity for the message, must be info, warning or error Function - ideally the name of the function or the script writing the log entry. .OUTPUTS Appends Log entry to AzStackHciEnvironmentChecker.log under the script root. .NOTES General notes #> [cmdletbinding()] param( [string] $Message, [ValidateSet('INFO', 'INFORMATIONAL', 'WARNING', 'CRITICAL', 'ERROR', 'SUCCESS')] [string] $Type = 'INFORMATIONAL', [ValidateNotNullOrEmpty()] [string]$Function = ((Get-PSCallStack)[0].Command), [switch]$ConsoleOut, [switch]$Telemetry ) $Message = RunMask $Message if ($ConsoleOut) { #if ($PSEdition -eq 'desktop') if ($true) { switch -wildcard ($function) { '*-AzStackHciEnvironment*' { $foregroundcolor = 'DarkYellow' } default { $foregroundcolor = "White" } } switch ($Type) { 'SUCCESS' { $foregroundcolor = 'Green' } 'WARNING' { $foregroundcolor = 'Yellow' } 'CRITICAL' { $foregroundcolor = 'Red' } 'ERROR' { $foregroundcolor = 'Red' } default { $foregroundcolor = "White" } } Write-Host $message -ForegroundColor $foregroundcolor } else { Write-Host $message } } else { Write-Verbose $message } if (-not [string]::IsNullOrEmpty($message)) { # Log to ETW if ($Telemetry) { $source = "AzStackHciEnvironmentChecker/Telemetry" $EventId = 17201 } else { $source = "AzStackHciEnvironmentChecker/Operational" $EventId = 17203 } $logName = 'AzStackHciEnvironmentChecker' $EventType = switch ($Type) { "ERROR" { "Error" } "CRITICAL" { "Error" } "WARNING" { 'Warning' } "SUCCESS" { "Information" } "INFORMATIONAL" { "Information" } Default { "Information" } } # Only write telemetry or non-info entries to the eventlog to save time and noise. if ($Telemetry -or $EventType -ne "Information") { Write-ETWLog -Source $Source -logName $logName -Message $Message -EventType $EventType -EventId $EventId } # Log to file $entry = "[{0}] [{1}] [{2}] {3}" -f ([datetime]::now).tostring(), $type.ToUpper(), $function, ($Message -replace "`n|`t", "") # If the log file path doesnt exist, create it if ([string]::IsNullOrEmpty($AzStackHciEnvironmentLogFile)) { Set-AzStackHciOutputPath } if (-not (Test-Path $AzStackHciEnvironmentLogFile)) { New-Item -Path $AzStackHciEnvironmentLogFile -Force | Out-Null } $retries = 3 for ($i = 1; $i -le $retries; $i++) { try { $entry | Out-File -FilePath $AzStackHciEnvironmentLogFile -Append -Force -Encoding UTF8 $writeFailed = $false break } catch { $writeFailed = "Log-info $i/$retries failed: $($_.ToString())" start-sleep -Seconds 5 } } if ($writeFailed) { throw $writeFailed } } } function RunMask { [cmdletbinding()] [OutputType([string])] Param ( [Parameter(ValueFromPipeline = $True)] [string] $in ) Begin {} Process { try { <#$in | Get-PIIMask | Get-GuidMask#> $in | Get-GuidMask } catch { $_.exception } } End {} } function Get-PIIMask { [cmdletbinding()] [OutputType([string])] Param ( [Parameter(ValueFromPipeline = $True)] [string] $in ) Begin { $pii = $($ENV:USERDNSDOMAIN), $($ENV:COMPUTERNAME), $($ENV:USERNAME), $($ENV:USERDOMAIN) | ForEach-Object { if ($null -ne $PSITEM) { $PSITEM } } $r = $pii -join '|' } Process { try { return [regex]::replace($in, $r, "[*redacted*]") } catch { $_.exception } } End {} } function Get-GuidMask { [OutputType([string])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $True)] [String] $guid ) Begin { $r = [regex]::new("(-([a-fA-F0-9]{4}-){3})") } Process { try { return [regex]::replace($guid, $r, "-xxxx-xxxx-xxxx-") } catch { $_.exception } } End {} } function Write-AzStackHciHeader { <# .SYNOPSIS Write invocation and system information into log and writes cmdlet name and version to screen. #> param ( [Parameter()] [System.Management.Automation.InvocationInfo] $invocation, [psobject] $params, [switch] $PassThru ) try { $paramToString = (($params | Protect-SensitiveProperties).GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ';' $cmdLetName = Get-CmdletName $cmdletVersion = (Get-Command $cmdletName -ErrorAction SilentlyContinue).version.tostring() Log-Info -Message '' Log-Info -Message ('{0} v{1} started.' -f ` $cmdLetName, $cmdletVersion) ` -ConsoleOut:(-not $PassThru) Log-Info -Telemetry -Message ('{0} started version: {1} with parameters: {2}. Id:{3}' ` -f $cmdLetName, (Get-Module AzStackHci.EnvironmentChecker).Version.ToString(), $paramToString, $ENV:EnvChkrId) Log-Info -Message ('OSVersion: {0} PSVersion: {1} PSEdition: {2} Security Protocol: {3} Lanaguage Mode: {4}' -f ` [environment]::OSVersion.Version.tostring(), $PSVersionTable.PSVersion.tostring(), $PSEdition, [Net.ServicePointManager]::SecurityProtocol, $ExecutionContext.SessionState.LanguageMode) Write-PsSessionInfo -params $params } catch { if (-not $PassThru) { Log-Info ("Unable to write header to screen. Error: {0}" -f $_.exception.message) } } } function Write-AzStackHciFooter { <# .SYNOPSIS Writes report, log and cmdlet to screen. #> param ( [Parameter()] [System.Management.Automation.InvocationInfo] $invocation, [System.Management.Automation.ErrorRecord] $Exception, [switch] $PassThru ) Log-Info -Message ("`nLog location: $AzStackHciEnvironmentLogFile") -ConsoleOut:(-not $PassThru) Log-Info -Message ("Report location: $AzStackHciEnvironmentReport") -ConsoleOut:(-not $PassThru) Log-Info -Message ("Use -Passthru parameter to return results as a PSObject.") -ConsoleOut:(-not $PassThru) if ($Exception) { Log-Info -Message ("{0} failed." -f (Get-CmdletName)) -ConsoleOut:(-not $PassThru) -Type Error Log-Info -Message ("{0} failed. Id:{1}. Exeception: {2}" -f (Get-CmdletName),$ENV:EnvChkrId,$Exception) -Type Error -Telemetry } else { Log-Info -Message ("{0} completed. Id:{1} " -f (Get-CmdletName),$ENV:EnvChkrId) -Telemetry } } function Get-CmdletName { try { foreach ($c in (Get-PSCallStack).Command) { $functionCalled = Select-String -InputObject $c -Pattern "Invoke-AzStackHci(.*)Validation" if ($functionCalled) { break } } $functionCalled } catch { throw "Hci Validation" } } function Write-AzStackHciResult { <# .SYNOPSIS Displays results to screen .DESCRIPTION Displays test results to screen, highlighting failed tests. #> param ( [Parameter()] [string] $Title, [Parameter()] [psobject] $result, $seperator = ' -> ', [switch] $Expand, [switch] $ShowFailedOnly ) try { if (-not $result) { throw "Results missing. Ensure tests ran successfully." } Log-Info ("`n{0}:" -f $Title) -ConsoleOut foreach ($r in ($result | Sort-Object Status, Title, Description)) { if ($r.status -ne 'SUCCESS' -or $Expand) { Write-StatusSymbol -Status $r.Status -Severity $r.Severity Write-Host " " -NoNewline Write-Host @expandDownSymbol Write-Host " " -NoNewline if ($r.status -ne 'SUCCESS') { switch ($r.Severity) { Critical { Write-Host @needsRemediation } Warning { Write-Host @needsAttention } Informational { Write-Host @forInformation } Default { Write-Host @Critical } } } Write-Host " " -NoNewline Write-Host ($r.TargetResourceType + " - " + $r.Title + " " + $r.Description) foreach ($detail in ($r.AdditionalData | Sort-Object Status -Descending)) { if ($ShowFailedOnly -and $detail.Status -eq 'SUCCESS') { continue } else { Write-Host " " -NoNewline Write-StatusSymbol -Status $detail.Status -Severity $r.Severity Write-Host " " -NoNewline Write-Host " " -NoNewline Write-Host ("{0}{1}{2}" -f $detail.Source, $seperator, $detail.Resource) } } if ($detail.Status -ne 'SUCCESS') { Write-Host " " -NoNewline Write-Host @helpSymbol Write-Host (" Help URL: {0}" -f $r.Remediation) Write-Host "" } } else { if (-not $ShowFailedOnly) { Write-Host @expandOutSymbol Write-Host " " -NoNewline Write-Host @greenTickSymbol Write-Host " " -NoNewline Write-Host @isHealthy Write-Host " " -NoNewline Write-Host ($r.TargetResourceType + " " + $r.Title + " " + $r.Description) } } } } catch { Log-Info "Unable to write results. Error: $($_.exception.message)" -Type Warning } } function Write-ETWLog { [CmdletBinding()] param ( [Parameter()] [string] $source = 'AzStackHciEnvironmentChecker/Diagnostic', [Parameter()] [string] $logName = 'AzStackHciEnvironmentChecker', [Parameter(Mandatory = $true)] [string] $Message, [Parameter()] [string] $EventId = 0, [Parameter()] [string] $EventType = 'Information' ) try { Write-EventLog -LogName $LogName -Source $Source -EntryType $EventType -Message $Message -EventId $EventId } catch { throw "Writing event log failed. Error $($_.exception.message)" } } function Assert-EventLog { param ( [Parameter()] [string] $source = 'AzStackHciEnvironmentChecker/Diagnostic' ) try { $eventLog = Get-EventLog -LogName AzStackHciEnvironmentChecker -Source $Source -ErrorAction SilentlyContinue } catch {} # Try to create the log if (-not $eventLog) { New-AzStackHciEnvironmentCheckerLog } } function Write-ETWResult { <# .SYNOPSIS Write result to telemetry channel #> [CmdletBinding()] param ( [Parameter()] [psobject] $Result ) try { $source = 'AzStackHciEnvironmentChecker/Telemetry' if (![string]::IsNullOrEmpty($ENV:EnvChkrId)) { $Result | Add-Member -MemberType NoteProperty -Name 'HealthCheckSource' -Value $ENV:EnvChkrId -Force -ErrorAction SilentlyContinue } $Message = $Result | ConvertTo-Json -Depth 5 $EventId = 17205 $EventType = if ($Result.Status -ne 'SUCCESS') { 'WARNING' } else { 'Information' } Write-ETWLog -Source $Source -EventType $EventType -Message $Message -EventId $EventId } catch { Log-Info "Failed to write result to telemetry channel. Error: $($_.Exception.message)" -Type Warning } } function Get-AzStackHciEnvironmentCheckerEvents { <# .SYNOPSIS Retrieve AzStackHCI Environment Checker events from event log .EXAMPLE Get-AzStackHciEnvironmentCheckerEvents -Verbose Retrieve AzStackHCI Environment Checker events from event log .EXAMPLE $results = Get-AzStackHciEnvironmentCheckerEvents | ? EventId -eq 17205 | Select -last 1 | Select -expand Message | Convertfrom-Json Write-AzStackHciResult -result $results Get last result and write to screen #> [CmdletBinding()] param ( [Parameter()] [ValidateSet('Operational', 'Diagnostic', 'Telemetry')] [string] $Source ) try { $sourceFilter = switch ($source) { Operational { "AzStackHciEnvironmentChecker/Operational" } Diagnostic { "AzStackHciEnvironmentChecker/Diagnostic" } Telemetry { "AzStackHciEnvironmentChecker/Telemetry" } Default { "*" } } try { Get-EventLog -LogName AzStackHciEnvironmentChecker -Source $SourceFilter } catch {} } catch { throw "Failed to retrieve AzStackHCI environment checker logs. Error: $($_.exception.message)" } } function New-AzStackHciEnvironmentCheckerLog { try { $scriptBlock = { $logName = 'AzStackHciEnvironmentChecker' $sources = @('AzStackHciEnvironmentChecker/Operational', 'AzStackHciEnvironmentChecker/Diagnostic', 'AzStackHciEnvironmentChecker/Telemetry', 'AzStackHciEnvironmentChecker/RemoteSupport', 'AzStackHciEnvironmentChecker/StandaloneObservability') foreach ($source in $sources) { New-EventLog -LogName $logName -Source $Source -ErrorAction SilentlyContinue Limit-EventLog -LogName $logName -MaximumSize 250MB Write-EventLog -Message ('Initializing log provider {0}' -f $source) -EventId 0 -EntryType Information -Source $source -LogName $logName -ErrorAction Stop } } if (Test-Elevation) { Invoke-Command -ScriptBlock $scriptBlock } else { $psProcess = if (Join-Path -Path $PSHOME -ChildPath powershell.exe -Resolve -ErrorAction SilentlyContinue) { Join-Path -Path $PSHOME -ChildPath powershell.exe } elseif (Join-Path -Path $PSHOME -ChildPath pwsh.exe -Resolve -ErrorAction SilentlyContinue) { Join-Path -Path $PSHOME -ChildPath pwsh.exe } else { throw "Cannot find powershell process. Please run powershell elevated and run the following command: 'New-EventLog -LogName $logName -Source $sourceName'" } Write-Warning "We need to run an elevated process to register our event log. `nPlease continue and accept the UAC prompt to continue. `nAlternatively, run: `nNew-EventLog -LogName $logName -Source $source `nmanually and restart this command." if (Grant-UACConcent) { Start-Process $psProcess -Verb Runas -ArgumentList "-command (Invoke-Command -ScriptBlock {$scriptBlock})" -Wait } else { throw "Unable to elevate and register event log provider." } } } catch { throw "Failed to create Environment Checker log. Error: $($_.Exception.Message)" } } function Remove-AzStackHciEnvironmentCheckerEventLog { <# .SYNOPSIS Remove AzStackHCI Environment Checker event log .EXAMPLE Remove-AzStackHciEnvironmentCheckerEventLog -Verbose Remove AzStackHCI Environment Checker event log #> [cmdletbinding()] param() Remove-EventLog -LogName "AzStackHciEnvironmentChecker" } function Grant-UACConcent { $concentAnswered = $false $concent = $false while ($false -eq $concentAnswered) { $promptResponse = Read-Host -Prompt "Register the event log. (Y/N)" if ($promptResponse -imatch '^y$|^yes$') { $concentAnswered = $true $concent = $true } elseif ($promptResponse -imatch '^n$|^no$') { $concentAnswered = $true $concent = $false } else { Write-Warning "Unexpected response" } } return $concent } function Write-Summary { param ($result, $property1, $property2, $property3, $seperator = '->') try { $summary = Get-Summary @PSBoundParameters # Write percentage Write-Host "`nSummary" Write-Host $lTxt.Summary if (-not ([string]::IsNullOrEmpty($summary.FailedResourceCritical))) { Write-Host " " -NoNewline Write-StatusSymbol -status 'FAILURE' -Severity Critical Write-Host (" {0} Critical Issue(s)" -f @($summary.FailedResourceCritical).Count) } if (-not ([string]::IsNullOrEmpty($summary.FailedResourceWarning))) { Write-Host " " -NoNewline Write-StatusSymbol -status 'FAILURE' -Severity Warning Write-Host (" {0} Warning Issue(s)" -f @($summary.FailedResourceWarning).Count) } if (-not ([string]::IsNullOrEmpty($summary.FailedResourceInformational))) { Write-Host " " -NoNewline Write-StatusSymbol -status 'FAILURE' -Severity Informational Write-Host (" {0} Informational Issue(s)" -f @($summary.FailedResourceInformational).Count) } if ($Summary.successCount -gt 0) { Write-Host " " -NoNewline Write-StatusSymbol -status 'SUCCESS' Write-Host (" {0} successes" -f ($Summary.successCount)) } <#Write-Host @expandDownSymbol Write-Host " " -NoNewline switch ($Severity) { 'CRITICAL' { Write-Host @redCrossSymbol } 'WARNING' { Write-Host @warningSymbol } Default { Write-Host @redCrossSymbol } }#> #Write-Host (" {0} / {1} ({2}%)" -f $summary.SuccessCount, $Result.AdditionalData.Resource.Count, $summary.SuccessPercentage) # Write issues by severity foreach ($severity in 'CRITICAL', 'WARNING', 'INFORMATIONAL') { $SeverityProp = "FailedResource{0}" -f $severity $failedResources = $summary.$SeverityProp | Sort-Object | Get-Unique if ($failedResources -gt 0) { Write-Host "" Write-Severity -severity $Severity Write-Host "" #Write-Host "`n$Severity Issues:" $failedResources | Sort-Object | Get-Unique | ForEach-Object { Write-Host " " -NoNewline switch ($Severity) { 'CRITICAL' { Write-Host @redCrossSymbol } 'WARNING' { Write-Host @warningSymbol } Default { Write-Host @redCrossSymbol } } Write-Host " $PSITEM" } } } if ($Summary.HelpLinks) { Write-Host "`nRemediation: " $Summary.HelpLinks | ForEach-Object { Write-Host " " -NoNewline Write-Host @helpSymbol Write-Host " $PSITEM" } } if (-not $summary.FailedResourceCritical -and -not $summary.FailedResourceWarning -and -not $summary.FailedResourceInformational) { Write-Host "`nSummary" Write-Host @expandOutSymbol Write-Host " " -NoNewline Write-Host @greenTickSymbol Write-Host (" {0} / {1} ({2}%) resources test successfully." -f $summary.SuccessCount, $Result.AdditionalData.Resource.Count, $summary.SuccessPercentage) } } catch { Log-Info -Message "Summary failed. $($_.Exception.Message)" -ConsoleOut -Type Warning } } function Get-Summary { param ($result, $property1, $property2, $property3, $seperator = '->') try { if (-not $result) { throw "Unable to write summary. Check tests run successfully." } [array]$success = $result | Select-Object -ExpandProperty AdditionalData | Where-Object Status -EQ 'SUCCESS' [array]$HelpLinks = $result | Where-Object Status -NE 'SUCCESS' | Select-Object -ExpandProperty Remediation | Sort-Object | Get-Unique [array]$nonSuccess = $result | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'SUCCESS' [array]$nonSuccessCritical = $result | Where-Object Severity -EQ Critical | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'SUCCESS' [array]$nonSuccessWarning = $result | Where-Object Severity -EQ Warning | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'SUCCESS' [array]$nonSuccessInformational = $result | Where-Object Severity -EQ Informational | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'SUCCESS' $successPercentage = if ($success.count -gt 0) { [Math]::Round(($success.Count / $result.AdditionalData.Resource.count) * 100) } else { 0 } $sourceDestsb = { if ([string]::IsNullOrEmpty($_.$property2) -and [string]::IsNullOrEmpty($_.$property3)) { "{0}" -f $_.$property1 } elseif ([string]::IsNullOrEmpty($_.$property3)) { "{0}{1}{2}" -f $_.$property1, $seperator, $_.$property2 } else { "{0}{1}{2}({3})" -f $_.$property1, $seperator, $_.$property2, $_.$property3 } } $FailedResourceCritical = $nonSuccessCritical | Select-Object @{ label = 'SourceDest'; Expression = $sourceDestsb } -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SourceDest | Sort-Object | Get-Unique $FailedResourceWarning = $nonSuccessWarning | Select-Object @{ label = 'SourceDest'; Expression = $sourceDestsb } -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SourceDest | Sort-Object | Get-Unique $FailedResourceInformational = $nonSuccessInformational | Select-Object @{ label = 'SourceDest'; Expression = $sourceDestsb } -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SourceDest | Sort-Object | Get-Unique $summary = New-Object -Type PsObject -Property @{ successCount = $success.Count nonSuccessCount = $nonSuccess.Count successPercentage = $successPercentage HelpLinks = $HelpLinks FailedResourceCritical = $FailedResourceCritical FailedResourceWarning = $FailedResourceWarning FailedResourceInformational = $FailedResourceInformational } return $summary } catch { throw "Unable to calculate summary. Error $($_.exception.message)" } } # Symbols $global:greenTickSymbol = @{ Object = [Char]0x2713 #8730 ForegroundColor = 'Green' NoNewLine = $true } $global:redCrossSymbol = @{ Object = [Char]0x2622 #0x00D7 ForegroundColor = 'Red' NoNewLine = $true } $global:WarningSymbol = @{ Object = [char]0x26A0 ForegroundColor = 'Yellow' NoNewLine = $true } $global:bulletSymbol = @{ Object = [Char]0x25BA NoNewLine = $true } # Text $global:needsAttention = @{ object = $lTxt.NeedsAttention; ForegroundColor = 'Yellow' NoNewLine = $true } $global:needsRemediation = @{ object = $lTxt.NeedsRemediation; ForegroundColor = 'Red' NoNewLine = $true } $global:ForInformation = @{ object = $lTxt.ForInformation; NoNewLine = $true } $global:expandDownSymbol = @{ object = [Char]0x25BC # expand down NoNewLine = $true } $global:expandOutSymbol = @{ object = [Char]0x25BA # expand out NoNewLine = $true } $global:helpSymbol = @{ object = [char]0x270E #0x263C # sunshine NoNewLine = $true #ForegroundColor = 'Yellow' } $global:Critical = @{ object = $lTxt.Critical; ForegroundColor = 'Red' NoNewLine = $true } $global:Warning = @{ object = $lTxt.Warning; ForegroundColor = 'Yellow' NoNewLine = $true } $global:Information = @{ object = $lTxt.Informational; NoNewLine = $true } $global:isHealthy = @{ object = $lTxt.Healthy NoNewLine = $true } function Write-StatusSymbol { param ($status, $severity) switch ($status) { "SUCCESS" { Write-Host @greenTickSymbol } "FAILURE" { switch ($Severity) { 'CRITICAL' { Write-Host @redCrossSymbol } 'WARNING' { Write-Host @warningSymbol } Default { Write-Host @redCrossSymbol } } } Default { Write-Host @bulletSymbol } } } function Write-Severity { param ($severity) switch ($severity) { 'CRITICAL' { Write-Host @needsRemediation } 'WARNING' { Write-Host @needsAttention } 'INFORMATIONAL' { Write-Host @ForInformation } Default { Write-Host @Critical } } } function Set-AzStackHciIdentifier { $ENV:EnvChkrId = $null if ([string]::IsNullOrEmpty($ENV:EnvChkrOp)) { $ENV:EnvChkrOp = 'Manual' } # Check if validator implemented HardwareClass parameter and set it to Medium if not provided $cmdletHardwareClass = Get-CmdletHardwareClass # If the user passed hardwareclass to the validator, use it if (-not [string]::IsNullOrEmpty($cmdletHardwareClass)) { $HardwareClass = $cmdletHardwareClass } else { $HardwareClass = 'Medium' } $validatorCmd = Get-CmdletName if(-not [string]::IsNullOrWhiteSpace($validatorCmd)) { $ENV:EnvChkrId = "{0}\{1}\{2}\{3}" -f $ENV:EnvChkrOp, $HardwareClass, $validatorCmd.matches.groups[1], (([system.guid]::newguid()) -split '-' | Select-Object -first 1) } } function Get-CmdletHardwareClass { try { $cmdletHardwareClass = Get-PSCallStack | ` Where-Object FunctionName -like 'Invoke-AzStackHci*Validation' | ` Select-Object -First 1 | ` Select-Object -ExpandProperty InvocationInfo | ` Select-Object -ExpandProperty BoundParameters -ErrorAction SilentlyContinue $hardwareClass = $cmdletHardwareClass.HardwareClass return $hardwareClass } catch { Log-Info -Message "Failed to get cmdlet hardware class. Error: $($_.exception.message)" -Type Warning return $null } } function Write-PsSessionInfo { <# .SYNOPSIS Write some pertainent information to the log about any PsSessions passed #> [CmdletBinding()] param ( $params ) try { if ($params['PsSession']) { foreach ($session in $params['PsSession']) { Log-Info -Message ("PsSession info: {0}, {1}, {2}, {3}, {4}, {5}" -f $session.ComputerName, $session.Name, $session.Id, $session.Runspace.ConnectionInfo.credential.username, $session.Runspace.SessionStateProxy.LanguageMode, $session.Runspace.ConnectionInfo.AuthenticationMechanism) } } else { Log-Info -Message "No PsSession info to write" } } catch { Log-Info -Message "Failed to write PsSession info: $($_.exception.message)" } } function Log-CimData { [CmdletBinding()] param ( $cimData, [array]$properties ) try { # Use properties provided or all properties if none provided $selectProperties = @() $selectProperties += if ($null -eq $Properties) { "*" } else { foreach ($property in $Properties) { if ($property -is [hashtable]) { $property.Keys } else { $property } } } # For each server log the cimdata foreach ($serverName in ($cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique)) { $sData = $cimData | Where-Object {$_.CimSystemProperties.ServerName -eq $ServerName } [string]$className = $sData.CimClass.CimClassName | Sort-Object | Get-Unique Log-Info ("{0} cim instance data for {1}" -f $className, $serverName) $sData | Select-Object -Property $selectProperties | ConvertTo-Csv | Out-String -Stream | ForEach-Object { if (![string]::IsNullOrEmpty($_)) { Log-Info $_ } } } } catch { Log-Info "Failed to write cimdata to log file. Error: $($_.Exception.Message)" -Type Error } } function New-AzStackHciResultObject { [CmdletBinding()] param ( [Parameter()] [String] $Name, [Parameter()] [String] $DisplayName, [Parameter()] [String] $Title, [Parameter()] [Hashtable] $Tags, [Parameter()] [String] $Status, [Parameter()] [String] $Severity, [Parameter()] [String] $Description, [Parameter()] [String] $Remediation, [Parameter()] [String] $TargetResourceID, [Parameter()] [String] $TargetResourceName, [Parameter()] [String] $TargetResourceType, [Parameter()] [datetime] $Timestamp, [Parameter()] [Hashtable] $AdditionalData, [Parameter()] [string] $HealthCheckSource ) # load the assembly as an array of bytes to avoid locking the dll. $bytes = [system.io.file]::ReadAllBytes("$PsScriptRoot\Schema\Microsoft.EnvironmentReadiness.Validator.Client.dll") $assembly = [Reflection.Assembly]::Load($bytes) $type = $assembly.GetType("Microsoft.EnvironmentReadiness.Client.Models.EnvironmentReadinessTestResult") $resultObj = New-Object -TypeName $type.FullName # Set properties from PSBoundParameters foreach ($param in $PSBoundParameters.Keys) { if ($PSBoundParameters[$param]) { if ($PSBoundParameters[$param] -is [System.Collections.Hashtable]) { $resultObj.$param = ConvertTo-Dictionary -hashTable $PSBoundParameters[$param] } else { $resultObj.$param = $PSBoundParameters[$param] } } } return $resultObj } # Convert hashtable to dictionary function ConvertTo-Dictionary { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [hashtable]$hashTable ) begin { $dictionary = New-Object 'system.collections.generic.dictionary[string,string]' } process { foreach ($key in $hashTable.Keys) { if ([string]::IsNullOrEmpty($hashTable[$key])) { $value = "" } else { $value = $hashTable[$key] } $dictionary.Add($key, $value) } } end { return ,$dictionary } } # function to get psobject from pipeline and redact sensitive property values function Protect-SensitiveProperties { param ( [Parameter(ValueFromPipeline = $true)] [psObject] $params ) BEGIN { $array = @() } PROCESS { try { $ret = @{} foreach($key in $_.Keys) { # Redact sensitive parameters if($Key -match "ArmAccessToken|Account|PsSession") { $ret += @{$Key = '[redacted]'} } else { $ret += @{$Key = $_[$Key]} } } $array += $ret } catch { Log-Info "Error occurred trying to remove sensitive parameters. Error: $($_.Exception.Message)" -Type ERROR } } END { return $array } } Export-ModuleMember -function Add-AzStackHciEnvJob Export-ModuleMember -function Close-AzStackHciEnvJob Export-ModuleMember -function Get-AzStackHciEnvironmentCheckerEvents Export-ModuleMember -function Get-AzStackHciEnvProgress Export-ModuleMember -function Log-Info Export-ModuleMember -function Set-AzStackHciOutputPath Export-ModuleMember -function Write-AzStackHciEnvReport Export-ModuleMember -function Write-AzStackHciFooter Export-ModuleMember -function Write-AzStackHciHeader Export-ModuleMember -function Write-AzStackHciResult Export-ModuleMember -function Write-ETWLog Export-ModuleMember -function Write-ETWResult Export-ModuleMember -function Write-Summary Export-ModuleMember -function Log-CimData Export-ModuleMember -function New-AzStackHciResultObject # SIG # Begin signature block # MIIoQwYJKoZIhvcNAQcCoIIoNDCCKDACAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDxrfDxpcMSr8n9 # HiYQHNIkuJdwhDlS1RdP3Wt0Np7hWqCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0 # 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 # /Xmfwb1tbWrJUnMTDXpQzTGCGiMwghofAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIFzhzqTCkvCWi8vVkaSiPVEU # nm1+SuAIU/rswtv/iq7NMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAAR9ysaMc134nYcyXWywwPg/HsJOU2Rst4G7+ufM12IBHuX1CyHoH6wb2 # gFKuoMWyka2jxbjVzrJHRRoDuWC3gjKn01lTwJu2VlP6x9otWAXSxehMyG1Ku1aL # T2X96ss/y45F4nPZE+zDSG8nGkg+JVMOtxaKClkPHtVox4a13urYRoDhn4fUadqU # 3pm2QEpm+MQnKzZC/G3CHdCLz6iBIgS16BO4dZ3sX382RzzGTUESAS5/j8bhtwvn # 08MqypGceRBGEY/MhXOrgLeFuoaDuMHYLFKBhcU8Jf+TGWbVbzrRz7LqDGVHAznE # oWF4rV2s/KBGPSVXAs1kobeE3++zHaGCF60wghepBgorBgEEAYI3AwMBMYIXmTCC # F5UGCSqGSIb3DQEHAqCCF4YwgheCAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq # hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCDKi7xfWqxJ5nSrpjR+a8UQlLoUzZ4LojLYUujFsd0bggIGZ0nuZNPN # GBMyMDI0MTIwNDE1MDM0Ny4yNTZaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # TjoyRDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaCCEfswggcoMIIFEKADAgECAhMzAAAB/XP5aFrNDGHtAAEAAAH9MA0G # CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u # MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp # b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0 # MDcyNTE4MzExNloXDTI1MTAyMjE4MzExNlowgdMxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w # ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjJEMUEt # MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoWWs+D+Ou4JjYnRHRedu # 0MTFYzNJEVPnILzc02R3qbnujvhZgkhp+p/lymYLzkQyG2zpxYceTjIF7HiQWbt6 # FW3ARkBrthJUz05ZnKpcF31lpUEb8gUXiD2xIpo8YM+SD0S+hTP1TCA/we38yZ3B # EtmZtcVnaLRp/Avsqg+5KI0Kw6TDJpKwTLl0VW0/23sKikeWDSnHQeTprO0zIm/b # tagSYm3V/8zXlfxy7s/EVFdSglHGsUq8EZupUO8XbHzz7tURyiD3kOxNnw5ox1eZ # X/c/XmW4H6b4yNmZF0wTZuw37yA1PJKOySSrXrWEh+H6++Wb6+1ltMCPoMJHUtPP # 3Cn0CNcNvrPyJtDacqjnITrLzrsHdOLqjsH229Zkvndk0IqxBDZgMoY+Ef7ffFRP # 2pPkrF1F9IcBkYz8hL+QjX+u4y4Uqq4UtT7VRnsqvR/x/+QLE0pcSEh/XE1w1fcp # 6Jmq8RnHEXikycMLN/a/KYxpSP3FfFbLZuf+qIryFL0gEDytapGn1ONjVkiKpVP2 # uqVIYj4ViCjy5pLUceMeqiKgYqhpmUHCE2WssLLhdQBHdpl28+k+ZY6m4dPFnEoG # cJHuMcIZnw4cOwixojROr+Nq71cJj7Q4L0XwPvuTHQt0oH7RKMQgmsy7CVD7v55d # OhdHXdYsyO69dAdK+nWlyYcCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBTpDMXA4ZW8 # +yL2+3vA6RmU7oEKpDAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf # BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz # L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww # bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m # dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El # MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF # BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAY9hYX+T5AmCr # YGaH96TdR5T52/PNOG7ySYeopv4flnDWQLhBlravAg+pjlNv5XSXZrKGv8e4s5dJ # 5WdhfC9ywFQq4TmXnUevPXtlubZk+02BXK6/23hM0TSKs2KlhYiqzbRe8QbMfKXE # DtvMoHSZT7r+wI2IgjYQwka+3P9VXgERwu46/czz8IR/Zq+vO5523Jld6ssVuzs9 # uwIrJhfcYBj50mXWRBcMhzajLjWDgcih0DuykPcBpoTLlOL8LpXooqnr+QLYE4Bp # Uep3JySMYfPz2hfOL3g02WEfsOxp8ANbcdiqM31dm3vSheEkmjHA2zuM+Tgn4j5n # +Any7IODYQkIrNVhLdML09eu1dIPhp24lFtnWTYNaFTOfMqFa3Ab8KDKicmp0Ath # RNZVg0BPAL58+B0UcoBGKzS9jscwOTu1JmNlisOKkVUVkSJ5Fo/ctfDSPdCTVaIX # XF7l40k1cM/X2O0JdAS97T78lYjtw/PybuzX5shxBh/RqTPvCyAhIxBVKfN/hfs4 # CIoFaqWJ0r/8SB1CGsyyIcPfEgMo8ceq1w5Zo0JfnyFi6Guo+z3LPFl/exQaRubE # rsAUTfyBY5/5liyvjAgyDYnEB8vHO7c7Fg2tGd5hGgYs+AOoWx24+XcyxpUkAajD # hky9Dl+8JZTjts6BcT9sYTmOodk/SgIwggdxMIIFWaADAgECAhMzAAAAFcXna54C # 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 # ahC0HVUzWLOhcGbyoYIDVjCCAj4CAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # TjoyRDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAoj0WtVVQUNSKoqtrjinRAsBUdoOggYMw # gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF # AAIFAOr6W3MwIhgPMjAyNDEyMDQwNDM5MTVaGA8yMDI0MTIwNTA0MzkxNVowdDA6 # BgorBgEEAYRZCgQBMSwwKjAKAgUA6vpbcwIBADAHAgEAAgIC0zAHAgEAAgISpjAK # AgUA6vus8wIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB # AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQCMVaifckgf3a4N # wJ9x1eRE0UVe3unOXiPIKj0BwTGXlA2pWovZJK7CRFyoHSyjSWQ3GX2c28n4qyU2 # VKoG7NA1fdfQPqBX9m5rNdyhiHZ4dDhmQIKD8P9UI8J838h8waFXWTKsUGNuLmjJ # TkcZh2TRBGQsMrRFxpyFQ/RR2DT4kEno85awXkLfQunHZTIFC7s8TAvHCQbnOKM4 # H7l7ryDZzCtIr7NGnSUPMY9fif0SM4m1aqbLmJlPzrHLBVdARBMMrnh+OyWtetIV # uuC1JewlA52NxyP2WBYmby3T5QqGk6M88YShdo2VuWkgx+DIHwMN5Gip6EnFYbHz # BdjHBHHlMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTACEzMAAAH9c/loWs0MYe0AAQAAAf0wDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqG # SIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgkBD4ytJMA4gA # iXFJR2w7wc02vE+Gjw6EBCAJGdv3wuIwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHk # MIG9BCCAKEgNyUowvIfx/eDfYSupHkeF1p6GFwjKBs8lRB4NRzCBmDCBgKR+MHwx # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1p # Y3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB/XP5aFrNDGHtAAEAAAH9 # MCIEIFNkXSqphigY570q9drmM7TFezyh4V3e+r1klmjIIiOBMA0GCSqGSIb3DQEB # CwUABIICAFt72c0lUYkDyE28pYVmrLjFVaZvc9I16UoZ/2v+tDMqHqTBcbWiNIXj # yF17ZeeVX/V6bDzP4g3g88Oemp9xPQyRVhU7NAKt0P/wvUdOahmXSrJo1gZvZ/wm # RWqnWxAehh7HJFOUjyudb/vyMSgN2vmus73nMlONdl5P6O7SfvXKaS31jeYEwxgU # ybiZxOVAL9UiSAGvTMB+l8CyptCHzvJsEEf8E7GV3XShBRsu/4n0b3jF+7sF9tTU # 5J6U/oh1zg5ZoiyixCK9mpMerpNvxniosb2pAoAJMtktRNECBAQjDBgBqrrWNgee # 4fmmCAB42hJA5TD77lSD4BFMnKd1BM+HVpvni/LQRuV1LAuJ9eV1+LYS5C/ZkMR9 # 4LbrNmmU+MCqct4DdCQt/TS/vFp9IFDdl6TpcGUY9S5saLZ8wq6LKx8k+YCEZicI # gepmtRovwls26QDi00aoIWHYEwfTD1wLYagLUEGpqCZJLA+ODn3PfM3zvsh/X2ed # 8Fs99Hnsm9Jwgo1PnmFYQYCllQO2iYQQi5jCxy1ADpb0W1fR09RIkOxLuUCcbvmq # V5W7bKRblD9A2Pv+BOCPDruFVuGOIzziBfxupxFmOJNajAnQdiEaTCS35CIJjPNP # Rhv7GVz2iSzHH/z8zf8bcFTnT3HPbf+5ylw5g9PbOo2ZZ6o7YDlZ # SIG # End signature block |