helpers/discovery/Setup-Docker-Discovery.ps1
|
<#
.SYNOPSIS Docker Discovery -- Discover Docker hosts and containers, then optionally push to WhatsUp Gold as monitored devices. .DESCRIPTION Interactive script that discovers Docker hosts and running containers via the Docker Engine REST API, then lets you choose what to do with the results: [1] Push monitors to WhatsUp Gold (creates devices + REST API monitors) [2] Export discovery plan to JSON [3] Export discovery plan to CSV [4] Show full plan table in console [5] Exit Architecture (when pushed to WUG): [Docker Host device in WUG] |-- "Docker Host Health - <host>" (REST API Active Monitor) |-- "Docker Container Health - <container>" (REST API Active Monitor) |-- "memory_usage - <container> (Docker)" (REST API Perf Monitor) |-- "network_rx_bytes - <container> (Docker)" (REST API Perf Monitor) '-- "network_tx_bytes - <container> (Docker)" (REST API Perf Monitor) First Run: 1. Prompts for Docker host(s) and port 2. Discovers host info and running containers 3. Shows summary, then asks what to do Subsequent Runs: Docker Engine API typically uses anonymous access (port 2375). .PARAMETER Target Docker host(s) -- IP address or FQDN. Accepts multiple values. .PARAMETER ApiPort Docker API port. Default: 2375 (unencrypted). .PARAMETER Action What to do with discovery results. When specified, skips the interactive menu. Valid values: PushToWUG, ExportJSON, ExportCSV, ShowTable, None. .PARAMETER WUGServer WhatsUp Gold server address. Default: 192.168.74.74. .PARAMETER WUGCredential PSCredential for WhatsUp Gold admin login (non-interactive WUG push). .PARAMETER NonInteractive Suppress all prompts. Uses parameter defaults. .NOTES Docker Engine API must be exposed on a TCP port (default 2375). For TLS-secured Docker (port 2376), certificate auth is required. .EXAMPLE .\Setup-Docker-Discovery.ps1 # Interactive mode. .EXAMPLE .\Setup-Docker-Discovery.ps1 -Target '10.0.0.5','10.0.0.6' -Action PushToWUG -NonInteractive # Discover two Docker hosts and push to WUG. .EXAMPLE .\Setup-Docker-Discovery.ps1 -Target '10.0.0.5' -ApiPort 2376 -Action ExportJSON # Discover a TLS Docker host and export plan. #> [CmdletBinding()] param( [string[]]$Target, [int]$ApiPort = 2375, [ValidateSet('PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'None')] [string]$Action, [string]$WUGServer = '192.168.74.74', [PSCredential]$WUGCredential, [string]$OutputPath, [switch]$NonInteractive ) # --- Output directory --------------------------------------------------------- if (-not $OutputPath) { if ($NonInteractive) { $OutputPath = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Output' } else { $OutputPath = $env:TEMP } } if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } $OutputDir = $OutputPath $DefaultHost = '10.0.0.5' $DockerPort = $ApiPort # --- Load helpers ------------------------------------------------------------- $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent . (Join-Path $scriptDir 'DiscoveryHelpers.ps1') . (Join-Path $scriptDir 'DiscoveryProvider-Docker.ps1') # Load dynamic dashboard generator $dynDashPath = Join-Path (Split-Path $scriptDir -Parent) 'reports\Export-DynamicDashboardHtml.ps1' if (Test-Path $dynDashPath) { . $dynDashPath } # ============================================================================== # STEP 1: Gather target info # ============================================================================== Write-Host "=== Docker Discovery ===" -ForegroundColor Cyan Write-Host "" if ($Target) { $DockerHosts = @($Target) } elseif ($NonInteractive) { $DockerHosts = @($DefaultHost) } else { Write-Host "Enter Docker host(s) -- IP address or FQDN." -ForegroundColor Cyan Write-Host "For multiple hosts, separate with commas." -ForegroundColor Gray $hostInput = Read-Host -Prompt "Docker host(s) [default: $DefaultHost]" if ([string]::IsNullOrWhiteSpace($hostInput)) { $hostInput = $DefaultHost } $DockerHosts = @($hostInput -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } if ($DockerHosts.Count -eq 0) { Write-Error 'No valid host provided.'; return } Write-Host "Targets: $($DockerHosts -join ', ')" -ForegroundColor Cyan Write-Host "" # --- Port prompt --- if (-not $PSBoundParameters.ContainsKey('ApiPort') -and -not $NonInteractive) { $portInput = Read-Host -Prompt "Docker API port [default: $DockerPort]" if ($portInput -and $portInput -match '^\d+$') { $DockerPort = [int]$portInput } } # ============================================================================== # STEP 2: Docker uses anonymous access by default (no auth needed) # ============================================================================== $dockerCredential = @{ Anonymous = $true } Write-Host "Using anonymous access (Docker Engine API)." -ForegroundColor Green Write-Host "" # ============================================================================== # STEP 3: Discover -- query Docker Engine API # ============================================================================== Write-Host "Querying Docker at $($DockerHosts -join ', ')..." -ForegroundColor Cyan $plan = Invoke-Discovery -ProviderName 'Docker' ` -Target $DockerHosts ` -ApiPort $DockerPort ` -Credential $dockerCredential if (-not $plan -or $plan.Count -eq 0) { Write-Warning "No items discovered. Check Docker connectivity and API port." return } # ============================================================================== # STEP 4: Show the plan # ============================================================================== $devicePlan = [ordered]@{} foreach ($item in $plan) { $hostName = $item.Attributes['Docker.HostName'] if (-not $hostName) { $hostName = $item.Attributes['Docker.HostIP'] } $key = "host:$hostName" if (-not $devicePlan.Contains($key)) { $devicePlan[$key] = @{ Name = $hostName IP = $item.Attributes['Docker.HostIP'] Type = 'DockerHost' Attrs = $item.Attributes Items = [System.Collections.ArrayList]@() } } [void]$devicePlan[$key].Items.Add($item) } $hostDevices = @($devicePlan.Values) $activeTemplates = @($plan | Where-Object { $_.ItemType -eq 'ActiveMonitor' } | Select-Object -ExpandProperty Name -Unique) $perfTemplates = @($plan | Where-Object { $_.ItemType -eq 'PerformanceMonitor' } | Select-Object -ExpandProperty Name -Unique) Write-Host "" Write-Host "Discovery complete!" -ForegroundColor Green Write-Host " Docker hosts: $($hostDevices.Count)" -ForegroundColor White Write-Host " Active monitor templates: $($activeTemplates.Count) ($(@($plan | Where-Object { $_.ItemType -eq 'ActiveMonitor' }).Count) assignments)" -ForegroundColor White Write-Host " Perf monitor templates: $($perfTemplates.Count) ($(@($plan | Where-Object { $_.ItemType -eq 'PerformanceMonitor' }).Count) assignments)" -ForegroundColor White Write-Host " Total plan items: $($plan.Count)" -ForegroundColor White Write-Host "" Write-Host "Monitor templates:" -ForegroundColor Cyan foreach ($t in $activeTemplates) { Write-Host " [Active] $t" -ForegroundColor White } foreach ($t in $perfTemplates) { Write-Host " [Perf] $t" -ForegroundColor White } Write-Host "" $devicePlan.Values | Sort-Object @{E={$_.Name}} | ForEach-Object { [PSCustomObject]@{ Device = $_.Name Type = $_.Type IP = if ($_.IP) { $_.IP } else { 'N/A' } Monitors = $_.Items.Count }} | Format-Table -AutoSize # ============================================================================== # STEP 5: Export or push to WUG # ============================================================================== $choice = $null if ($Action) { switch ($Action) { 'PushToWUG' { $choice = '1' } 'ExportJSON' { $choice = '2' } 'ExportCSV' { $choice = '3' } 'ShowTable' { $choice = '4' } 'Dashboard' { $choice = '5' } 'None' { $choice = '6' } 'DashboardAndPush' { $choice = '7' } } } if (-not $choice -and $NonInteractive) { $choice = '5' } if (-not $choice) { Write-Host "What would you like to do?" -ForegroundColor Cyan Write-Host " [1] Push monitors to WhatsUp Gold" Write-Host " [2] Export plan to JSON file" Write-Host " [3] Export plan to CSV file" Write-Host " [4] Show full plan table" Write-Host " [5] Generate HTML dashboard" Write-Host " [6] Exit (do nothing)" Write-Host " [7] Dashboard + Push to WUG" Write-Host "" $choice = Read-Host -Prompt "Choice [1-7]" } # Handle DashboardAndPush: run Dashboard then PushToWUG sequentially if ($choice -eq '7') { $actionsToRun = @('5', '1') } else { $actionsToRun = @($choice) } foreach ($currentChoice in $actionsToRun) { switch ($currentChoice) { '1' { # --- Push to WUG --- Write-Host "Loading WhatsUpGoldPS module..." -ForegroundColor Cyan try { $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $repoPsd1 = Join-Path $repoRoot 'WhatsUpGoldPS.psd1' if (Test-Path $repoPsd1) { Import-Module $repoPsd1 -Force -ErrorAction Stop } else { Import-Module WhatsUpGoldPS -ErrorAction Stop } } catch { Write-Error "Could not load WhatsUpGoldPS module: $_"; return } # Dot-source internal helper so scripts can call Get-WUGAPIResponse directly $apiResponsePath = Join-Path $PSScriptRoot '..\..\functions\Get-WUGAPIResponse.ps1' if (Test-Path $apiResponsePath) { . $apiResponsePath } if ($WUGCredential) { Connect-WUGServer -serverUri $WUGServer -Credential $WUGCredential -IgnoreSSLErrors } else { Connect-WUGServer -AutoConnect -IgnoreSSLErrors } $stats = @{ HealthCreated = 0; HealthSkipped = 0; HealthFailed = 0 PerfCreated = 0; PerfSkipped = 0; PerfFailed = 0 DevicesCreated = 0; DevicesFound = 0; CredsAssigned = 0 } $wugDeviceMap = @{} $deviceKeys = @($devicePlan.Keys | Sort-Object) $devTotal = $deviceKeys.Count # ---- 1. Create/find REST API credential (anonymous) --- Write-Host "" Write-Host "Setting up Docker REST API credential in WUG..." -ForegroundColor Cyan $credName = "Docker API (Anonymous)" $dockerCredId = $null try { $existingCreds = @(Get-WUGCredential -Type restapi -SearchValue $credName -View basic) if ($existingCreds.Count -eq 0) { $existingCreds = @(Get-WUGCredential -SearchValue $credName -View basic) } if ($existingCreds.Count -gt 0) { $matchCred = $existingCreds | Where-Object { $_.name -eq $credName } | Select-Object -First 1 if ($matchCred) { $dockerCredId = $matchCred.id; Write-Host " Found existing credential (ID: $dockerCredId)" -ForegroundColor Green } } } catch { } if (-not $dockerCredId) { Write-Host " Creating credential '$credName'..." -ForegroundColor Yellow try { $credResult = Add-WUGCredential -Name $credName ` -Description "Docker API anonymous credential (auto-created by discovery)" ` -Type restapi ` -RestApiUsername '' ` -RestApiPassword '' ` -RestApiAuthType '0' ` -RestApiIgnoreCertErrors 'True' if ($credResult) { if ($credResult.PSObject.Properties['data']) { $dockerCredId = $credResult.data.idMap.resultId } elseif ($credResult.PSObject.Properties['resourceId']) { $dockerCredId = $credResult.resourceId } elseif ($credResult.PSObject.Properties['id']) { $dockerCredId = $credResult.id } if ($dockerCredId) { Write-Host " Created credential (ID: $dockerCredId)" -ForegroundColor Green } } } catch { } if (-not $dockerCredId) { Write-Warning "Could not create REST API credential." } } # ---- 2a. Create active monitors --- Write-Host "" Write-Host " Creating active monitors in library..." -ForegroundColor Cyan $uniqueActiveMonitors = @{} foreach ($key in $deviceKeys) { foreach ($actItem in @($devicePlan[$key].Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' })) { if ($actItem.Name -and -not $uniqueActiveMonitors.ContainsKey($actItem.Name)) { $uniqueActiveMonitors[$actItem.Name] = $actItem } } } $existingActiveNames = @{} foreach ($actName in @($uniqueActiveMonitors.Keys)) { try { $found = @(Get-WUGActiveMonitor -Search $actName) $exact = $found | Where-Object { $_.name -eq $actName } | Select-Object -First 1 if ($exact) { $existingActiveNames[$actName] = [int]$exact.id } } catch { } } $toCreateActive = @($uniqueActiveMonitors.Keys | Where-Object { -not $existingActiveNames.ContainsKey($_) }) $stats.HealthSkipped = $uniqueActiveMonitors.Count - $toCreateActive.Count if ($toCreateActive.Count -gt 0) { Write-Host " Creating $($toCreateActive.Count) new active monitors..." -ForegroundColor DarkGray $activeTemplateArr = @() $actTplIdMap = @{} $actTplIdx = 0 foreach ($actName in $toCreateActive) { $actItem = $uniqueActiveMonitors[$actName] $mp = $actItem.MonitorParams $tplId = "act_$actTplIdx"; $actTplIdMap[$tplId] = $actName; $actTplIdx++ $bags = @( @{ name = 'MonRestApi:RestUrl'; value = "$($mp.RestApiUrl)" } @{ name = 'MonRestApi:HttpMethod'; value = if ($mp.RestApiMethod) { "$($mp.RestApiMethod)" } else { 'GET' } } @{ name = 'MonRestApi:HttpTimeoutMs'; value = if ($mp.RestApiTimeoutMs) { "$($mp.RestApiTimeoutMs)" } else { '10000' } } @{ name = 'MonRestApi:IgnoreCertErrors'; value = if ($mp.RestApiIgnoreCertErrors) { "$($mp.RestApiIgnoreCertErrors)" } else { '0' } } @{ name = 'MonRestApi:UseAnonymousAccess'; value = if ($mp.RestApiUseAnonymous) { "$($mp.RestApiUseAnonymous)" } else { '1' } } @{ name = 'MonRestApi:CustomHeader'; value = '' } @{ name = 'MonRestApi:DownIfResponseCodeIsIn'; value = if ($mp.RestApiDownIfResponseCodeIsIn) { "$($mp.RestApiDownIfResponseCodeIsIn)" } else { '[]' } } @{ name = 'MonRestApi:ComparisonList'; value = if ($mp.RestApiComparisonList) { "$($mp.RestApiComparisonList)" } else { '[]' } } @{ name = 'Cred:Type'; value = '8192' } ) $activeTemplateArr += @{ templateId = $tplId name = $actName description = 'Docker RestApi active monitor' useInDiscovery = $false monitorTypeInfo = @{ baseType = 'active'; classId = 'f0610672-d515-4268-bd21-ac5ebb1476ff' } propertyBags = $bags } } try { $bulkActResult = Add-WUGMonitorTemplate -ActiveMonitors $activeTemplateArr if ($bulkActResult.idMap) { foreach ($mapping in $bulkActResult.idMap) { if ($actTplIdMap.ContainsKey($mapping.templateId) -and $mapping.resultId) { $existingActiveNames[$actTplIdMap[$mapping.templateId]] = [int]$mapping.resultId $stats.HealthCreated++ } } } if ($bulkActResult.errors) { foreach ($err in $bulkActResult.errors) { Write-Warning "Active monitor error: $($err.messages -join '; ')" $stats.HealthFailed++ } } } catch { Write-Warning "Bulk active monitor creation failed: $_"; $stats.HealthFailed += $toCreateActive.Count } } Write-Host " Active monitors: $($stats.HealthCreated) created, $($stats.HealthSkipped) existing, $($stats.HealthFailed) failed" -ForegroundColor DarkGray # ---- 2b. Create perf monitors --- Write-Host " Creating performance monitors in library..." -ForegroundColor Cyan $uniquePerfMonitors = @{} foreach ($key in $deviceKeys) { foreach ($perfItem in @($devicePlan[$key].Items | Where-Object { $_.ItemType -eq 'PerformanceMonitor' })) { if ($perfItem.Name -and -not $uniquePerfMonitors.ContainsKey($perfItem.Name)) { $uniquePerfMonitors[$perfItem.Name] = $perfItem } } } $existingPerfNames = @{} foreach ($monName in @($uniquePerfMonitors.Keys)) { try { $found = @(Get-WUGPerformanceMonitor -Search $monName) $exact = $found | Where-Object { $_.name -eq $monName } | Select-Object -First 1 if ($exact) { $existingPerfNames[$monName] = "$($exact.id)" } } catch { } } $toCreatePerf = @($uniquePerfMonitors.Keys | Where-Object { -not $existingPerfNames.ContainsKey($_) }) $stats.PerfSkipped = $uniquePerfMonitors.Count - $toCreatePerf.Count if ($toCreatePerf.Count -gt 0) { Write-Host " Creating $($toCreatePerf.Count) new perf monitors..." -ForegroundColor DarkGray $perfTemplateArr = @() $perfTplIdMap = @{} $perfTplIdx = 0 foreach ($monName in $toCreatePerf) { $perfItem = $uniquePerfMonitors[$monName] $mp = $perfItem.MonitorParams $tplId = "perf_$perfTplIdx"; $perfTplIdMap[$tplId] = $monName; $perfTplIdx++ $bags = @( @{ name = 'RdcRestApi:RestUrl'; value = "$($mp.RestApiUrl)" } @{ name = 'RdcRestApi:JsonPath'; value = "$($mp.RestApiJsonPath)" } @{ name = 'RdcRestApi:HttpMethod'; value = if ($mp.RestApiHttpMethod) { "$($mp.RestApiHttpMethod)" } else { 'GET' } } @{ name = 'RdcRestApi:HttpTimeoutMs'; value = if ($mp.RestApiHttpTimeoutMs) { "$($mp.RestApiHttpTimeoutMs)" } else { '10000' } } @{ name = 'RdcRestApi:IgnoreCertErrors'; value = if ($mp.RestApiIgnoreCertErrors) { "$($mp.RestApiIgnoreCertErrors)" } else { '0' } } @{ name = 'RdcRestApi:UseAnonymousAccess'; value = if ($mp.RestApiUseAnonymousAccess) { "$($mp.RestApiUseAnonymousAccess)" } else { '1' } } @{ name = 'RdcRestApi:CustomHeader'; value = '' } ) $perfTemplateArr += @{ templateId = $tplId name = $monName description = 'Docker RestApi performance monitor' monitorTypeInfo = @{ baseType = 'performance'; classId = '987bb6a4-70f4-4f46-97c6-1c9dd1766437' } propertyBags = $bags } } try { $bulkPerfResult = Add-WUGMonitorTemplate -PerformanceMonitors $perfTemplateArr if ($bulkPerfResult.idMap) { foreach ($mapping in $bulkPerfResult.idMap) { if ($perfTplIdMap.ContainsKey($mapping.templateId) -and $mapping.resultId) { $existingPerfNames[$perfTplIdMap[$mapping.templateId]] = "$($mapping.resultId)" $stats.PerfCreated++ } } } if ($bulkPerfResult.errors) { foreach ($err in $bulkPerfResult.errors) { Write-Warning "Perf monitor error: $($err.messages -join '; ')" $stats.PerfFailed++ } } } catch { Write-Warning "Bulk perf monitor creation failed: $_"; $stats.PerfFailed += $toCreatePerf.Count } } Write-Host " Perf monitors: $($stats.PerfCreated) created, $($stats.PerfSkipped) existing, $($stats.PerfFailed) failed" -ForegroundColor DarkGray # ---- 2c. Check existing vs new devices --- Write-Host " Checking for existing devices..." -ForegroundColor Cyan $existingDevices = @{} $newDeviceKeys = [System.Collections.Generic.List[string]]::new() foreach ($key in $deviceKeys) { $dev = $devicePlan[$key] $displayName = "$($dev.Name) (Docker)" $deviceId = $null try { $searchResults = @(Get-WUGDevice -SearchValue $dev.Name) if ($searchResults.Count -gt 0) { $match = $searchResults | Where-Object { $_.displayName -eq $dev.Name -or $_.displayName -eq $displayName -or $_.hostName -eq $dev.Name } | Select-Object -First 1 if ($match) { $deviceId = $match.id } } if (-not $deviceId -and $dev.IP) { $searchResults = @(Get-WUGDevice -SearchValue $dev.IP) $match = $searchResults | Where-Object { $_.networkAddress -eq $dev.IP } | Select-Object -First 1 if ($match) { $deviceId = $match.id } } } catch { } if ($deviceId) { $existingDevices[$key] = $deviceId; $wugDeviceMap[$key] = $deviceId; $stats.DevicesFound++ } else { $newDeviceKeys.Add($key) } } Write-Host " Found $($stats.DevicesFound) existing, $($newDeviceKeys.Count) new" -ForegroundColor DarkGray # ---- 2d. Create new devices --- if ($newDeviceKeys.Count -gt 0) { Write-Host " Creating $($newDeviceKeys.Count) devices..." -ForegroundColor Yellow foreach ($key in $newDeviceKeys) { $dev = $devicePlan[$key] $addIP = if ($dev.IP) { $dev.IP } else { '0.0.0.0' } $displayName = "$($dev.Name) (Docker)" $devAttrs = @() foreach ($attrName in $dev.Attrs.Keys) { if ($dev.Attrs[$attrName]) { $devAttrs += @{ name = $attrName; value = "$($dev.Attrs[$attrName])" } } } $actNames = @() $seenAct = @{} foreach ($actItem in @($dev.Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' })) { if ($actItem.Name -and $existingActiveNames.ContainsKey($actItem.Name) -and -not $seenAct.ContainsKey($actItem.Name)) { $actNames += $actItem.Name; $seenAct[$actItem.Name] = $true } } $perfNames = @() $seenPerf = @{} foreach ($perfItem in @($dev.Items | Where-Object { $_.ItemType -eq 'PerformanceMonitor' })) { if ($perfItem.Name -and $existingPerfNames.ContainsKey($perfItem.Name) -and -not $seenPerf.ContainsKey($perfItem.Name)) { $perfNames += $perfItem.Name; $seenPerf[$perfItem.Name] = $true } } if ($actNames.Count -eq 0 -and $perfNames.Count -eq 0) { continue } $splat = @{ displayName = $displayName DeviceAddress = $addIP Hostname = $dev.Name Brand = 'Docker' Note = "Docker host (auto-created by discovery)" NoDefaultActiveMonitor = $true } if ($devAttrs.Count -gt 0) { $splat['Attributes'] = $devAttrs } if ($dockerCredId) { $splat['CredentialRestApi'] = $credName } if ($actNames.Count -gt 0) { $splat['ActiveMonitors'] = $actNames } if ($perfNames.Count -gt 0) { $splat['PerformanceMonitors'] = $perfNames } try { $devResult = Add-WUGDeviceTemplate @splat $isErrorResult = $false if ($devResult -is [array]) { $isErrorResult = $true } elseif ($devResult -and $devResult.PSObject.Properties['messages']) { $isErrorResult = $true } if ($devResult -and -not $isErrorResult) { $newDeviceId = $null if ($devResult.idMap) { $newDeviceId = ($devResult.idMap | Select-Object -First 1).resultId } elseif ($devResult.PSObject.Properties['resultId']) { $newDeviceId = $devResult.resultId } if ($newDeviceId) { $wugDeviceMap[$key] = $newDeviceId $stats.DevicesCreated++ Write-Verbose "Created device '$displayName' (ID: $newDeviceId)" } else { Write-Warning "Device '$displayName' - API returned success but no device ID." } } else { $errMsgs = @() if ($devResult -is [array]) { foreach ($e in $devResult) { if ($e.PSObject.Properties['messages']) { $errMsgs += ($e.messages -join '; ') } } } elseif ($devResult -and $devResult.PSObject.Properties['messages']) { $errMsgs += ($devResult.messages -join '; ') } $errText = if ($errMsgs.Count -gt 0) { $errMsgs -join ' | ' } else { 'Unknown error (no details returned)' } Write-Warning "Failed to create device '$displayName': $errText" } } catch { Write-Warning "Error creating device '$displayName': $_" } } Write-Host " Devices created: $($stats.DevicesCreated)" -ForegroundColor Green } # ---- 2e. Update existing devices --- if ($existingDevices.Count -gt 0) { Write-Host " Updating $($existingDevices.Count) existing devices..." -ForegroundColor Cyan foreach ($key in $existingDevices.Keys) { $deviceId = [int]$existingDevices[$key] $dev = $devicePlan[$key] if ($dockerCredId) { try { $null = Set-WUGDeviceCredential -DeviceId $deviceId -CredentialId $dockerCredId -Assign; $stats.CredsAssigned++ } catch { } } $actMonitorIds = @() foreach ($actItem in @($dev.Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' })) { if ($actItem.Name -and $existingActiveNames.ContainsKey($actItem.Name)) { $actMonitorIds += $existingActiveNames[$actItem.Name] } } if ($actMonitorIds.Count -gt 0) { try { Add-WUGActiveMonitorToDevice -DeviceId $deviceId -MonitorId $actMonitorIds -ErrorAction Stop } catch { } } $perfMonitorIds = @() foreach ($perfItem in @($dev.Items | Where-Object { $_.ItemType -eq 'PerformanceMonitor' })) { if ($perfItem.Name -and $existingPerfNames.ContainsKey($perfItem.Name)) { $perfMonitorIds += [int]$existingPerfNames[$perfItem.Name] } } if ($perfMonitorIds.Count -gt 0) { try { Add-WUGPerformanceMonitorToDevice -DeviceId $deviceId -MonitorId $perfMonitorIds -PollingIntervalMinutes 5 -ErrorAction Stop } catch { } } } } # ---- Summary --- Write-Host "" Write-Host "Push complete!" -ForegroundColor Green Write-Host " Active monitors: $($stats.HealthCreated) created, $($stats.HealthSkipped) existing, $($stats.HealthFailed) failed" -ForegroundColor White Write-Host " Perf monitors: $($stats.PerfCreated) created, $($stats.PerfSkipped) existing, $($stats.PerfFailed) failed" -ForegroundColor White Write-Host " Devices: $($stats.DevicesCreated) created, $($stats.DevicesFound) existing" -ForegroundColor White Write-Host " Creds assigned: $($stats.CredsAssigned)" -ForegroundColor White } '2' { $jsonPath = Join-Path $OutputDir 'docker-discovery-plan.json' $plan | Export-DiscoveryPlan -Format JSON -Path $jsonPath -IncludeParams Write-Host "Exported to: $jsonPath" -ForegroundColor Green } '3' { $csvPath = Join-Path $OutputDir 'docker-discovery-plan.csv' $plan | Export-DiscoveryPlan -Format CSV -Path $csvPath Write-Host "Exported to: $csvPath" -ForegroundColor Green } '4' { $plan | Export-DiscoveryPlan -Format Table } '5' { # ---------------------------------------------------------------- # Generate Docker HTML Dashboard from discovery data # ---------------------------------------------------------------- Write-Host "" Write-Host "Building Docker dashboard from discovery data..." -ForegroundColor Cyan $dashboardRows = @() foreach ($key in $devicePlan.Keys) { $dev = $devicePlan[$key] $activeItems = @($dev.Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' }) $perfItems = @($dev.Items | Where-Object { $_.ItemType -eq 'PerformanceMonitor' }) foreach ($item in ($activeItems + $perfItems)) { $dashboardRows += [PSCustomObject]@{ Device = $dev.Name IP = if ($dev.IP) { $dev.IP } else { '(docker)' } Monitor = $item.Name -replace '\s*\[.*\]$', '' Type = $item.ItemType Status = 'Discovered' LastDiscovery = (Get-Date).ToString('yyyy-MM-dd HH:mm') } } } if ($dashboardRows.Count -eq 0) { Write-Warning "No data to generate dashboard." } else { $dashPath = Join-Path $OutputDir 'Docker-Dashboard.html' if (Get-Command -Name 'Export-DynamicDashboardHtml' -ErrorAction SilentlyContinue) { Export-DynamicDashboardHtml -Data $dashboardRows ` -OutputPath $dashPath ` -ReportTitle 'Docker Discovery Dashboard' ` -CardField 'Device','Type' ` -StatusField 'Status' } else { Write-Warning "No dashboard function available. Exporting as JSON instead." $jsonPath = Join-Path $OutputDir "Docker-Plan-$(Get-Date -Format yyyyMMdd-HHmmss).json" $plan | Export-DiscoveryPlan -Format JSON -Path $jsonPath Write-Host "JSON exported: $jsonPath" -ForegroundColor Green break } Write-Host "" Write-Host "Dashboard generated: $dashPath" -ForegroundColor Green Write-Host " Devices: $($devicePlan.Count) | Monitors: $($dashboardRows.Count)" -ForegroundColor White $nmConsolePaths = @( "${env:ProgramFiles(x86)}\Ipswitch\WhatsUp\Html\NmConsole" "${env:ProgramFiles}\Ipswitch\WhatsUp\Html\NmConsole" ) $nmConsolePath = $nmConsolePaths | Where-Object { Test-Path $_ } | Select-Object -First 1 if ($nmConsolePath) { $wugDashDir = Join-Path $nmConsolePath 'dashboards' if (-not (Test-Path $wugDashDir)) { New-Item -ItemType Directory -Path $wugDashDir -Force | Out-Null } $wugDashPath = Join-Path $wugDashDir 'Docker-Dashboard.html' try { Copy-Item -Path $dashTempPath -Destination $wugDashPath -Force Write-Host "Copied to WUG: $wugDashPath" -ForegroundColor Green Write-Host " Access via WUG web UI: /NmConsole/dashboards/Docker-Dashboard.html" -ForegroundColor Cyan } catch { Write-Warning "Could not copy to NmConsole (run as admin?): $_" } Deploy-DashboardWebConfig -Path $wugDashDir } } } '6' { Write-Host "No action taken." -ForegroundColor Gray } default { Write-Host "Invalid choice." -ForegroundColor Red } } } # end foreach actionsToRun Write-Host "" Write-Host "Re-run anytime to discover new Docker containers." -ForegroundColor Cyan # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBsgzixoqY3FXsH # wSPSspTvHHnhGSXbNACXO66/JplW3qCCJQ0wggVvMIIEV6ADAgECAhBI/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 # BCBoQq2kS0lP0pCHkmrk5mqIBvm/O5CXwbjktzpPLC1VUzANBgkqhkiG9w0BAQEF # AASCAgBD8jEYdkkql/t/YM+uWuPhj/Jl+5YjugmzUEhwldBEp8BFKMgDdP3/xCBl # jye/unjjeK6FAC1lR73CvSpcYurEOxTXosvY5/v6HQxciXtqRhp2K9rC5ZE+MDVM # uZorYktOSfSJv8g3Ge+ofrytDN2bmRWMPu5JS6CpwObxCpV5rgZdvfiLxLzp9/sY # c7BSTlGMtgrLpPfXTZYdhUeTqevjE8JmenHDHmyU4OQM4W4JQGo61KgjEuQEt9mT # enEfYfhtp3ZpHJppgO7xVA3sgdQqRrsvGFWRFL/zwqNgm670CExY1xsvP9WVxaHL # SaiABRuZj5CFdMaBbK2/HYdgRIuu8T+pfF+YfP0xZLPcazZImAMuaXWd7L/JkPNI # epW/siEWRkbTrAR58spToF1HY+0wqaYgJaSDyufwp/PAuL8tPUvLfEsA/4Hz1T/A # PNOCF6Cfuq4bM6mYTN/KGLOjClJKE1w1jdPD0yI+3rC3ZtXHCpMgAJe5C9LgOwv2 # tY2S2jRJx9WX3aQgLvXKZH6bnsmlBY0ulUVv7vj6V/F0f/8D2Vd7i45v7eSaIkRx # C2O4B/YX4LrRg6uvu4WzQ3BQs+L9Fl4DLjayLC2O9HR/rmtlx5TmvBIngXcX1uiX # gMZ26HyZL/oyvzXXYHNJe+ENbe+mOII52N7ZMnxUI8HJITz8oqGCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV # dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA1MjgyMDA4MTBaMC8GCSqGSIb3DQEJBDEiBCD7IWjv # qM4wCkNEwRbZIxRACf7tQnuK0zNk6vqcKRqhYDANBgkqhkiG9w0BAQEFAASCAgCF # 0fmOgW2eULZK5rEVCoJrXU98sEINCgmpiNRBcXt48bj0SL3uoUGQaW0XkR2ufFdt # 3euW4kkpUaCbEhaNe8rfQ3vwFLitP9XJvjGq4AKnvcvGLBxOKS9u1CziKXHXbrL0 # C7nJPF9JMO464oiZg6dSNE8bwdcGGMbgmRLJJC2PDBNy99kgse/cO03r7Z2SrFrj # phmBvAKvyKWzURGC0tYEQtOlUOLMK6/19q1dt9MyfyK8SpH7vifgqfF2SZL0Nhuj # pz/fPB+z3NLWE/QOpnF5Qu1fneUKAtmSM/Yu1vmTtoC7id0urbgz+fJDQSeru5Ss # GqZbgUZxI5u4VFg2cojG6rhhOfraE6aqJTA2c4hWZb3pgYqWQ9OWdNCKcO2xTl9l # 10/waGQyy2JAToxjIHoLDanslefDSGKzGmR/7EfpeEraBKNfTZXsxh4uvQcJOzzt # cakRTbYglcyPKEr7W8TBHhwQ4Zl5Mat2JTTnkWZmAST/KunlS0KxDLmW6s9Jm0nc # mlUya9GN5ch9Fvbf8cp7BovDhVRc1LaiGXekUoO3EcHabIRqus58m/6gBPtTuRou # kD04YG+qsmE0Pm+slbdSI0apiTAyWViv0OL4PDpI0BQIW4Owgq4dOgNZ0zzX9Ygn # gEq1QU7SJ5WZ1T9rxXxYcKEYDxtXwc/WhjA7+I2kfQ== # SIG # End signature block |