helpers/discovery/Setup-OCI-Discovery.ps1
|
<#
.SYNOPSIS OCI Discovery -- Discover Oracle Cloud compute, databases, and load balancers, then optionally push to WhatsUp Gold. .DESCRIPTION Interactive script that discovers Oracle Cloud Infrastructure resources via the OCI.PSModules, then lets you choose what to do with the results: [1] Push devices to WhatsUp Gold (Ping monitors for public IPs) [2] Export discovery plan to JSON [3] Export discovery plan to CSV [4] Show full plan table in console [5] Generate HTML dashboard with metric snapshots [6] Exit [7] Dashboard + Push to WUG Monitor Strategy (Option B -- built-in WUG monitors): OCI REST APIs require HTTP Signature authentication which WUG cannot perform natively. Instead, this script uses built-in WUG Ping monitors for resources with public IPs. Resources without public IPs are included as inventory items in the dashboard only. Architecture (when pushed to WUG): [OCI Compute device in WUG] '-- Ping monitor (default, auto-added by WUG) [OCI Load Balancer device in WUG] '-- Ping monitor (default, auto-added by WUG) Resources without public IPs: '-- Dashboard inventory only (no WUG device created) OCI Monitoring metrics (CPU, memory, disk, network) are queried via OCI.PSModules during discovery and stored as device attributes for dashboard display. The OCI CLI is optional for metrics enrichment. First Run: 1. Prompts for OCI config file path and profile name 2. Prompts for tenancy OCID 3. Stores config path in DPAPI vault (encrypted) 4. Validates OCI connectivity and discovers resources 5. Shows summary, then asks what to do Subsequent Runs: Loads OCI config path from vault and reconnects. Prerequisites: 1. OCI.PSModules installed (Install-Module OCI.PSModules) 2. OCI config file (~/.oci/config) with valid API key 3. Appropriate IAM policies for read access .PARAMETER TenancyId OCI tenancy OCID. Required for non-interactive mode. .PARAMETER ConfigFile Path to OCI config file. Default: ~/.oci/config. .PARAMETER Profile OCI config profile name. Default: DEFAULT. .PARAMETER Region Override OCI region for discovery. .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 cached vault credentials and parameter defaults. .NOTES OCI.PSModules handles API request signing via config file for discovery. WUG monitors use built-in Ping (no cloud auth needed). OCI metrics are captured as point-in-time snapshots during discovery and displayed in the dashboard. .EXAMPLE .\Setup-OCI-Discovery.ps1 # Interactive mode. .EXAMPLE .\Setup-OCI-Discovery.ps1 -TenancyId 'ocid1.tenancy.oc1..aaaaaa' -Action PushToWUG -NonInteractive # Scheduled mode. .EXAMPLE .\Setup-OCI-Discovery.ps1 -TenancyId 'ocid1.tenancy.oc1..aaa' -Region 'us-ashburn-1' -Action ExportJSON # Discover in specific region and export. #> [CmdletBinding()] param( [Alias('Target')] [string]$TenancyId, [string]$ConfigFile, [string]$Profile = 'DEFAULT', [string]$Region, [ValidateSet('PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'None')] [string]$Action, [string]$WUGServer = '192.168.74.74', [PSCredential]$WUGCredential, [string]$OutputPath, # --- Opt-in cost, quota and waste analysis ------------------------------- [switch]$IncludeCostAndQuota, [hashtable]$UnitPrice, [string]$CostReportPath, [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 # --- Load helpers ------------------------------------------------------------- $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent . (Join-Path $scriptDir 'DiscoveryHelpers.ps1') . (Join-Path $scriptDir 'DiscoveryProvider-OCI.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 "=== OCI Discovery ===" -ForegroundColor Cyan Write-Host "" # Resolve config file if (-not $ConfigFile) { $defaultConfig = Join-Path $env:USERPROFILE '.oci\config' if ($NonInteractive) { $ConfigFile = $defaultConfig } else { $cfgInput = Read-Host -Prompt "OCI config file [default: $defaultConfig]" $ConfigFile = if ([string]::IsNullOrWhiteSpace($cfgInput)) { $defaultConfig } else { $cfgInput.Trim() } } } if (-not (Test-Path $ConfigFile)) { Write-Error "OCI config file not found: $ConfigFile" return } # Resolve profile if (-not $PSBoundParameters.ContainsKey('Profile') -and -not $NonInteractive) { $profInput = Read-Host -Prompt "OCI profile [default: $Profile]" if (-not [string]::IsNullOrWhiteSpace($profInput)) { $Profile = $profInput.Trim() } } # Resolve tenancy ID if (-not $TenancyId) { if ($NonInteractive) { # Try to extract from config file try { $configContent = Get-Content $ConfigFile -Raw if ($configContent -match 'tenancy\s*=\s*(ocid1\.tenancy\.[^\s]+)') { $TenancyId = $Matches[1] } } catch { } if (-not $TenancyId) { Write-Error 'No tenancy ID specified. Pass -TenancyId parameter.'; return } } else { # Try to extract default from config $defaultTenancy = $null try { $configContent = Get-Content $ConfigFile -Raw if ($configContent -match 'tenancy\s*=\s*(ocid1\.tenancy\.[^\s]+)') { $defaultTenancy = $Matches[1] } } catch { } $tenancyPrompt = if ($defaultTenancy) { "Tenancy OCID [default: $defaultTenancy]" } else { "Tenancy OCID" } $tenancyInput = Read-Host -Prompt $tenancyPrompt $TenancyId = if ([string]::IsNullOrWhiteSpace($tenancyInput) -and $defaultTenancy) { $defaultTenancy } else { $tenancyInput.Trim() } } } if (-not $TenancyId) { Write-Error 'No tenancy OCID provided.'; return } Write-Host "Tenancy: $TenancyId" -ForegroundColor Cyan Write-Host "Config: $ConfigFile (profile: $Profile)" -ForegroundColor Cyan if ($Region) { Write-Host "Region: $Region" -ForegroundColor Cyan } Write-Host "" # ============================================================================== # STEP 2: Authentication (OCI config file) # ============================================================================== Write-Host "Validating OCI connectivity..." -ForegroundColor Cyan try { $connectSplat = @{} if ($ConfigFile) { $connectSplat['ConfigFile'] = $ConfigFile } if ($Profile -and $Profile -ne 'DEFAULT') { $connectSplat['Profile'] = $Profile } Connect-OCIProfile @connectSplat Write-Host "OCI authentication successful." -ForegroundColor Green } catch { Write-Error "OCI authentication failed: $_" return } Write-Host "" # ============================================================================== # STEP 3: Discover -- query OCI APIs # ============================================================================== Write-Host "Querying OCI tenancy..." -ForegroundColor Cyan $ociCredential = @{ ConfigFile = $ConfigFile Profile = $Profile TenancyId = $TenancyId } if ($Region) { $ociCredential['Region'] = $Region } $plan = Invoke-Discovery -ProviderName 'OCI' ` -Target $TenancyId ` -Credential $ociCredential if (-not $plan -or $plan.Count -eq 0) { Write-Warning "No cloud resources discovered in tenancy." $plan = @() } # ============================================================================== # STEP 3b: Cost, quota and waste analysis (opt-in) # ============================================================================== $costFindings = @() if ($IncludeCostAndQuota) { $costHelpers = Join-Path $scriptDir 'CloudCostHelpers.ps1' if (-not (Test-Path $costHelpers)) { Write-Warning "CloudCostHelpers.ps1 not found; skipping cost and quota analysis." } else { . $costHelpers Write-Host "" Write-Host "Analyzing OCI cost, quota and waste..." -ForegroundColor Cyan $compartments = @($plan | ForEach-Object { $_.Attributes['OCI.CompartmentId'] } | Where-Object { $_ } | Sort-Object -Unique) if ($compartments.Count -eq 0 -and $TenancyId) { $compartments = @($TenancyId) } foreach ($compartmentId in $compartments) { Write-Host " Compartment $compartmentId..." -ForegroundColor DarkGray try { $costSplat = @{ CompartmentId = $compartmentId; CompartmentName = $compartmentId; Region = $Region } if ($UnitPrice) { $costSplat['UnitPrice'] = $UnitPrice } $costFindings += @(Get-OCICostQuotaFinding @costSplat) } catch { Write-Warning " Cost analysis failed for ${compartmentId}: $($_.Exception.Message)" } } if ($costFindings.Count -gt 0) { Get-CloudCostSummary -Finding $costFindings | Format-Table -AutoSize | Out-String -Width 160 | Write-Host $costFindings | Where-Object { $_.Status -eq 'Fail' -or $_.Status -eq 'Warn' } | Select-Object Category, Check, Status, Value, Resource | Format-Table -AutoSize | Out-String -Width 160 | Write-Host if (-not $CostReportPath) { $CostReportPath = Join-Path $OutputDir 'OCI-Cost-Quota.html' } try { Export-CloudCostDashboard -Finding $costFindings -OutputPath $CostReportPath -ReportTitle 'OCI Cost and Quota' | Out-Null Write-Host " Cost report: $CostReportPath" -ForegroundColor Green } catch { Write-Warning " Could not render cost dashboard: $($_.Exception.Message)" } } else { Write-Host " No cost or quota findings returned." -ForegroundColor DarkGray } } } # ============================================================================== # STEP 4: Show the plan # ============================================================================== $devicePlan = [ordered]@{} foreach ($item in $plan) { $devType = $item.Attributes['OCI.DeviceType'] $name = $item.Attributes['ComputedDisplayName'] $ip = $item.Attributes['OCI.IPAddress'] switch ($devType) { 'Compute' { $key = "compute:$($item.Attributes['OCI.InstanceId'])" } 'DBSystem' { $key = "db:$($item.Attributes['OCI.DBSystemId'])" } 'AutonomousDB' { $key = "adb:$($item.Attributes['OCI.AutonomousDbId'])" } 'LoadBalancer' { $key = "lb:$($item.Attributes['OCI.LoadBalancerId'])" } default { continue } } if (-not $devicePlan.Contains($key)) { $devicePlan[$key] = @{ Name = $name IP = $ip Type = $devType Attrs = $item.Attributes Items = [System.Collections.ArrayList]@() } } [void]$devicePlan[$key].Items.Add($item) } $computeDevices = @($devicePlan.Values | Where-Object { $_.Type -eq 'Compute' }) $dbDevices = @($devicePlan.Values | Where-Object { $_.Type -eq 'DBSystem' }) $adbDevices = @($devicePlan.Values | Where-Object { $_.Type -eq 'AutonomousDB' }) $lbDevices = @($devicePlan.Values | Where-Object { $_.Type -eq 'LoadBalancer' }) # Separate monitorable items from inventory-only $monitorableItems = @($plan | Where-Object { -not $_.MonitorParams._InventoryOnly }) $inventoryItems = @($plan | Where-Object { $_.MonitorParams._InventoryOnly }) $activeTemplates = @($monitorableItems | Where-Object { $_.ItemType -eq 'ActiveMonitor' } | Select-Object -ExpandProperty Name -Unique) Write-Host "" Write-Host "Discovery complete!" -ForegroundColor Green Write-Host " Compute Instances: $($computeDevices.Count)" -ForegroundColor White Write-Host " DB Systems: $($dbDevices.Count)" -ForegroundColor White Write-Host " Autonomous Databases: $($adbDevices.Count)" -ForegroundColor White Write-Host " Load Balancers: $($lbDevices.Count)" -ForegroundColor White Write-Host " Monitorable items: $($monitorableItems.Count) (Ping/TCP)" -ForegroundColor White Write-Host " Inventory-only items: $($inventoryItems.Count) (no public IP)" -ForegroundColor White Write-Host " Total plan items: $($plan.Count)" -ForegroundColor White Write-Host "" if ($activeTemplates.Count -gt 0) { Write-Host "Monitor templates:" -ForegroundColor Cyan foreach ($t in $activeTemplates) { Write-Host " [Active] $t" -ForegroundColor White } Write-Host "" } $devicePlan.Values | Sort-Object @{E={$_.Type}}, @{E={$_.Name}} | ForEach-Object { $monCount = @($_.Items | Where-Object { -not $_.MonitorParams._InventoryOnly }).Count $invCount = @($_.Items | Where-Object { $_.MonitorParams._InventoryOnly }).Count [PSCustomObject]@{ Device = $_.Name Type = $_.Type IP = if ($_.IP) { $_.IP } else { '(no public IP)' } Monitors = $monCount Inventory = if ($invCount -gt 0) { 'yes' } else { '' } } } | 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 (Ping/TCP monitors -- no REST API credential needed) --- 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 = @{ DevicesCreated = 0; DevicesFound = 0; DevicesSkipped = 0; MonitorsAssigned = 0 } $wugDeviceMap = @{} $deviceKeys = @($devicePlan.Keys | Sort-Object) # Filter to devices that have at least one monitorable item (has public IP) $monitorableDeviceKeys = @($deviceKeys | Where-Object { $dev = $devicePlan[$_] @($dev.Items | Where-Object { -not $_.MonitorParams._InventoryOnly }).Count -gt 0 }) $inventoryOnlyDeviceKeys = @($deviceKeys | Where-Object { $_ -notin $monitorableDeviceKeys }) Write-Host "" Write-Host " Devices with public IPs (will create in WUG): $($monitorableDeviceKeys.Count)" -ForegroundColor Cyan Write-Host " Devices without public IPs (inventory only): $($inventoryOnlyDeviceKeys.Count)" -ForegroundColor DarkGray # ---- 1. Check existing vs new devices --- Write-Host " Checking for existing devices..." -ForegroundColor Cyan $existingDevices = @{} $newDeviceKeys = [System.Collections.Generic.List[string]]::new() foreach ($key in $monitorableDeviceKeys) { $dev = $devicePlan[$key] $displayName = "$($dev.Name) (OCI)" $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 } | 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 # ---- 2. Create new devices with Ping active monitor (built-in) --- if ($newDeviceKeys.Count -gt 0) { Write-Host " Creating $($newDeviceKeys.Count) devices..." -ForegroundColor Yellow $devIdx = 0 foreach ($key in $newDeviceKeys) { $devIdx++ $dev = $devicePlan[$key] $addIP = if ($dev.IP) { $dev.IP } else { '0.0.0.0' } $displayName = "$($dev.Name) (OCI)" Write-Progress -Activity 'Creating OCI devices' ` -Status "$devIdx / $($newDeviceKeys.Count) - $displayName" ` -PercentComplete ([Math]::Round(($devIdx / $newDeviceKeys.Count) * 100)) $devAttrs = @() foreach ($attrName in $dev.Attrs.Keys) { if ($dev.Attrs[$attrName]) { $devAttrs += @{ name = $attrName; value = "$($dev.Attrs[$attrName])" } } } # Ping is the default active monitor in WUG -- just create the device # with its IP address and WUG adds Ping automatically $splat = @{ displayName = $displayName DeviceAddress = $addIP Hostname = $dev.Name Brand = 'Oracle Cloud' Note = "OCI $($dev.Type) - discovered $(Get-Date -Format 'yyyy-MM-dd')" } if ($devAttrs.Count -gt 0) { $splat['Attributes'] = $devAttrs } 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-Progress -Activity 'Creating OCI devices' -Completed Write-Host " Devices created: $($stats.DevicesCreated)" -ForegroundColor Green } # ---- 3. Update attributes on existing devices --- if ($existingDevices.Count -gt 0) { Write-Host " Updating $($existingDevices.Count) existing device attributes..." -ForegroundColor Cyan foreach ($key in $existingDevices.Keys) { $dev = $devicePlan[$key] $devAttrs = @() foreach ($attrName in $dev.Attrs.Keys) { if ($dev.Attrs[$attrName]) { $devAttrs += @{ name = $attrName; value = "$($dev.Attrs[$attrName])" } } } if ($devAttrs.Count -gt 0) { try { Set-WUGDeviceAttribute -DeviceId ([int]$existingDevices[$key]) -Attributes $devAttrs -ErrorAction Stop } catch { Write-Verbose "Could not update attributes for $($dev.Name): $_" } } } } # ---- Summary --- Write-Host "" Write-Host "Push complete!" -ForegroundColor Green Write-Host " Devices: $($stats.DevicesCreated) created, $($stats.DevicesFound) existing" -ForegroundColor White Write-Host " Inventory-only: $($inventoryOnlyDeviceKeys.Count) (no public IP, skipped)" -ForegroundColor White Write-Host "" Write-Host "WUG monitors: Ping (default) monitors network reachability." -ForegroundColor Cyan Write-Host "OCI metric snapshots are stored as device attributes for dashboard." -ForegroundColor Cyan } '2' { $jsonPath = Join-Path $OutputDir 'oci-discovery-plan.json' $plan | Export-DiscoveryPlan -Format JSON -Path $jsonPath -IncludeParams Write-Host "Exported to: $jsonPath" -ForegroundColor Green } '3' { $csvPath = Join-Path $OutputDir 'oci-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 OCI HTML Dashboard from discovery data # ---------------------------------------------------------------- Write-Host "" Write-Host "Building OCI dashboard from discovery data..." -ForegroundColor Cyan $dashboardRows = @() foreach ($key in $devicePlan.Keys) { $dev = $devicePlan[$key] $attrs = $dev.Attrs $row = [ordered]@{ Device = $dev.Name Type = $dev.Type State = if ($attrs['OCI.LifecycleState']) { $attrs['OCI.LifecycleState'] } else { 'Unknown' } IP = if ($dev.IP) { $dev.IP } else { '(no public IP)' } Shape = if ($attrs['OCI.Shape']) { $attrs['OCI.Shape'] } else { 'N/A' } Region = if ($attrs['OCI.Region']) { $attrs['OCI.Region'] } else { 'default' } AD = if ($attrs['OCI.AD']) { $attrs['OCI.AD'] } else { 'N/A' } Created = if ($attrs['OCI.TimeCreated']) { $attrs['OCI.TimeCreated'] } else { 'N/A' } Monitors = @($dev.Items | Where-Object { -not $_.MonitorParams._InventoryOnly }).Count } # Add metric snapshots if available foreach ($attrKey in @($attrs.Keys | Sort-Object)) { if ($attrKey -match '^OCI\.Metric\.(.+)$') { $metricName = $Matches[1] $row[$metricName] = $attrs[$attrKey] } } $dashboardRows += [PSCustomObject]$row } if ($dashboardRows.Count -eq 0) { # Generate a summary dashboard even with 0 resources $configRegion = 'default' try { $cfgContent = Get-Content $ConfigFile -Raw if ($cfgContent -match 'region\s*=\s*(\S+)') { $configRegion = $Matches[1] } } catch { } $dashboardRows = @( [PSCustomObject][ordered]@{ Device = "(tenancy scan)" Type = 'Summary' State = 'Scanned' IP = 'N/A' Shape = 'N/A' Region = $configRegion AD = 'N/A' Created = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') Monitors = 0 Compute = "$($computeDevices.Count) instances" DBSystem = "$($dbDevices.Count) DB systems" ADB = "$($adbDevices.Count) autonomous DBs" LB = "$($lbDevices.Count) load balancers" Tenancy = $TenancyId } ) Write-Host " No cloud resources found -- generating summary dashboard." -ForegroundColor Yellow } $dashPath = Join-Path $OutputDir 'OCI-Dashboard.html' if (Get-Command -Name 'Export-DynamicDashboardHtml' -ErrorAction SilentlyContinue) { Export-DynamicDashboardHtml -Data $dashboardRows ` -OutputPath $dashPath ` -ReportTitle 'OCI Discovery Dashboard' ` -CardField 'Type','State' ` -StatusField 'State' } else { Write-Warning "No dashboard function available. Exporting as JSON instead." $jsonPath = Join-Path $OutputDir "OCI-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 'OCI-Dashboard.html' try { Copy-Item -Path $dashPath -Destination $wugDashPath -Force Write-Host "Copied to WUG: $wugDashPath" -ForegroundColor Green Write-Host " Access via WUG web UI: /NmConsole/dashboards/OCI-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 OCI resources." -ForegroundColor Cyan # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCMAxDFgs6NxIX9 # oOYPdRy1C9l0fD09d58313l1p5ZNfaCCJQ0wggVvMIIEV6ADAgECAhBI/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 # kPZPubdcMIIG7TCCBNWgAwIBAgIQCE/cM09+RU7bww+P+ZIYNTANBgkqhkiG9w0B # AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExMB4XDTI2MDgwNTAwMDAwMFoXDTM3MTEwNDIzNTk1OVow # YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD # EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw # MjYgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALZ7pvLJ/s1K+NSb # TGWz/TjGMPh8CQ6RucZCLv5anHzWJjF/NWJrFIhy24fcpKXlgRiky4WAawDfU3YP # 0BMxt9l3Dm5oCG5Z69AqEN1kgHg2epx+l+lZBcmJCcN0ASURML5uFIS80sZsDwO3 # BSkUxDjLJhBI+qiZP3aixAC/qEGLjsBNlLol9VZ7pfGEXiMlneJIC5/YKuizVzNF # KZZEeoy/0B8Zm+nzKBgSWG52lCO1w+nCg6XpCtklTJXeIg283hw7TmmsZXR+SMbj # brEOvZ3fP2VxIgeR28Y90ZStd3F9VuA5RVynb/whITPAo9b75Zr4Ta6Mj3URm26Q # ZYMn/FnbuTegcoRcFEZ9FOqM5T6MTdtr/n74lIT/ug0eeOzmZ6QTFg33otX+bFRs # IolvykE1jive4PuESaT8zzVeFWDAMDtozNgLctkGD1ZjkEyZtJrLl5ya0m5doH/S # cpaZCZVl6pNUOCybMc/kxC6EAmSJY24L0yYKD1Nkddsnb/ItVKi/2nXpQNMu1PT5 # prW83vV8d67WowuUs0HdY4H8AMLGvdL/WHEj3ZnqMqAQQP9u3Ai9t+5eQ02GDwy0 # ODjdzi0xlp70W+ow63/0++YDEX1M0iwgUHwbrJvfpklkZQvw3+kv3vUPItdwrocz # k9icflf55W1zOEKAcJVAIXpcMCU9AgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA # MB0GA1UdDgQWBBQUyWOKMC7USvtulPPm40B+9ezN4jAfBgNVHSMEGDAWgBTvb1NK # 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI # KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT # SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k # aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2 # U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9 # bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQCNxTphHp1SCt+ZrAmAfn0oQLFr0mLywSLa # DXQIENoyKqxrFbJblzCVP/pkXmwXOdrOpWygLzlT12os5ipDCy35RBCg2UMeApEt # rfGhz45F4Wt4WGdNdIbRWt3YTYJmpR+b7lr4d7Uwn+H600u4D7RnOGf8Wj4UNgAd # ZkfHhHv1mx9EVh71SJelcEN/oORSjXzdjfw1iZH9d8Nh/thn6hH23d+VsPAr6GAY # yzSA02nXD1nYLI7Ijmiv+xLCiYC41DSFYL3GhTiy0PxpawPtGRyaBVGzq+UiTfM8 # pD7KVyF5aQyWP4KhVGUUTnmm/RlYJoW3TiXA/+t0YcT2oRVBm3JETjajHug2AL+v # 5jhtKVnd3D0rbHXEu27o+Q8p4sEWPMqKDB+qbceb6T/6WcwTwXmQ9lOCLLYcsQeS # WmvKqzpAec9etE14jOQAzLKWdE3w/TCaKtLRaRT7LCkRYVnhA2D73FLje1O5b3HR # 5eHs0NzU/+xX7NbEdcofy0W3Wdwd1XOqtlpg/JgwtKfZM5dqO94lbUveOiJBI+xZ # EbGRsMNbXmMREUTgu+Oca7Y73MPWcslIx2VhkSKSXjDbD6rgg39H5Mh7QfieAIjW # agkJNt68Yfim6cjEzVSiLSeZfdkr5dtFPTW6jATlWJdYeeDRGCyatf8R1hSjzSvd # N8yWQPT9gzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl # Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu # aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY # BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3 # AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi # BCAKCbXVviuKV2q30KCPro6/QG7QmzICHSeWjR2WOqz6SDANBgkqhkiG9w0BAQEF # AASCAgAICleU8GKXU9hPmDENYQK6Bdqlko7DJjuc88KTxCKryHdV+aSsCBSSu//h # xHE3giBuwHAOGt4zlved3X0uMDeLi/PX3hS0b4IHgBTW3Gu37JEB6XIQdngbMz6j # tvObghNxD0HBEQ7JbHb/vyFaPU4cwMwiMEa0Adtfl9mbQpYqqCcNSDAoBoHaeTm6 # WM2doZgm3vIzia/K2M56bibC1gD+9eSavmJPGANxop0PB8ZOTVtOXV2Fkc2N+Vor # TeEDJuYdv2wXnohC90jqX33CQRaYxedHGg5NXgKvE2AhyBA2K7YOaU19pefACSri # hA4PJ78mZmGfSwzs/gG1IDLJ1VrwExwzqqjkcmaPAP+q3j8e04AUReO3HUbTBMC8 # JqM51j8iPJc+M6ecZvY5lfRe59V7uBdmn8cVKdu6pUir6tW5MuBsTZUPSdjYsymD # tcatb9pB1E7o+66nIVzeJKJ7lpYG2BdGf+y+NFVQhlBqxCLnOzZcnjiGfwmAaJ4u # qrmByggt8zbsldlaAs1wzyTnWjpWOReU4vU48CpT/pTDYBaJkpx5DJU1XONnVRbi # nUb1IqQyS36YqIdEDgNL5/YUb5KD3sUVitetdQQHwr4cmlovkhKiLxBjhO+gGVW4 # 3n3YoZZ/g413KY/c67yVzF92qJNCg4W75Szu5sl2lTK7Rd/X5aGCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAhP3DNPfkVO28MPj/mS # GDUwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA5MjkxOTUxMDJaMC8GCSqGSIb3DQEJBDEiBCCm9EpF # vq8/aN5+iPOfsfNXHstH1e3inJYCrME7te2b/zANBgkqhkiG9w0BAQEFAASCAgCn # iqCycpP+a4EtjcqRZAP2M1Tcrg/NYKXsUZrgajmSfoyHBCB9WVScNOQ/C0StKuDL # b9AP6ybsTpwc3Z0hUgZjPXgrJB7B6ZQhrRkCbSG4I7gKdgTi8Hm0YzVcBXIji5Lo # 9AalWPIIciwK98ePPeBlEnBUGqyKsbji8PpUmd8EgyWU1hXtnpKERjr6Z/VItIau # qsCK4h+ly2DegH0xrVANhCyIWzZSQplO9CQX7p6LFwY1BYbCwni1b0FGf/htjEgO # ET2BygnoWFUqGpYO+Fxj6Au7rWFIyIBVhv7kKa5vY9NGTIkqtMUJlzPO0h8WiCNy # SdFiI74GH6QUALNI405BXWBttMyxn4V9PXQ/SVYgTDvABbTIcKp2LUE8QtPS+Iuu # fANNbFDuXW5T98b/z7vO0UJKa5bXz9GZxeepsyEfeWUcu9u3FPmkCPS7OZiIWIjJ # KleTiu2pVAFWzbpV+NiSEkd7mFsp2sEPNxXT23/l+KCdmizkFG0kBvj4X087mlbl # QnkoI8FchdgWACpTcWvQV/G2wRrrj8pZNYD/U8/BtvejNxSIfcRNH7Cm0WTLC/dr # Hg/LwwO8Qmbn2Ei384AEMpAq3Rb8ovNqP5174nrVZMS9X5ZXmIX3pN/BsgccW8UL # SyE3la0kiWd/sJ9e3O2gPPMUQt7A0+qSAd3LRGMa7A== # SIG # End signature block |