helpers/test/Invoke-WUGDiscoveryE2ETest.ps1
|
<#
.SYNOPSIS End-to-end dashboard tests for all discovery providers. .DESCRIPTION Runs each Setup-*-Discovery.ps1 script with -Action Dashboard -NonInteractive and verifies the dashboard HTML was generated. Produces a colour-coded HTML results dashboard using the shared Test-Dashboard-Template.html (same format as Invoke-WUGModuleTest, Invoke-WUGHelperTest, and Invoke-WUGDiscoveryRunner). Providers that require external infrastructure (cloud accounts, network appliances) are skipped unless their target parameters are supplied. Docker and Windows WMI providers auto-detect local availability. .PARAMETER IncludeProvider Run only the specified provider(s). Tab-completes to all known names. .PARAMETER ExcludeProvider Skip the specified provider(s). .PARAMETER DockerHost Docker Engine API endpoint(s). Default: localhost. .PARAMETER DockerPort Docker API port. Default: 2375. .PARAMETER WindowsTarget Target(s) for Windows WMI discovery. Default: localhost. .PARAMETER HyperVTarget Hyper-V host(s) to scan. Skipped if not provided. .PARAMETER VMwareTarget vCenter/ESXi host to scan. Skipped if not provided. .PARAMETER ProxmoxTarget Proxmox host(s) to scan. Skipped if not provided. .PARAMETER NutanixTarget Nutanix Prism host(s) to scan. Skipped if not provided. .PARAMETER F5Target F5 BIG-IP host(s) to scan. Skipped if not provided. .PARAMETER FortinetTarget FortiGate host(s) to scan. Skipped if not provided. .PARAMETER BigleafTarget Bigleaf target label. Skipped if not provided. .PARAMETER AwsRegion AWS region(s) to scan. Skipped if not provided. .PARAMETER AzureTenantId Azure tenant ID. Skipped if not provided. .PARAMETER GcpProject GCP project ID(s). Skipped if not provided. .PARAMETER OciTenancyId OCI tenancy OCID. Skipped if not provided. .PARAMETER OutputPath Directory for dashboard output files and the test report. Default: $env:TEMP\DiscoveryE2E. .PARAMETER OpenDashboards Open generated provider dashboards in the default browser. .PARAMETER OpenReport Open the HTML test results report in the default browser. .EXAMPLE .\Invoke-WUGDiscoveryE2ETest.ps1 Tests Docker + WindowsAttributes + WindowsDiskIO against localhost. .EXAMPLE .\Invoke-WUGDiscoveryE2ETest.ps1 -IncludeProvider Docker Tests only Docker. .EXAMPLE .\Invoke-WUGDiscoveryE2ETest.ps1 -VMwareTarget 'vcenter.lab.local' -OpenReport Also tests VMware and opens the HTML report. .NOTES Author : jason@wug.ninja Created : 2026-04-18 Requires: PowerShell 5.1+, Setup-*-Discovery.ps1 scripts #> [CmdletBinding()] param( [ValidateSet('AWS','Azure','Bigleaf','CiscoWLC','CUCM','Docker','F5','Fortinet','GCP', 'HyperV','LoadMaster','Nutanix','NvidiaSmi','OCI','Proxmox','VMware', 'WindowsAttributes','WindowsDiskIO')] [string[]]$IncludeProvider, [ValidateSet('AWS','Azure','Bigleaf','CiscoWLC','CUCM','Docker','F5','Fortinet','GCP', 'HyperV','LoadMaster','Nutanix','NvidiaSmi','OCI','Proxmox','VMware', 'WindowsAttributes','WindowsDiskIO')] [string[]]$ExcludeProvider, [string[]]$DockerHost = @('localhost'), [int]$DockerPort = 2375, [string[]]$WindowsTarget = @('localhost'), [string[]]$HyperVTarget, [string]$VMwareTarget, [string[]]$ProxmoxTarget, [string[]]$NutanixTarget, [string[]]$F5Target, [string[]]$FortinetTarget, [string[]]$LoadMasterTarget, [string]$BigleafTarget, [string[]]$CiscoWLCTarget, [string[]]$CUCMTarget, [string[]]$NvidiaSmiTarget, [string]$NvidiaSmiSshUsername, [string]$NvidiaSmiSshKeyFile, [string]$AwsRegion, [string]$AzureTenantId, [string[]]$GcpProject, [string]$OciTenancyId, [string]$OutputPath, [switch]$NonInteractive, # Vault scope for credential lookups. Default: LocalMachine (matches Initialize-WUGDiscoveryVault.ps1 default). # Use CurrentUser if credentials were saved without -VaultScope LocalMachine. [ValidateSet('LocalMachine','CurrentUser')] [string]$VaultScope = 'LocalMachine', [switch]$OpenDashboards, [switch]$OpenReport ) # ── Setup ──────────────────────────────────────────────────────────────────── $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent $discoveryDir = Join-Path (Split-Path $scriptDir -Parent) 'discovery' $timestamp = (Get-Date).ToString('yyyyMMdd_HHmmss') if (-not $OutputPath) { $OutputPath = Join-Path $env:TEMP 'DiscoveryE2E' } if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } # ── Load DiscoveryHelpers for vault credential checks ──────────────────────── $discoveryHelpersPath = Join-Path $discoveryDir 'DiscoveryHelpers.ps1' if (Test-Path $discoveryHelpersPath) { . $discoveryHelpersPath # Set vault scope to match where Initialize-WUGDiscoveryVault.ps1 stored credentials if (Get-Command -Name 'Set-DiscoveryVaultScope' -ErrorAction SilentlyContinue) { Set-DiscoveryVaultScope -Scope $VaultScope Write-Verbose "Vault scope: $VaultScope Path: $script:DiscoveryVaultPath" } } else { Write-Warning "DiscoveryHelpers.ps1 not found - vault credential checks disabled." } # Helper: silently check if a vault credential exists function Test-VaultCredential { param([string]$VaultName, [string]$CredType) if (-not (Get-Command -Name 'Resolve-DiscoveryCredential' -ErrorAction SilentlyContinue)) { return $false } if ([string]::IsNullOrEmpty($VaultName)) { return $false } try { $cred = Resolve-DiscoveryCredential -Name $VaultName -CredType $CredType -NonInteractive return ($null -ne $cred) } catch { return $false } } # ── Test Framework (matches Invoke-WUGDiscoveryRunner / Invoke-WUGHelperTest) ─ $script:TestResults = [System.Collections.Generic.List[PSCustomObject]]::new() function Record-Test { param( [string]$Cmdlet, [string]$Endpoint, [string]$Status, [string]$Detail = '', [string]$Duration = '', [int]$DurationMs = 0 ) $script:TestResults.Add([PSCustomObject]@{ Cmdlet = $Cmdlet Endpoint = $Endpoint Status = $Status Duration = $Duration DurationMs = $DurationMs Detail = $Detail }) } function Export-TestResultsHtml { param( [Parameter(Mandatory)]$TestResults, [Parameter(Mandatory)][string]$OutputFilePath, [Parameter(Mandatory)][string]$TemplatePath, [string]$ReportTitle = 'Discovery E2E Results' ) if (-not (Test-Path $TemplatePath)) { Write-Warning "Report template not found at $TemplatePath - skipping HTML report." return $null } $columns = @( @{ field = 'Cmdlet'; title = 'Provider'; sortable = $true; searchable = $true } @{ field = 'Endpoint'; title = 'Test'; sortable = $true; searchable = $true } @{ field = 'Status'; title = 'Status'; sortable = $true; searchable = $true; formatter = 'formatTestStatus' } @{ field = 'Duration'; title = 'Duration'; sortable = $true; searchable = $true; sorter = 'durationSorter' } @{ field = 'DurationMs'; title = 'DurationMs'; sortable = $false; visible = $false } @{ field = 'Detail'; title = 'Detail'; sortable = $true; searchable = $true } ) $columnsJson = $columns | ConvertTo-Json -Depth 5 -Compress $dataRows = @($TestResults | Select-Object Cmdlet, Endpoint, Status, Duration, DurationMs, Detail) $dataJson = ConvertTo-Json -InputObject $dataRows -Depth 5 -Compress $tableConfig = @" columns: $columnsJson, data: $dataJson, sortName: 'Duration', sortOrder: 'desc' "@ $html = Get-Content -Path $TemplatePath -Raw $html = $html -replace 'replaceThisHere', $tableConfig $html = $html -replace 'ReplaceYourReportNameHere', $ReportTitle $html = $html -replace 'ReplaceUpdateTimeHere', (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') Set-Content -Path $OutputFilePath -Value $html -Encoding UTF8 return $OutputFilePath } # ── Resolve default targets from vault ──────────────────────────────────────── # For each provider: if vault creds exist, auto-use default target. # If no vault creds and not NonInteractive, prompt user to configure. # If multiple vault creds exist (e.g., Windows.WMI.Credential.1, .2), use first only. # Default targets (matching Invoke-WUGDiscoveryRunner.ps1 config) $defaultTargets = @{ HyperV = @('192.168.74.30') VMware = 'vcenter.corp.local' Proxmox = @('192.168.1.30') Nutanix = @('nutanix-cluster.corp.local') F5 = @('lb1.corp.local') Fortinet = @('fw1.corp.local') LoadMaster = @('loadmaster.corp.local') Bigleaf = 'bigleaf' AWS = 'all' Azure = $null GCP = @('gcp') OCI = 'oci' CiscoWLC = @('192.168.75.33') CUCM = @('192.168.75.33') NvidiaSmi = @('192.168.75.80') } # Vault names and cred types (matching Invoke-WUGDiscoveryRunner.ps1 config) $vaultConfig = [ordered]@{ HyperV = @{ Name = 'HyperV.192.168.74.30.Credential'; CredType = 'PSCredential'; ParamName = 'HyperVTarget'; Label = 'Hyper-V (192.168.74.30)' } VMware = @{ Name = 'VMware.vcenter.corp.local.Credential'; CredType = 'PSCredential'; ParamName = 'VMwareTarget'; Label = 'VMware vCenter' } Proxmox = @{ Name = 'Proxmox.192.168.1.30.Token'; CredType = 'BearerToken'; ParamName = 'ProxmoxTarget'; Label = 'Proxmox (192.168.1.30)' } Nutanix = @{ Name = 'Nutanix.nutanix-cluster.corp.local.Credential'; CredType = 'PSCredential'; ParamName = 'NutanixTarget'; Label = 'Nutanix AHV' } F5 = @{ Name = 'F5.lb1.corp.local.Credential'; CredType = 'PSCredential'; ParamName = 'F5Target'; Label = 'F5 BIG-IP' } Fortinet = @{ Name = 'FortiGate.fw1.corp.local'; CredType = 'BearerToken'; ParamName = 'FortinetTarget'; Label = 'FortiGate' } LoadMaster = @{ Name = 'LoadMaster.loadmaster.corp.local.ApiKey'; CredType = 'BearerToken'; ParamName = 'LoadMasterTarget'; Label = 'Kemp LoadMaster' } Bigleaf = @{ Name = 'Bigleaf.Credential'; CredType = 'PSCredential'; ParamName = 'BigleafTarget'; Label = 'Bigleaf SD-WAN' } AWS = @{ Name = 'AWS.Credential'; CredType = 'AWSKeys'; ParamName = 'AwsRegion'; Label = 'AWS' } Azure = @{ Name = $null; CredType = 'AzureSP'; ParamName = 'AzureTenantId'; Label = 'Azure' } GCP = @{ Name = 'GCP.ServiceAccount'; CredType = 'FilePath'; ParamName = 'GcpProject'; Label = 'Google Cloud' } OCI = @{ Name = 'OCI.Config'; CredType = 'OCIConfig'; ParamName = 'OciTenancyId'; Label = 'Oracle Cloud' } CiscoWLC = @{ Name = 'CiscoWLC.Snmp'; CredType = 'PSCredential'; ParamName = 'CiscoWLCTarget'; Label = 'Cisco WLC' } CUCM = @{ Name = 'CUCM.Snmp'; CredType = 'PSCredential'; ParamName = 'CUCMTarget'; Label = 'Cisco CUCM' } NvidiaSmi = @{ Name = 'NvidiaSmi.Ssh'; CredType = 'PSCredential'; ParamName = 'NvidiaSmiTarget'; Label = 'NVIDIA GPU (SSH)' } } $hasResolveCmd = Get-Command -Name 'Resolve-DiscoveryCredential' -ErrorAction SilentlyContinue $vaultStatus = [ordered]@{} $promptedAny = $false # Auto-discover Azure vault key (stored as Azure.<TenantId>.ServicePrincipal) if ($hasResolveCmd -and $null -eq $vaultConfig['Azure'].Name) { $vaultDir = $script:DiscoveryVaultPath if ($vaultDir -and (Test-Path $vaultDir)) { $azureCredFile = Get-ChildItem -Path $vaultDir -Filter 'Azure.*.ServicePrincipal.cred' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($azureCredFile) { $azureVaultKey = [System.IO.Path]::GetFileNameWithoutExtension($azureCredFile.Name) $vaultConfig['Azure'] = @{ Name = $azureVaultKey; CredType = 'AzureSP'; ParamName = 'AzureTenantId'; Label = 'Azure' } Write-Verbose "Azure vault key auto-discovered: $azureVaultKey" } } } foreach ($provName in $vaultConfig.Keys) { # Skip providers not in the run list if ($IncludeProvider -and $provName -notin $IncludeProvider) { continue } if ($ExcludeProvider -and $provName -in $ExcludeProvider) { continue } $vc = $vaultConfig[$provName] $paramVar = $vc.ParamName # Skip if user explicitly passed this target param if ($PSBoundParameters.ContainsKey($paramVar)) { $vaultStatus[$provName] = 'user-provided' continue } # Check vault silently $hasVault = $false if ($hasResolveCmd -and -not [string]::IsNullOrEmpty($vc.Name)) { try { $cred = Resolve-DiscoveryCredential -Name $vc.Name -CredType $vc.CredType -NonInteractive $hasVault = ($null -ne $cred) } catch { } } if ($hasVault) { # Vault creds found — auto-set default target if ($provName -eq 'Azure') { # New format: Azure.<TenantId>.ServicePrincipal -- extract TenantId from credential if ($cred -is [PSCredential]) { $tenantId = ($cred.UserName -split '\|', 2)[0] } elseif ($cred -is [hashtable] -and $cred.TenantId) { $tenantId = $cred.TenantId } else { $tenantId = ($cred.UserName -split '\|', 2)[0] } Set-Variable -Name $paramVar -Value $tenantId } else { Set-Variable -Name $paramVar -Value $defaultTargets[$provName] } $vaultStatus[$provName] = 'vault' } elseif (-not $NonInteractive -and $hasResolveCmd) { # No vault creds — prompt user to configure if (-not $promptedAny) { Write-Host '' Write-Host ' Credential Setup' -ForegroundColor Cyan Write-Host ' Providers without saved vault credentials will be prompted.' -ForegroundColor Gray Write-Host ' Once saved, future runs are fully automated.' -ForegroundColor Gray Write-Host '' $promptedAny = $true } Write-Host " $($vc.Label): no vault credential found." -ForegroundColor Yellow $answer = Read-Host -Prompt " Configure now? [Y]es / [N]o skip (default: N)" if ($answer -match '^[Yy]') { $newCred = Resolve-DiscoveryCredential -Name $vc.Name -CredType $vc.CredType -ProviderLabel $vc.Label if ($null -ne $newCred) { if ($provName -eq 'Azure' -and $newCred -is [PSCredential]) { $tenantId = ($newCred.UserName -split '\|', 2)[0] Set-Variable -Name $paramVar -Value $tenantId } else { Set-Variable -Name $paramVar -Value $defaultTargets[$provName] } $vaultStatus[$provName] = 'new' $resolvedTarget = (Get-Variable -Name $paramVar -ValueOnly) Write-Host " Saved. Will test $provName against $resolvedTarget." -ForegroundColor Green } else { $vaultStatus[$provName] = 'skipped' Write-Host " Skipped." -ForegroundColor DarkGray } } else { $vaultStatus[$provName] = 'skipped' } } else { $vaultStatus[$provName] = 'no-cred' } } if ($promptedAny) { Write-Host '' } # ── Provider definitions ───────────────────────────────────────────────────── $providers = [ordered]@{ Docker = @{ Script = 'Setup-Docker-Discovery.ps1' Args = @{ Target = $DockerHost; ApiPort = $DockerPort; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'Docker-Dashboard.html' PreCheck = { try { $null = Invoke-RestMethod -Uri "http://$($DockerHost[0]):$DockerPort/info" -TimeoutSec 3; $true } catch { $false } } PreCheckLabel = "Docker API on $($DockerHost[0]):$DockerPort" } WindowsAttributes = @{ Script = 'Setup-WindowsAttributes-Discovery.ps1' Args = @{ Target = $WindowsTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'WindowsAttributes-Dashboard.html' PreCheck = { try { $null = Get-WmiObject Win32_OperatingSystem -ErrorAction Stop; $true } catch { $false } } PreCheckLabel = 'WMI (Win32_OperatingSystem)' } WindowsDiskIO = @{ Script = 'Setup-WindowsDiskIO-Discovery.ps1' Args = @{ Target = $WindowsTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'WindowsDiskIO-Dashboard.html' PreCheck = { try { $null = Get-WmiObject Win32_PerfFormattedData_PerfDisk_LogicalDisk -ErrorAction Stop; $true } catch { $false } } PreCheckLabel = 'WMI (Win32_PerfFormattedData_PerfDisk_LogicalDisk)' } HyperV = @{ Script = 'Setup-HyperV-Discovery.ps1' Args = @{ Target = $HyperVTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'HyperV-Dashboard.html' VaultName = 'HyperV.192.168.74.30.Credential' PreCheck = { $null -ne $HyperVTarget -and $HyperVTarget.Count -gt 0 } PreCheckLabel = "HyperV vault cred + target ($($HyperVTarget -join ', '))" } VMware = @{ Script = 'Setup-VMware-Discovery.ps1' Args = @{ Target = $VMwareTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'VMware-Dashboard.html' VaultName = 'VMware.vcenter.corp.local.Credential' PreCheck = { -not [string]::IsNullOrEmpty($VMwareTarget) } PreCheckLabel = "VMware vault cred + target ($VMwareTarget)" } Proxmox = @{ Script = 'Setup-Proxmox-Discovery.ps1' Args = @{ Target = $ProxmoxTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'Proxmox-Dashboard.html' VaultName = 'Proxmox.192.168.1.30.Token' PreCheck = { $null -ne $ProxmoxTarget -and $ProxmoxTarget.Count -gt 0 } PreCheckLabel = "Proxmox vault cred + target ($($ProxmoxTarget -join ', '))" } Nutanix = @{ Script = 'Setup-Nutanix-Discovery.ps1' Args = @{ Target = $NutanixTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'Nutanix-Dashboard.html' VaultName = 'Nutanix.nutanix-cluster.corp.local.Credential' PreCheck = { $null -ne $NutanixTarget -and $NutanixTarget.Count -gt 0 } PreCheckLabel = "Nutanix vault cred + target ($($NutanixTarget -join ', '))" } F5 = @{ Script = 'Setup-F5-Discovery.ps1' Args = @{ Target = $F5Target; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'F5-Dashboard.html' VaultName = 'F5.lb1.corp.local.Credential' PreCheck = { $null -ne $F5Target -and $F5Target.Count -gt 0 } PreCheckLabel = "F5 vault cred + target ($($F5Target -join ', '))" } Fortinet = @{ Script = 'Setup-Fortinet-Discovery.ps1' Args = @{ Target = $FortinetTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'Fortinet-Dashboard.html' VaultName = 'FortiGate.fw1.corp.local' PreCheck = { $null -ne $FortinetTarget -and $FortinetTarget.Count -gt 0 } PreCheckLabel = "Fortinet vault cred + target ($($FortinetTarget -join ', '))" } LoadMaster = @{ Script = 'Setup-LoadMaster-Discovery.ps1' Args = @{ Target = $LoadMasterTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'LoadMaster-Dashboard.html' VaultName = 'LoadMaster.loadmaster.corp.local.ApiKey' PreCheck = { $null -ne $LoadMasterTarget -and $LoadMasterTarget.Count -gt 0 } PreCheckLabel = "LoadMaster vault cred + target ($($LoadMasterTarget -join ', '))" } Bigleaf = @{ Script = 'Setup-Bigleaf-Discovery.ps1' Args = @{ Target = $BigleafTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'Bigleaf-Dashboard.html' VaultName = 'Bigleaf.Credential' PreCheck = { -not [string]::IsNullOrEmpty($BigleafTarget) } PreCheckLabel = "Bigleaf vault cred + target ($BigleafTarget)" } AWS = @{ Script = 'Setup-AWS-Discovery.ps1' Args = @{ Region = $AwsRegion; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'AWS-Dashboard.html' VaultName = 'AWS.Credential' PreCheck = { -not [string]::IsNullOrEmpty($AwsRegion) } PreCheckLabel = "AWS vault cred + region ($AwsRegion)" } Azure = @{ Script = 'Setup-Azure-Discovery.ps1' Args = @{ TenantId = $AzureTenantId; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'Azure-Dashboard.html' VaultName = 'Azure.f7b2ef38-1a73-44e0-9b44-ae4d09864721.ServicePrincipal' PreCheck = { -not [string]::IsNullOrEmpty($AzureTenantId) } PreCheckLabel = "Azure vault cred + tenant ($AzureTenantId)" } GCP = @{ Script = 'Setup-GCP-Discovery.ps1' Args = @{ Target = $GcpProject; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'GCP-Dashboard.html' VaultName = 'GCP.ServiceAccount' PreCheck = { $null -ne $GcpProject -and $GcpProject.Count -gt 0 } PreCheckLabel = "GCP vault cred + project ($($GcpProject -join ', '))" } OCI = @{ Script = 'Setup-OCI-Discovery.ps1' Args = @{ TenancyId = $OciTenancyId; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'OCI-Dashboard.html' VaultName = 'OCI.Config' PreCheck = { -not [string]::IsNullOrEmpty($OciTenancyId) } PreCheckLabel = "OCI vault cred + tenancy ($OciTenancyId)" } CiscoWLC = @{ Script = 'Setup-CiscoWLC-Discovery.ps1' Args = @{ Target = $CiscoWLCTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'CiscoWLC-Dashboard.html' VaultName = 'CiscoWLC.Snmp' PreCheck = { $null -ne $CiscoWLCTarget -and $CiscoWLCTarget.Count -gt 0 } PreCheckLabel = "CiscoWLC vault cred + target ($($CiscoWLCTarget -join ', '))" } CUCM = @{ Script = 'Setup-CUCM-Discovery.ps1' Args = @{ Target = $CUCMTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath } DashboardFile = 'cucm-dashboard-phones.html' VaultName = 'CUCM.Snmp' PreCheck = { $null -ne $CUCMTarget -and $CUCMTarget.Count -gt 0 } PreCheckLabel = "CUCM vault cred + target ($($CUCMTarget -join ', '))" } NvidiaSmi = @{ Script = 'Setup-NvidiaSmi-Discovery.ps1' Args = @{ Target = $NvidiaSmiTarget; Action = 'Dashboard'; NonInteractive = $true; OutputPath = $OutputPath; SshUsername = $NvidiaSmiSshUsername; SshKeyFile = $NvidiaSmiSshKeyFile } DashboardFile = 'nvidiasmi-dashboard-gpu.html' VaultName = 'NvidiaSmi.Ssh' PreCheck = { $null -ne $NvidiaSmiTarget -and $NvidiaSmiTarget.Count -gt 0 } PreCheckLabel = "NvidiaSmi SSH vault cred + target ($($NvidiaSmiTarget -join ', '))" } } # ── Filter providers ───────────────────────────────────────────────────────── $runList = [ordered]@{} foreach ($name in $providers.Keys) { if ($IncludeProvider -and $name -notin $IncludeProvider) { continue } if ($ExcludeProvider -and $name -in $ExcludeProvider) { continue } $runList[$name] = $providers[$name] } # ── Banner ─────────────────────────────────────────────────────────────────── $divider = '=' * 60 Write-Host '' Write-Host $divider -ForegroundColor DarkCyan Write-Host ' WhatsUpGoldPS Discovery E2E Tests' -ForegroundColor Cyan Write-Host $divider -ForegroundColor DarkCyan Write-Host " Providers queued : $($runList.Count) / $($providers.Count)" -ForegroundColor White Write-Host " Output : $OutputPath" -ForegroundColor White Write-Host " Timestamp : $timestamp" -ForegroundColor White Write-Host " Mode : $(if ($NonInteractive) { 'NonInteractive (vault-only)' } else { 'Interactive' })" -ForegroundColor White # Show vault credential status $vaultReady = @($vaultStatus.Keys | Where-Object { $vaultStatus[$_] -in 'vault','new','user-provided' }) $vaultMissing = @($vaultStatus.Keys | Where-Object { $vaultStatus[$_] -in 'no-cred','skipped' }) if ($vaultReady.Count -gt 0) { Write-Host " Vault ready : $($vaultReady -join ', ')" -ForegroundColor Green } if ($vaultMissing.Count -gt 0) { Write-Host " No vault creds : $($vaultMissing -join ', ')" -ForegroundColor DarkGray } Write-Host $divider -ForegroundColor DarkCyan Write-Host '' # ── Run each provider ──────────────────────────────────────────────────────── $dashboardFiles = [System.Collections.Generic.List[string]]::new() foreach ($name in $runList.Keys) { $prov = $runList[$name] $scriptPath = Join-Path $discoveryDir $prov.Script # --- Pre-check --- $preOk = & $prov.PreCheck if (-not $preOk) { Write-Host " [$name] " -ForegroundColor Yellow -NoNewline Write-Host "SKIP " -ForegroundColor DarkGray -NoNewline Write-Host " ($($prov.PreCheckLabel) not available)" -ForegroundColor DarkGray Record-Test -Cmdlet $name -Endpoint 'Pre-check' -Status 'Skipped' -Detail "Prerequisite not met: $($prov.PreCheckLabel)" continue } # --- Verify script exists --- if (-not (Test-Path $scriptPath)) { Write-Host " [$name] " -ForegroundColor Red -NoNewline Write-Host "FAIL " -ForegroundColor Red -NoNewline Write-Host " (script not found: $($prov.Script))" -ForegroundColor DarkGray Record-Test -Cmdlet $name -Endpoint 'Script exists' -Status 'Fail' -Detail "Script not found: $scriptPath" continue } Record-Test -Cmdlet $name -Endpoint 'Script exists' -Status 'Pass' Write-Host " [$name] " -ForegroundColor White -NoNewline Write-Host "RUNNING... " -ForegroundColor Cyan -NoNewline # --- Remove stale dashboard --- $expectedDash = Join-Path $OutputPath $prov.DashboardFile if (Test-Path $expectedDash) { Remove-Item $expectedDash -Force } # --- Execute with Measure-Command --- $runError = $null $argsSplat = $prov.Args $script:_capturedErrors = [System.Collections.Generic.List[string]]::new() $script:_capturedWarnings = [System.Collections.Generic.List[string]]::new() $measured = Measure-Command { try { & $scriptPath @argsSplat 2>&1 3>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $script:_capturedErrors.Add($_.ToString()) } elseif ($_ -is [System.Management.Automation.WarningRecord]) { $script:_capturedWarnings.Add($_.ToString()) } } } catch { $script:_capturedErrors.Add($_.Exception.Message) } } if ($script:_capturedErrors.Count -gt 0) { $runError = $script:_capturedErrors -join '; ' } $script:_capturedErrors = $null $elapsedMs = [Math]::Round($measured.TotalMilliseconds) if ($measured.TotalSeconds -ge 60) { $elapsed = '{0}m {1:0.0}s' -f [Math]::Floor($measured.TotalMinutes), ($measured.TotalSeconds % 60) } else { $elapsed = '{0:0.0}s' -f $measured.TotalSeconds } $elapsedDetail = '{0:0.000}s ({1}ms)' -f $measured.TotalSeconds, $elapsedMs if ($runError) { Write-Host "FAIL" -ForegroundColor Red -NoNewline Write-Host " ($elapsed) $runError" -ForegroundColor DarkGray Record-Test -Cmdlet $name -Endpoint 'Execute' -Status 'Fail' -Duration $elapsed -DurationMs $elapsedMs -Detail "$runError" Record-Test -Cmdlet $name -Endpoint 'Dashboard generated' -Status 'Fail' -DurationMs $elapsedMs -Detail 'Execution failed' # Wipe vault credential on failure so next run prompts for fresh creds if ($prov.VaultName) { $vaultCred = Get-DiscoveryCredential -Name $prov.VaultName -ErrorAction SilentlyContinue if ($vaultCred) { Remove-DiscoveryCredential -Name $prov.VaultName -Confirm:$false Write-Host " [$name] " -ForegroundColor White -NoNewline Write-Host "Wiped vault credential '$($prov.VaultName)' due to failure" -ForegroundColor Yellow } } continue } # --- Check if warnings indicate no data / connectivity issues --- $noDataWarnings = @($script:_capturedWarnings | Where-Object { $_ -match 'No items discovered' -or $_ -match 'No data to generate' -or $_ -match 'connection methods failed' -or $_ -match 'failed to authenticate' -or $_ -match 'Cannot generate dashboard' -or $_ -match 'No.*credentials available' }) $script:_capturedWarnings = $null if ($noDataWarnings.Count -gt 0) { $warnDetail = $noDataWarnings -join '; ' Write-Host "SKIP" -ForegroundColor Yellow -NoNewline Write-Host " ($elapsed) $warnDetail" -ForegroundColor DarkGray Record-Test -Cmdlet $name -Endpoint 'Execute' -Status 'Skipped' -Duration $elapsed -DurationMs $elapsedMs -Detail $warnDetail Record-Test -Cmdlet $name -Endpoint 'Dashboard generated' -Status 'Skipped' -DurationMs $elapsedMs -Detail 'No data available' continue } $script:_capturedWarnings = $null Record-Test -Cmdlet $name -Endpoint 'Execute' -Status 'Pass' -Duration $elapsed -DurationMs $elapsedMs -Detail $elapsedDetail # --- Verify dashboard --- if (Test-Path $expectedDash) { $fileSize = (Get-Item $expectedDash).Length $sizeKB = [Math]::Round($fileSize / 1KB) Write-Host "PASS" -ForegroundColor Green -NoNewline Write-Host " ($elapsed, ${sizeKB}KB)" -ForegroundColor Gray Record-Test -Cmdlet $name -Endpoint 'Dashboard generated' -Status 'Pass' -Duration $elapsed -DurationMs $elapsedMs -Detail "$($prov.DashboardFile) (${sizeKB}KB)" $dashboardFiles.Add($expectedDash) } else { Write-Host "FAIL" -ForegroundColor Red -NoNewline Write-Host " ($elapsed, dashboard not found)" -ForegroundColor DarkGray Record-Test -Cmdlet $name -Endpoint 'Dashboard generated' -Status 'Fail' -Duration $elapsed -DurationMs $elapsedMs -Detail "Expected: $expectedDash" } } # ── Scheduled Task E2E Test ────────────────────────────────────────────────── # Tests the full scheduled task lifecycle: register → fire → verify output → cleanup. # Uses the first provider that passed its dashboard test as the candidate. # Requires admin rights (Register-ScheduledTask). $schedTestProvider = $null $schedTestDashFile = $null # Pick a provider whose dashboard generation already succeeded foreach ($df in $dashboardFiles) { $dfName = [System.IO.Path]::GetFileName($df) foreach ($pName in $providers.Keys) { if ($providers[$pName].DashboardFile -eq $dfName) { $schedTestProvider = $pName $schedTestDashFile = $dfName break } } if ($schedTestProvider) { break } } if (-not $schedTestProvider) { Write-Host '' Write-Host " [SchedTask] " -ForegroundColor Yellow -NoNewline Write-Host "SKIP (no provider dashboard passed — cannot test scheduled task)" -ForegroundColor DarkGray Record-Test -Cmdlet 'SchedTask' -Endpoint 'Pre-check' -Status 'Skipped' -Detail 'No provider dashboard passed' } else { $registerScript = Join-Path $discoveryDir 'Register-DiscoveryScheduledTask.ps1' $schedTaskName = "DiscoverySyncE2ETest-$schedTestProvider-$timestamp" $schedTaskFolder = '\WhatsUpGoldPS' $schedOutputDir = Join-Path $OutputPath "SchedTask-$timestamp" if (-not (Test-Path $schedOutputDir)) { New-Item -ItemType Directory -Path $schedOutputDir -Force | Out-Null } $schedProv = $providers[$schedTestProvider] $schedTarget = $schedProv.Args['Target'] if (-not $schedTarget) { $schedTarget = $schedProv.Args['Region'] } if (-not $schedTarget) { $schedTarget = $schedProv.Args['TenantId'] } if (-not $schedTarget) { $schedTarget = $schedProv.Args['TenancyId'] } Write-Host '' Write-Host " [SchedTask] " -ForegroundColor White -NoNewline Write-Host "Testing scheduled task lifecycle ($schedTestProvider)..." -ForegroundColor Cyan # --- Test 1: Register the task --- Write-Host " [SchedTask] " -ForegroundColor White -NoNewline Write-Host "Registering... " -ForegroundColor Cyan -NoNewline $regError = $null $regMeasured = Measure-Command { try { $regArgs = @{ Mode = 'Provider' Provider = $schedTestProvider Action = 'Dashboard' TaskName = $schedTaskName TriggerType = 'Once' TimeOfDay = '23:59' OutputPath = $schedOutputDir UseSystemVault = ($VaultScope -eq 'LocalMachine') SkipVaultPopulate = ($VaultScope -eq 'LocalMachine') } if ($schedTarget) { $regArgs['Target'] = $schedTarget } & $registerScript @regArgs 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $regError = $_.ToString() } } } catch { $regError = $_.Exception.Message } } $regMs = [Math]::Round($regMeasured.TotalMilliseconds) $regElapsed = '{0:0.0}s' -f $regMeasured.TotalSeconds if ($regError) { Write-Host "FAIL ($regElapsed) $regError" -ForegroundColor Red Record-Test -Cmdlet 'SchedTask' -Endpoint 'Register' -Status 'Fail' -Duration $regElapsed -DurationMs $regMs -Detail $regError } else { # Verify task exists in Task Scheduler $taskObj = Get-ScheduledTask -TaskName $schedTaskName -TaskPath "$schedTaskFolder\" -ErrorAction SilentlyContinue if ($taskObj) { Write-Host "PASS ($regElapsed)" -ForegroundColor Green Record-Test -Cmdlet 'SchedTask' -Endpoint 'Register' -Status 'Pass' -Duration $regElapsed -DurationMs $regMs -Detail "Task: $schedTaskName" } else { Write-Host "FAIL (task not found in scheduler)" -ForegroundColor Red Record-Test -Cmdlet 'SchedTask' -Endpoint 'Register' -Status 'Fail' -Duration $regElapsed -DurationMs $regMs -Detail 'Task not found after registration' } } # --- Test 2: Fire the task and wait for completion --- if ($taskObj) { Write-Host " [SchedTask] " -ForegroundColor White -NoNewline Write-Host "Firing task... " -ForegroundColor Cyan -NoNewline $fireError = $null # Remove any stale dashboard from the scheduled output dir $schedExpectedDash = Join-Path $schedOutputDir $schedTestDashFile if (Test-Path $schedExpectedDash) { Remove-Item $schedExpectedDash -Force -ErrorAction SilentlyContinue } $fireMeasured = Measure-Command { try { Start-ScheduledTask -TaskName $schedTaskName -TaskPath "$schedTaskFolder\" -ErrorAction Stop # Poll until task completes (max 120 seconds) $maxWait = 120 $waited = 0 $pollInterval = 3 while ($waited -lt $maxWait) { Start-Sleep -Seconds $pollInterval $waited += $pollInterval $info = Get-ScheduledTaskInfo -TaskName $schedTaskName -TaskPath "$schedTaskFolder\" -ErrorAction SilentlyContinue $state = (Get-ScheduledTask -TaskName $schedTaskName -TaskPath "$schedTaskFolder\" -ErrorAction SilentlyContinue).State if ($state -ne 'Running') { break } } if ($state -eq 'Running') { $fireError = "Task still running after ${maxWait}s" } } catch { $fireError = $_.Exception.Message } } $fireMs = [Math]::Round($fireMeasured.TotalMilliseconds) $fireElapsed = '{0:0.0}s' -f $fireMeasured.TotalSeconds if ($fireError) { Write-Host "FAIL ($fireElapsed) $fireError" -ForegroundColor Red Record-Test -Cmdlet 'SchedTask' -Endpoint 'Fire + complete' -Status 'Fail' -Duration $fireElapsed -DurationMs $fireMs -Detail $fireError } else { # Check last run result (0 = success) $taskInfo = Get-ScheduledTaskInfo -TaskName $schedTaskName -TaskPath "$schedTaskFolder\" -ErrorAction SilentlyContinue $lastResult = $taskInfo.LastTaskResult if ($lastResult -eq 0) { Write-Host "PASS ($fireElapsed, exit 0)" -ForegroundColor Green Record-Test -Cmdlet 'SchedTask' -Endpoint 'Fire + complete' -Status 'Pass' -Duration $fireElapsed -DurationMs $fireMs -Detail "Completed in $fireElapsed (exit 0)" } else { Write-Host "WARN ($fireElapsed, exit $lastResult)" -ForegroundColor Yellow Record-Test -Cmdlet 'SchedTask' -Endpoint 'Fire + complete' -Status 'Pass' -Duration $fireElapsed -DurationMs $fireMs -Detail "Completed in $fireElapsed (exit $lastResult)" } } # --- Test 3: Verify output (log file + dashboard) --- Write-Host " [SchedTask] " -ForegroundColor White -NoNewline Write-Host "Checking output... " -ForegroundColor Cyan -NoNewline $logDir = Join-Path $schedOutputDir 'logs' $logFiles = @() if (Test-Path $logDir) { $logFiles = @(Get-ChildItem -Path $logDir -Filter "${schedTaskName}_*.log" -ErrorAction SilentlyContinue) } $schedDashPath = Join-Path $schedOutputDir $schedTestDashFile $outputDetails = @() if ($logFiles.Count -gt 0) { $logSizeKB = [Math]::Round(($logFiles | Measure-Object -Property Length -Sum).Sum / 1KB) $outputDetails += "Log: $($logFiles.Count) file(s), ${logSizeKB}KB" } else { $outputDetails += 'Log: not found' } if (Test-Path $schedDashPath) { $dashSizeKB = [Math]::Round((Get-Item $schedDashPath).Length / 1KB) $outputDetails += "Dashboard: ${dashSizeKB}KB" } else { $outputDetails += 'Dashboard: not found' } $outputDetail = $outputDetails -join ' | ' if ($logFiles.Count -gt 0 -and (Test-Path $schedDashPath)) { Write-Host "PASS ($outputDetail)" -ForegroundColor Green Record-Test -Cmdlet 'SchedTask' -Endpoint 'Output verified' -Status 'Pass' -Detail $outputDetail } elseif ($logFiles.Count -gt 0) { # Log exists but no dashboard — check log for warnings $logContent = Get-Content -Path $logFiles[0].FullName -Raw -ErrorAction SilentlyContinue if ($logContent -match 'No data to generate|No items discovered|connection methods failed') { Write-Host "SKIP ($outputDetail — no data)" -ForegroundColor Yellow Record-Test -Cmdlet 'SchedTask' -Endpoint 'Output verified' -Status 'Skipped' -Detail "$outputDetail (no data in discovery)" } else { Write-Host "FAIL ($outputDetail)" -ForegroundColor Red Record-Test -Cmdlet 'SchedTask' -Endpoint 'Output verified' -Status 'Fail' -Detail $outputDetail } } else { Write-Host "FAIL ($outputDetail)" -ForegroundColor Red Record-Test -Cmdlet 'SchedTask' -Endpoint 'Output verified' -Status 'Fail' -Detail $outputDetail } # --- Cleanup: remove the test task --- Write-Host " [SchedTask] " -ForegroundColor White -NoNewline Write-Host "Cleanup... " -ForegroundColor Cyan -NoNewline try { Unregister-ScheduledTask -TaskName $schedTaskName -TaskPath "$schedTaskFolder\" -Confirm:$false -ErrorAction Stop Write-Host "PASS (task removed)" -ForegroundColor Green Record-Test -Cmdlet 'SchedTask' -Endpoint 'Cleanup' -Status 'Pass' -Detail "Removed $schedTaskName" } catch { Write-Host "WARN ($($_.Exception.Message))" -ForegroundColor Yellow Record-Test -Cmdlet 'SchedTask' -Endpoint 'Cleanup' -Status 'Pass' -Detail "Cleanup warning: $($_.Exception.Message)" } } } # ── HTML Test Report ───────────────────────────────────────────────────────── $templatePath = Join-Path $scriptDir 'Test-Dashboard-Template.html' $reportPath = Join-Path $OutputPath "DiscoveryE2E-Report-${timestamp}.html" $exportedPath = Export-TestResultsHtml -TestResults $script:TestResults ` -OutputFilePath $reportPath ` -TemplatePath $templatePath ` -ReportTitle "Discovery E2E Tests - $timestamp" # ── Summary ────────────────────────────────────────────────────────────────── $pass = @($script:TestResults | Where-Object { $_.Status -eq 'Pass' }).Count $fail = @($script:TestResults | Where-Object { $_.Status -eq 'Fail' }).Count $skipped = @($script:TestResults | Where-Object { $_.Status -eq 'Skipped' }).Count Write-Host '' Write-Host $divider -ForegroundColor DarkCyan Write-Host ' RESULTS SUMMARY' -ForegroundColor Cyan Write-Host $divider -ForegroundColor DarkCyan Write-Host '' foreach ($r in $script:TestResults) { $color = switch ($r.Status) { 'Pass' { 'Green' } 'Fail' { 'Red' } 'Skipped' { 'DarkGray' } default { 'Yellow' } } $icon = switch ($r.Status) { 'Pass' { '[+]' } 'Fail' { '[-]' } 'Skipped' { '[ ]' } default { '[?]' } } Write-Host " $icon " -ForegroundColor $color -NoNewline Write-Host ("{0,-22}" -f $r.Cmdlet) -ForegroundColor White -NoNewline Write-Host ("{0,-22}" -f $r.Endpoint) -ForegroundColor Gray -NoNewline if ($r.Duration) { Write-Host ("{0,-12}" -f $r.Duration) -ForegroundColor Gray -NoNewline } else { Write-Host ("{0,-12}" -f '-') -ForegroundColor DarkGray -NoNewline } Write-Host $r.Detail -ForegroundColor $color } Write-Host '' $summaryColor = if ($fail -gt 0) { 'Red' } elseif ($pass -gt 0) { 'Green' } else { 'Yellow' } Write-Host " Total: $($script:TestResults.Count) | Pass: $pass | Fail: $fail | Skipped: $skipped" -ForegroundColor $summaryColor if ($exportedPath) { Write-Host " Report: $exportedPath" -ForegroundColor Green } Write-Host '' # ── Open dashboards / report ───────────────────────────────────────────────── if ($OpenDashboards -and $dashboardFiles.Count -gt 0) { Write-Host " Opening $($dashboardFiles.Count) dashboard(s)..." -ForegroundColor Cyan foreach ($d in $dashboardFiles) { Start-Process $d } } if ($OpenReport -and $exportedPath -and (Test-Path $exportedPath)) { Write-Host " Opening report..." -ForegroundColor Cyan Start-Process $exportedPath } # ── Return results for pipeline use ───────────────────────────────────────── $script:TestResults # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD6+wGPjEZnlv9/ # wZK4JFPE3Thd7QTNxcuYfv4aUEtgj6CCJQ0wggVvMIIEV6ADAgECAhBI/JO0YFWU # jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI # DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM # EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy # dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG # EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv # IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s # hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD # J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7 # P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme # me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz # T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q # RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz # mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc # QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T # OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/ # AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID # AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD # VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV # HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE # VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v # ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE # KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI # hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF # OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC # J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ # pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl # d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH # +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggWNMIIEdaADAgECAhAOmxiO # +dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi # BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAw # MDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp # Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERp # Z2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsb # hA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iT # cMKyunWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGb # NOsFxl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclP # XuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCr # VYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFP # ObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTv # kpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWM # cCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls # 5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBR # a2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6 # MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8E # BAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDig # NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCg # v0NcVec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQT # SnovLbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh # 65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSw # uKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAO # QGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjD # TZ9ztwGpn1eqXijiuZQwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0G # CSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExp # bWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290 # IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYT # AkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28g # UHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IB # jwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEe # AEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGV # oYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk # 8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzh # P06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41 # aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+ # ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/ # 2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9g # Rvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhA # QnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8B # Af8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcD # AzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyG # Omh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5n # Um9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8v # Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYu # cDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG # 9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW # 4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIh # rCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYp # mlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7Dc # ML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yE # Lg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRz # NyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw # 4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6 # E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6 # xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZt # g0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggY+ # MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqGSIb3DQEBDAUAMFQxCzAJ # BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl # Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwHhcNMjYwMjA5MDAwMDAw # WhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLQ29ubmVj # dGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcwFQYDVQQDDA5KYXNvbiBB # bGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPN6aN4B1yYW # kI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyEtRYPxEmNJL3A38Bkg7mw # zPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d67MlJLUAEufl18tOr3ST1 # DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2h4KdtRAnCqwM+/V7iBdb # w+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAADLdjZr5ip8vIWbf857Yw1 # Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZBBL56l4YAlIVRsrOiE1k # dHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDeyIq+Oztz1LvEdy8Rcd0J # BY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN79CJ90UTZb2+I+tamj0d # n+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+PhhamOxF2px9LObhBLLEMyRs # CHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi2ttn6lLOPThXMiQaooRU # q6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSMjDSRFDfHRCdGbZsL/keE # LJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJMIIBhTAfBgNVHSMEGDAW # gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU6YF0o0D5AVhKHbVocr8G # aSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI # KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC # ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw # PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT # aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0 # cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz # Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqG # SIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvCwOA/RYQnFJD7R/bSQRu3 # wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03J5PrDTp1WPCWbXKX8Oz9 # vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9URrpiLPJ9rQjfHMb1BUd # vaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6sX93wphwJNBJAstH9Ozs6 # LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+zzieGEI50fKnN77FfNKFO # WKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcjGSHw4lTg1Wx+WMAMdl/7 # nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjsNrsk9ZlalBi5SHkIu+F6 # g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1nSX8OM++AXoyVGO+iTj7 # z+pl6XE9Gw/Td6WKKKswgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0G # CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy # NTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHT # CphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPh # of6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mA # xAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBv # MgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps # 0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF # 83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXi # UOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOM # CZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydP # pOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrU # G2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+ # sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s # BwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WI # GjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+ # IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8M # yb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2 # th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjaj # V/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2 # Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFze # GxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG # 7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+N # Jpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckT # etiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszW # kPZPubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0B # AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVow # YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD # EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw # MjUgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5g # VrMrV7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN # +vo8dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qo # me7MrxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ/ # /nBZZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouT # MYFOnHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8 # DD+nigNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnp # JeItK/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP5 # 1ho1zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49 # kPmk8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5P # WPsWeupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7Y # ufAkprxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA # MB0GA1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK # 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI # KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT # SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k # aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2 # U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9 # bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP # 2zWLpQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O # 6Lgjg8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskg # iC3QYIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMU # BaB5bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDF # kxUGtMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+ # zJNEsuEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/l # wd6UArb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxl # RcGG0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2 # zRWVFjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJg # baP5t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOC # IUjsarfNZzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl # Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu # aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY # BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3 # AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi # BCCH2OvvL5Jf/z4R81EaloTfeScxggfYNZuSsxiWtwNCMzANBgkqhkiG9w0BAQEF # AASCAgCd0DCZQKezfPwWTbkyOSz8JfL+XuOz8f6R/KwMV/AqGWcdwKt17TQuXfBU # WVqB26nx0PNL+Mz92tMwNQrZwrKS3UwUjI9n58qVD5cgAI0MIt/NZwHEeinaTUu+ # MCTcjlpei2TAjQXAE0+VCgZn18jVYmvegfsfMyE0t5Qe1+qkt5A32A4U79VM+ofm # 4EYn3/UBopAN4WAJc+gboRTHPzxRPiyBXfyBOOxWbdIuypFyCgUmVUWS0dPAr8yy # g0C/A0E+OLo8/FG2UNuI8NBOyuDJYycs7k2oQoeskdxgU2IpF8kuzkC/yqutCXoL # PWOxNnNItch+EZ1MDguUMbH0rZRMo6hHvNjwqtNlCcoONsux4GG+Fli3+InaWPYH # vsiW89MkmR+QGR3c5TmMkIhX9dTvDD+VhW79UD1bj3IkfIZ1ssu6aDEPVvYK8eCU # afLYNHvJtgt7oT1cYNolvMj4cIHMEERr3SEar7pyWo+3JO6doNgn/FlBrKfsrlVG # HaSP//gsr0ivInmPtR1PHtA/gaRJa8jnEuvcc+K4HHmljcm0dAySTzGXh6cRyGDd # cHwlL1gg7J6syE9bolj/KXY547Ze0brifyG2M5jYezdyqVaj3TuKWtJYMwgcLP1y # mmzjDGIz19HLRVafFNWABfbmOFWGCqc5C75xlY7dKtz3L0RUUKGCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV # dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA3MjkyMjA5MzVaMC8GCSqGSIb3DQEJBDEiBCDC03V6 # 5KOWm/l3aAhhKz56Q6j2o6ifcf2SuXLEVeek9TANBgkqhkiG9w0BAQEFAASCAgDI # up4zs5vX3ZMttMWG+QRiACxX8Y5k4tJVqPPhW3KZCBp1fCh2IcnByEeUTdXh+vcI # 4jleoAcCL+S964HPPvJCAQW4Fb+/8m6fcMdC2eiMfX4yfLnXQAgM3LiiRSZi84e9 # qUz2WPGWWSi67wdjIaZve6efMXP9p4piDODGmuTdu8GWMLGv5/a2MErah37B2f5n # Lyx1uLVciSOeU/y1GXDcSV/8ZwFiseWZwQpn9X+TcOqYxk9uNnZNXCx+xZGufi5L # 6LpIpD8Dq9FYKNy1VPLBnNt2SY7Jg5D0KalSdArc2c/gL3/d4WB75TYY1t09bBz2 # Zlp14ZXsIT3KtdLFfDuriBz7KJABBtio8EePbxKddB2hKOu804mFBCM5TKpW2LIw # iTVZsFICXBiZ0F/RoFN9Y6zg4X9xk2l5QmlNHNflrMOR/i1u5B7W1uBEBJM+Caxb # lZuweVI+vDcV2oX/F/y63SiC6LBdWvd7dlWdTbzcjcyPXQfW7n1YHvOsIxtTjPm3 # LRh6H+6w3HjzTY9Jg8bEAXvQ2EFalX2KxRCgT8XzpcPfhiGYqbUo/tLQ9iB3aI42 # clZwi8zmhjhjSytS+CPNiQehyse1ug4DhOn6xFFpyenjojoDSEa/U1fHX1+fMWga # WGgpc1AvVXbHabpmeyuyp+sc8ySwJIQnRhd8zIGksQ== # SIG # End signature block |