helpers/discovery/Setup-CiscoWLC-Discovery.ps1
|
#requires -Version 5.1 <# .SYNOPSIS Discover Cisco WLC wireless infrastructure via SNMP and optionally push monitors to WUG. .DESCRIPTION Standalone wrapper for CiscoWLC discovery provider. Uses WhatsUpGoldPS.Snmp module to query Cisco LWAPP MIBs and generate comprehensive wireless network inventory: - Access Points (health, uptime, model, location) - Wireless Clients (SSID, AP mapping, authentication, device type) - WLANs (SSIDs, security profiles, radio policy) - System information and statistics Actions: PushToWUG Create or reuse WUG devices and sync monitors. ExportJSON Export discovery data as JSON files. ExportCSV Export discovery data as CSV files. ShowTable Display results in console table format. Dashboard Generate interactive HTML dashboards. DashboardAndPush Generate dashboards and push to WUG. None Run discovery but skip export/push (useful for testing). .PARAMETER Target Array of WLC IP addresses or hostnames to discover. .PARAMETER Action Action to perform after discovery. Valid values: 'PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'ExportToWUG', 'None' ExportToWUG: Export previously discovered data to WUG without running discovery again. .PARAMETER SnmpVersion SNMP version to use. Valid values: 1, 2, 3. Default: 2 .PARAMETER Community SNMPv1/v2c community string. Default: 'public' .PARAMETER SnmpUsername SNMP v3 username (security name). .PARAMETER SnmpContext SNMP v3 context. .PARAMETER SnmpAuthProtocol SNMP v3 authentication protocol. Valid values: 'None', 'MD5', 'SHA', 'SHA1', 'SHA256', 'SHA384', 'SHA512'. .PARAMETER SnmpAuthPassword SNMP v3 authentication password. .PARAMETER SnmpPrivacyProtocol SNMP v3 privacy protocol. Valid values: 'None', 'DES', '3DES', 'TripleDES', 'AES', 'AES128', 'AES192', 'AES256'. .PARAMETER SnmpPrivacyPassword SNMP v3 privacy password. .PARAMETER SnmpPort SNMP port. Default: 161. .PARAMETER TimeoutMs SNMP query timeout in milliseconds. Default: 10000 (10 seconds). .PARAMETER Retries SNMP retry count. Default: 1. .PARAMETER VaultName Optional DPAPI vault bundle name for SNMP settings. Default: CiscoWLC.Snmp. .PARAMETER WUGServer WhatsUp Gold server IP or hostname. Default: 192.168.74.74. .PARAMETER WUGCredential PSCredential for WhatsUp Gold authentication. .PARAMETER DeviceGroupId Device group ID in WhatsUp Gold for discovered devices. Default: 0 (root group). .PARAMETER PollingIntervalSeconds Polling interval in seconds for created monitors. Valid range: 60-86400. Default: 300. .PARAMETER OutputPath Directory for exported discovery plans. Default: %LOCALAPPDATA%\WhatsUpGoldPS\DiscoveryHelpers\Output (for non-interactive or WUG push), or %TEMP% for interactive mode. .PARAMETER NonInteractive Suppress prompts. Requires direct SNMP parameters or a saved vault bundle. .EXAMPLE # Standalone discovery with SNMPv2c .\Setup-CiscoWLC-Discovery.ps1 -Target '192.168.75.33' -SnmpVersion 2 -Community 'public' -Action Dashboard .EXAMPLE # Discover with SNMPv3 .\Setup-CiscoWLC-Discovery.ps1 ` -Target '10.0.0.100' ` -SnmpVersion 3 ` -SnmpUsername 'snmpuser' ` -SnmpAuthProtocol 'SHA' ` -SnmpAuthPassword 'authpass123' ` -SnmpPrivacyProtocol 'AES128' ` -SnmpPrivacyPassword 'privpass123' ` -Action Dashboard .EXAMPLE # Non-interactive with WUG push .\Setup-CiscoWLC-Discovery.ps1 -Target '192.168.75.33' -SnmpVersion 2 -Community 'public' -Action PushToWUG -NonInteractive .EXAMPLE # Export previously discovered data to WUG (no discovery) .\Setup-CiscoWLC-Discovery.ps1 -Target '192.168.75.33' -Action ExportToWUG -WUGServer 192.168.74.74 .NOTES Author: Jason Alberino Requires: WhatsUpGoldPS.Snmp module, DiscoveryHelpers.ps1, DiscoveryProvider-CiscoWLC.ps1 Optional: WhatsUpGoldPS module (for WUG integration), Export-DynamicDashboardHtml (for dashboards) Encoding: UTF-8 with BOM #> [CmdletBinding(SupportsShouldProcess)] param( [string[]]$Target, [ValidateSet('PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'ExportToWUG', 'None')] [string]$Action, [ValidateSet(1, 2, 3)] [int]$SnmpVersion = 2, [string]$Community = 'public', [string]$SnmpUsername, [string]$SnmpContext, [ValidateSet('None', 'MD5', 'SHA', 'SHA1', 'SHA256', 'SHA384', 'SHA512')] [string]$SnmpAuthProtocol = 'SHA', [string]$SnmpAuthPassword, [ValidateSet('None', 'DES', '3DES', 'TripleDES', 'AES', 'AES128', 'AES192', 'AES256')] [string]$SnmpPrivacyProtocol = 'AES128', [string]$SnmpPrivacyPassword, [int]$SnmpPort = 161, [int]$TimeoutMs = 10000, [int]$Retries = 1, [string]$VaultName = 'CiscoWLC.Snmp', [string]$WUGServer = '192.168.74.74', [PSCredential]$WUGCredential, [int]$DeviceGroupId = 0, [ValidateRange(60, 86400)] [int]$PollingIntervalSeconds = 300, [string]$OutputPath, [switch]$NonInteractive ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $script:InputSnmpVersionSpecified = $PSBoundParameters.ContainsKey('SnmpVersion') $script:InputSnmpAuthProtocolSpecified = $PSBoundParameters.ContainsKey('SnmpAuthProtocol') $script:InputSnmpPrivacyProtocolSpecified = $PSBoundParameters.ContainsKey('SnmpPrivacyProtocol') $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent . (Join-Path $scriptDir 'DiscoveryHelpers.ps1') . (Join-Path $scriptDir 'DiscoveryProvider-CiscoWLC.ps1') # ============================================================================ # Helper: SecureString <-> plaintext (vault field I/O) # ============================================================================ function ConvertTo-WLCPlainText { param($Value) if ($null -eq $Value) { return '' } if ($Value -is [System.Security.SecureString]) { $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Value) try { return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } return [string]$Value } function ConvertTo-WLCSecureString { param([string]$Value) if ([string]::IsNullOrEmpty($Value)) { $ss = New-Object System.Security.SecureString $ss.MakeReadOnly() return $ss } return (ConvertTo-SecureString -String $Value -AsPlainText -Force) } # ============================================================================ # Vault credential resolver for SNMP settings # ============================================================================ function Get-WLCResolvedSnmpSettings { [CmdletBinding()] param() $saved = $null if ($VaultName) { try { $saved = Get-DiscoveryCredential -Name $VaultName -ErrorAction SilentlyContinue } catch { $saved = $null } } if ($saved) { $savedVersion = if ($saved.ContainsKey('SnmpVersion')) { [string]$saved['SnmpVersion'] } else { '2' } $savedLabel = if ($savedVersion -eq '3') { 'SNMP v3' } else { "SNMP v$savedVersion" } # Auto-reset if caller explicitly requested a different SNMP version if ($script:InputSnmpVersionSpecified -and [int]$savedVersion -ne [int]$SnmpVersion) { $requestedLabel = if ($SnmpVersion -eq 3) { 'SNMP v3' } else { "SNMP v$SnmpVersion" } Write-Host " Vault: $VaultName" -ForegroundColor DarkGray Write-Host " Found: $savedLabel settings (switching to $requestedLabel)" -ForegroundColor Yellow Write-Host ' Removing old credential...' -ForegroundColor Yellow Remove-DiscoveryCredential -Name $VaultName -Confirm:$false -ErrorAction SilentlyContinue $saved = $null } elseif ($NonInteractive -or $Action) { Write-Host " Vault: $VaultName" -ForegroundColor DarkGray Write-Host " Found: $savedLabel settings" -ForegroundColor Green Write-Host ' Using existing credential.' -ForegroundColor Green } else { Write-Host " Vault: $VaultName" -ForegroundColor DarkGray Write-Host " Found: $savedLabel settings" -ForegroundColor Green $choice = Read-Host -Prompt ' Use existing? [Y]es / [R]eset / [N]o skip' switch -Regex ($choice) { '^[Rr]' { Write-Host ' Removing old credential...' -ForegroundColor Yellow Remove-DiscoveryCredential -Name $VaultName -Confirm:$false -ErrorAction SilentlyContinue $saved = $null } '^[Nn]' { Write-Host ' Skipped.' -ForegroundColor DarkGray return $null } default { Write-Host ' Using existing credential.' -ForegroundColor Green } } } } # ---- Resolve each setting (explicit param wins over vault) ---- $resolvedVersion = $SnmpVersion if ($saved -and $saved.ContainsKey('SnmpVersion') -and -not $script:InputSnmpVersionSpecified) { $resolvedVersion = [int]$saved['SnmpVersion'] } $resolvedCommunity = $Community if ([string]::IsNullOrWhiteSpace($resolvedCommunity) -and $saved -and $saved.ContainsKey('Community')) { $resolvedCommunity = [string]$saved['Community'] } $resolvedUsername = $SnmpUsername if ([string]::IsNullOrWhiteSpace($resolvedUsername) -and $saved -and $saved.ContainsKey('Username')) { $resolvedUsername = [string]$saved['Username'] } $resolvedContext = $SnmpContext if ([string]::IsNullOrWhiteSpace($resolvedContext) -and $saved -and $saved.ContainsKey('Context')) { $resolvedContext = [string]$saved['Context'] } $resolvedAuthProtocol = $SnmpAuthProtocol if ($saved -and $saved.ContainsKey('AuthProtocol') -and -not $script:InputSnmpAuthProtocolSpecified) { $resolvedAuthProtocol = [string]$saved['AuthProtocol'] } $resolvedPrivacyProtocol = $SnmpPrivacyProtocol if ($saved -and $saved.ContainsKey('PrivacyProtocol') -and -not $script:InputSnmpPrivacyProtocolSpecified) { $resolvedPrivacyProtocol = [string]$saved['PrivacyProtocol'] } $resolvedAuthPassword = ConvertTo-WLCPlainText $SnmpAuthPassword if ([string]::IsNullOrWhiteSpace($resolvedAuthPassword) -and $saved -and $saved.ContainsKey('AuthPassword')) { $resolvedAuthPassword = [string]$saved['AuthPassword'] } $resolvedPrivacyPassword = ConvertTo-WLCPlainText $SnmpPrivacyPassword if ([string]::IsNullOrWhiteSpace($resolvedPrivacyPassword) -and $saved -and $saved.ContainsKey('PrivacyPassword')) { $resolvedPrivacyPassword = [string]$saved['PrivacyPassword'] } # ---- Prompt for any missing required values ---- if ($resolvedVersion -in @(1, 2) -and [string]::IsNullOrWhiteSpace($resolvedCommunity)) { if ($NonInteractive) { throw 'SNMP community is required for non-interactive SNMP v1/v2 discovery.' } $resolvedCommunity = ConvertTo-WLCPlainText (Read-Host -AsSecureString -Prompt 'SNMP read community') } if ($resolvedVersion -eq 3) { if ([string]::IsNullOrWhiteSpace($resolvedUsername)) { if ($NonInteractive) { throw 'SNMP v3 username is required for non-interactive discovery.' } $resolvedUsername = Read-Host -Prompt 'SNMP v3 username' } # Prompt for auth protocol if not explicitly specified and no vault value if (-not $script:InputSnmpAuthProtocolSpecified -and -not $saved -and -not $NonInteractive) { Write-Host '' Write-Host ' Authentication protocol:' -ForegroundColor Cyan Write-Host ' [1] SHA / SHA1 (default)' -ForegroundColor White Write-Host ' [2] SHA256' -ForegroundColor White Write-Host ' [3] SHA512' -ForegroundColor White Write-Host ' [4] MD5' -ForegroundColor White Write-Host ' [5] None (no authentication)' -ForegroundColor White $authChoice = Read-Host -Prompt ' Choice [default: 1]' $resolvedAuthProtocol = switch ($authChoice) { '2' { 'SHA256' } '3' { 'SHA512' } '4' { 'MD5' } '5' { 'None' } default { 'SHA' } } } if (-not [string]::IsNullOrWhiteSpace($resolvedAuthProtocol) -and $resolvedAuthProtocol -ne 'None' -and [string]::IsNullOrWhiteSpace($resolvedAuthPassword)) { if ($NonInteractive) { throw 'SNMP v3 auth password is required when SnmpAuthProtocol is set.' } $resolvedAuthPassword = ConvertTo-WLCPlainText (Read-Host -AsSecureString -Prompt 'SNMP v3 auth password') } # Prompt for privacy protocol if not explicitly specified and no vault value if (-not $script:InputSnmpPrivacyProtocolSpecified -and -not $saved -and -not $NonInteractive) { Write-Host '' Write-Host ' Privacy protocol:' -ForegroundColor Cyan Write-Host ' [1] AES / AES128 (default)' -ForegroundColor White Write-Host ' [2] AES256' -ForegroundColor White Write-Host ' [3] DES' -ForegroundColor White Write-Host ' [4] None (no encryption)' -ForegroundColor White $privChoice = Read-Host -Prompt ' Choice [default: 1]' $resolvedPrivacyProtocol = switch ($privChoice) { '2' { 'AES256' } '3' { 'DES' } '4' { 'None' } default { 'AES128' } } } if (-not [string]::IsNullOrWhiteSpace($resolvedPrivacyProtocol) -and $resolvedPrivacyProtocol -ne 'None' -and [string]::IsNullOrWhiteSpace($resolvedPrivacyPassword)) { if ($NonInteractive) { throw 'SNMP v3 privacy password is required when SnmpPrivacyProtocol is set.' } $resolvedPrivacyPassword = ConvertTo-WLCPlainText (Read-Host -AsSecureString -Prompt 'SNMP v3 privacy password') } } else { # v1/v2 -- clear v3-only fields $resolvedUsername = '' $resolvedContext = '' $resolvedAuthProtocol = 'None' $resolvedAuthPassword = '' $resolvedPrivacyProtocol = 'None' $resolvedPrivacyPassword = '' } $settings = [ordered]@{ SnmpVersion = $resolvedVersion Community = $resolvedCommunity Username = $resolvedUsername Context = $resolvedContext AuthProtocol = $resolvedAuthProtocol AuthPassword = $resolvedAuthPassword PrivacyProtocol = $resolvedPrivacyProtocol PrivacyPassword = $resolvedPrivacyPassword Port = $SnmpPort TimeoutMs = $TimeoutMs Retries = $Retries } # ---- Persist to vault (interactive mode only) ---- if (-not $NonInteractive -and $VaultName) { $fields = [ordered]@{ SnmpVersion = ConvertTo-WLCSecureString -Value ([string]$settings.SnmpVersion) } if ([int]$settings.SnmpVersion -in @(1, 2)) { if (-not [string]::IsNullOrWhiteSpace($settings.Community)) { $fields['Community'] = ConvertTo-WLCSecureString -Value $settings.Community } } else { if (-not [string]::IsNullOrWhiteSpace($settings.Username)) { $fields['Username'] = ConvertTo-WLCSecureString -Value $settings.Username } if (-not [string]::IsNullOrWhiteSpace($settings.Context)) { $fields['Context'] = ConvertTo-WLCSecureString -Value $settings.Context } $fields['AuthProtocol'] = ConvertTo-WLCSecureString -Value ([string]$settings.AuthProtocol) $fields['PrivacyProtocol'] = ConvertTo-WLCSecureString -Value ([string]$settings.PrivacyProtocol) if (-not [string]::IsNullOrWhiteSpace($settings.AuthPassword)) { $fields['AuthPassword'] = ConvertTo-WLCSecureString -Value $settings.AuthPassword } if (-not [string]::IsNullOrWhiteSpace($settings.PrivacyPassword)) { $fields['PrivacyPassword'] = ConvertTo-WLCSecureString -Value $settings.PrivacyPassword } } Save-DiscoveryCredential -Name $VaultName -Fields $fields -Description 'CiscoWLC SNMP settings' -Force | Out-Null } return $settings } if (-not $OutputPath) { if ($NonInteractive -or $Action -in @('PushToWUG', 'DashboardAndPush')) { $OutputPath = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Output' } else { $OutputPath = $env:TEMP } } if (-not (Test-Path -LiteralPath $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } # ============================================================================ # region Load Dependencies # ============================================================================ # NonInteractive fallback: default to Dashboard if no Action specified if ($NonInteractive -and -not $Action) { $Action = 'Dashboard' } # Load dashboard generator for Dashboard/DashboardAndPush actions $generateDashboard = $Action -in @('Dashboard', 'DashboardAndPush') if ($generateDashboard) { $dynDashPath = Join-Path (Split-Path $scriptDir -Parent) 'reports\Export-DynamicDashboardHtml.ps1' if (Test-Path $dynDashPath) { . $dynDashPath } else { Write-Warning "Dashboard generator not found at $dynDashPath" $generateDashboard = $false } } Write-Host "`n=== Cisco WLC Discovery ===" -ForegroundColor Cyan Write-Host "Output Directory: $OutputPath`n" -ForegroundColor Yellow # Create output subdirectories (needed for all modes) $fullDir = Join-Path $OutputPath 'full' $summaryDir = Join-Path $OutputPath 'summary' $dashboardDir = Join-Path $summaryDir 'dashboards' foreach ($dir in @($fullDir, $summaryDir, $dashboardDir)) { if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } } # ============================================================================ # region Discovery: Direct SNMP Walks # ============================================================================ # Prompt for targets interactively if none provided if (-not $Target -and -not $NonInteractive) { Write-Host 'Enter WLC IP address(es) or hostname(s) to discover.' -ForegroundColor Yellow Write-Host ' Separate multiple entries with commas (e.g., 10.0.0.1, 10.0.0.2)' -ForegroundColor DarkYellow $targetInput = Read-Host ' WLC Target(s)' if ($targetInput) { $Target = @($targetInput -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } if (-not $Target -or $Target.Count -eq 0) { Write-Warning 'No targets specified. Exiting.' return } } if ($Target) { Write-Host "Mode: Direct SNMP bulk walks" -ForegroundColor Green Write-Host "Target: $($Target -join ', ')" -ForegroundColor Yellow # ---- Resolve SNMP credentials (vault or direct params) ---- Write-Host "`nStep 0: Resolving SNMP credentials (vault: $VaultName)..." -ForegroundColor Cyan $snmpSettings = Get-WLCResolvedSnmpSettings if (-not $snmpSettings) { Write-Warning 'No SNMP credentials available. Exiting.' return } # Map numeric SnmpVersion (1, 2, 3) to version strings used by SNMP module (V1, V2, V3) $snmpVersionStr = @{ 1 = 'V1'; 2 = 'V2'; 3 = 'V3' }[[int]$snmpSettings.SnmpVersion] Write-Host "SNMP Version: $snmpVersionStr`n" -ForegroundColor Yellow # Load SNMP module Write-Host "Step 1: Loading SNMP module..." -ForegroundColor Cyan $snmpModulePath = Join-Path (Split-Path $scriptDir -Parent) 'snmp\WhatsUpGoldPS.Snmp\WhatsUpGoldPS.Snmp.psd1' Import-Module $snmpModulePath -Force -ErrorAction Stop Import-SharpSnmpLib -ErrorAction Stop | Out-Null Write-Host " [+] SNMP module loaded`n" -ForegroundColor Green # Define OID trees for discovery $oidTrees = @( @{ Name = 'wireless-sys'; OID = '1.3.6.1.4.1.9.9.618'; File = 'wireless-sys.jsonl'; Required = $false }, @{ Name = 'wireless-ap'; OID = '1.3.6.1.4.1.9.9.513.1.1.1'; File = 'wireless-ap.jsonl'; Required = $true }, @{ Name = 'wireless-dot11-client'; OID = '1.3.6.1.4.1.9.9.599.1.3.1'; File = 'wireless-dot11-client.jsonl'; Required = $true }, @{ Name = 'wireless-wlan'; OID = '1.3.6.1.4.1.9.9.512.1.1.1'; File = 'wireless-wlan.jsonl'; Required = $true }, @{ Name = 'wireless-dot11'; OID = '1.3.6.1.4.1.9.9.612'; File = 'wireless-dot11.jsonl'; Required = $false }, @{ Name = 'wireless-rogue'; OID = '1.3.6.1.4.1.9.9.610'; File = 'wireless-rogue.jsonl'; Required = $false }, @{ Name = 'wireless-rf'; OID = '1.3.6.1.4.1.9.9.778'; File = 'wireless-rf.jsonl'; Required = $false }, @{ Name = 'wireless-mobility-ext'; OID = '1.3.6.1.4.1.9.9.846'; File = 'wireless-mobility-ext.jsonl'; Required = $false }, @{ Name = 'wireless-airespace-client'; OID = '1.3.6.1.4.1.14179.2.1.4'; File = 'wireless-airespace-client.jsonl'; Required = $false }, @{ Name = 'wireless-airespace-ap'; OID = '1.3.6.1.4.1.14179.2.2.1'; File = 'wireless-airespace-ap.jsonl'; Required = $false } ) $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' foreach ($wlcAddress in $Target) { Write-Host "Step 2: SNMP Bulk Walk for $wlcAddress" -ForegroundColor Cyan foreach ($tree in $oidTrees) { Write-Host " Walking $($tree.Name) ($($tree.OID))..." -ForegroundColor Gray try { $walkParams = @{ Target = $wlcAddress Table = $tree.OID MaxRepetitions = 25 Timeout = $snmpSettings.TimeoutMs ErrorAction = 'Stop' } if ($snmpVersionStr -eq 'V3') { $walkParams['Version'] = 'V3' $walkParams['Username'] = $snmpSettings.Username $walkParams['AuthProtocol'] = $snmpSettings.AuthProtocol $walkParams['AuthPassword'] = $snmpSettings.AuthPassword $walkParams['PrivProtocol'] = $snmpSettings.PrivacyProtocol $walkParams['PrivPassword'] = $snmpSettings.PrivacyPassword $walk = Invoke-SNMPBulkWalkFriendly @walkParams } else { $walkParams['Version'] = $snmpVersionStr $walkParams['Community'] = $snmpSettings.Community $walk = Invoke-SNMPBulkWalk @walkParams } Write-Host " [+] Retrieved $($walk.Variables.Count) variables" -ForegroundColor Green # Export to JSONL $jsonlPath = Join-Path $fullDir $tree.File $writer = [System.IO.StreamWriter]::new($jsonlPath, $false, [System.Text.Encoding]::UTF8) try { foreach ($var in $walk.Variables) { $obj = @{ OID = [string]$var.Id Value = [string]$var.Data Type = $var.Data.GetType().Name } if ($var.Data -is [Lextm.SharpSnmpLib.OctetString]) { $hexVal = $var.Data.ToHexString() if (-not [string]::IsNullOrWhiteSpace($hexVal)) { $obj['HexValue'] = $hexVal } } $json = $obj | ConvertTo-Json -Compress $writer.WriteLine($json) } } finally { $writer.Dispose() } Write-Host " [+] Exported to $($tree.File)" -ForegroundColor Green } catch { if ($tree.Required) { Write-Host " [-] Failed: $_" -ForegroundColor Red throw } else { Write-Host " [!] Skipped (optional): $_" -ForegroundColor Yellow $jsonlPath = Join-Path $fullDir $tree.File '' | Out-File -FilePath $jsonlPath -Encoding UTF8 } } } } # Step 3: Build summaries Write-Host "`nStep 3: Building summaries..." -ForegroundColor Cyan $buildScript = Join-Path (Split-Path $scriptDir -Parent) 'cisco-wlc\Build-Wireless-Summaries.ps1' if (Test-Path $buildScript) { & $buildScript -InputDirectory $fullDir -OutputDirectory $summaryDir Write-Host " [+] Summaries built" -ForegroundColor Green } else { Write-Warning "Build-Wireless-Summaries.ps1 not found" } # Step 4: Post-discovery action # If no -Action was specified and not NonInteractive, show an interactive menu if (-not $Action -and -not $NonInteractive) { Write-Host '' Write-Host 'What would you like to do?' -ForegroundColor Cyan Write-Host ' [1] Generate HTML dashboards' Write-Host ' [2] Push AP devices to WhatsUp Gold' Write-Host ' [3] Dashboard + Push to WUG' Write-Host ' [4] Export inventory to JSON file' Write-Host ' [5] Exit (do nothing)' Write-Host '' $menuChoice = Read-Host -Prompt 'Choice [1-5]' switch ($menuChoice) { '1' { $Action = 'Dashboard' } '2' { $Action = 'PushToWUG' } '3' { $Action = 'DashboardAndPush' } '4' { $Action = 'ExportJSON' } '5' { $Action = 'None' } default { $Action = 'None' } } # Re-evaluate dashboard flag after menu selection $generateDashboard = $Action -in @('Dashboard', 'DashboardAndPush') } if ($generateDashboard) { Write-Host "`nStep 4: Generating dashboards..." -ForegroundColor Cyan $dashboardScript = Join-Path (Split-Path $scriptDir -Parent) 'cisco-wlc\Export-Wireless-Dashboard-Pack.ps1' if (Test-Path $dashboardScript) { # Resolve repo root: scriptDir is helpers/discovery, go up to repo root $repoRoot = Split-Path (Split-Path $scriptDir -Parent) -Parent & $dashboardScript -SummaryDirectory $summaryDir -OutputDirectory $dashboardDir -WhatsUpGoldPsRepoPath $repoRoot Write-Host " [+] Dashboards generated" -ForegroundColor Green $indexPath = Join-Path $dashboardDir 'wireless-dashboard-index.html' if (Test-Path $indexPath) { Write-Host "`nDashboard: $indexPath" -ForegroundColor Yellow Write-Host "Opening in browser..." -ForegroundColor Gray Start-Process $indexPath } # Copy dashboards to WUG NmConsole if installed locally $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 } $dashboardFiles = @(Get-ChildItem $dashboardDir -Filter '*.html' -ErrorAction SilentlyContinue) foreach ($dashFile in $dashboardFiles) { $destPath = Join-Path $wugDashDir $dashFile.Name try { Copy-Item -LiteralPath $dashFile.FullName -Destination $destPath -Force Write-Host " Copied to WUG: $destPath" -ForegroundColor Green } catch { Write-Warning "Could not copy dashboard to NmConsole (run as admin?): $_" } } Deploy-DashboardWebConfig -Path $wugDashDir Write-Host " Access via WUG web UI: /NmConsole/dashboards/" -ForegroundColor Cyan } } else { Write-Warning "Export-Wireless-Dashboard-Pack.ps1 not found" } } Write-Host "`n=== Discovery Complete ===" -ForegroundColor Cyan Write-Host "Full data: $fullDir" -ForegroundColor Gray Write-Host "Summaries: $summaryDir" -ForegroundColor Gray if ($generateDashboard) { Write-Host "Dashboards: $dashboardDir" -ForegroundColor Gray } } # ============================================================================ # region WUG Integration Mode # ============================================================================ if ($Action -in @('PushToWUG', 'DashboardAndPush')) { Write-Host "`nMode: WUG Integration (Push to WhatsUp Gold)" -ForegroundColor Green # For each WLC target, create AP devices in WUG foreach ($wlcAddress in $Target) { Write-Host "`nStep 5: Creating/updating AP devices in WUG for $wlcAddress..." -ForegroundColor Cyan $apExportScript = Join-Path (Split-Path $scriptDir -Parent) 'cisco-wlc\Export-CiscoWLCAPDevicesToWUG.ps1' if (Test-Path $apExportScript) { $apSplat = @{ InputDirectory = $fullDir WLCAddress = $wlcAddress WUGServer = "https://$WUGServer`:9644" DeviceGroupId = $DeviceGroupId PollingIntervalMinutes = [math]::Max(1, [math]::Floor($PollingIntervalSeconds / 60)) Verbose = $VerbosePreference -ne 'SilentlyContinue' } if ($WUGCredential) { $apSplat['WUGCredential'] = $WUGCredential } try { & $apExportScript @apSplat Write-Host " [+] AP device export complete" -ForegroundColor Green } catch { Write-Warning "AP device export failed: $_" } } else { Write-Warning "AP device export script not found at $apExportScript" } } } # ============================================================================ # region ExportToWUG Mode (Export Only, No Discovery) # ============================================================================ if ($Action -eq 'ExportToWUG') { Write-Host "`nMode: Export to WhatsUp Gold (No Discovery)" -ForegroundColor Green # Verify output directory exists with summary data if (-not (Test-Path $summaryDir)) { Write-Warning "Summary directory not found: $summaryDir" Write-Host "Run discovery first (e.g., with -Action 'Dashboard' or 'PushToWUG')" -ForegroundColor Yellow exit 1 } # For each WLC target, create/update AP devices in WUG foreach ($wlcAddress in $Target) { Write-Host "`nExporting AP devices in WUG for $wlcAddress..." -ForegroundColor Cyan $apExportScript = Join-Path (Split-Path $scriptDir -Parent) 'cisco-wlc\Export-CiscoWLCAPDevicesToWUG.ps1' if (Test-Path $apExportScript) { $apSplat = @{ InputDirectory = $summaryDir WLCAddress = $wlcAddress WUGServer = "https://$WUGServer`:9644" DeviceGroupId = $DeviceGroupId PollingIntervalMinutes = [math]::Max(1, [math]::Floor($PollingIntervalSeconds / 60)) Verbose = $VerbosePreference -ne 'SilentlyContinue' } if ($WUGCredential) { $apSplat['WUGCredential'] = $WUGCredential } try { & $apExportScript @apSplat Write-Host " [+] AP device export complete" -ForegroundColor Green } catch { Write-Warning "AP device export failed: $_" } } else { Write-Warning "AP device export script not found at $apExportScript" } } } Write-Host "`n=== Discovery Complete ===" -ForegroundColor Cyan # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBEOso+DDjztn1L # mwo6HSiESRvGvF5O42TKE0kwr0a1hqCCJQ0wggVvMIIEV6ADAgECAhBI/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 # BCD7WzxViXD4cS62nMsJKWj2wjdBW5CTZqZaq6uy4QyuITANBgkqhkiG9w0BAQEF # AASCAgB4NWke5tRqDY2bILa/fsHtRPfnNp+VR8Q39BfsA5yWT/GRL+tHdlDJm7gF # 3ujYCuQTRSv6dOFnJUTaWofNWJ5M3pnnqhSSMHtP9d10OnawqDUF2hRuvLafIO8+ # sKrT2UeIUmxiva1S78PEO1wMONvcvURMR5z86e74FDzAI+DV9D/quq/+/nV9L0cz # 4vufe2NfUKOIcGI8k5FGT5sC1ZbdbE0S3hxNt3Z4VWWDFUU6NuIIDDNLxA8tE4Zw # Rw1I85oXt3KueA1vbWBUSpKGAvXD24yd+1B+EYlUi6XAPMOlHFZ+Xd34TWRA29eG # En/Xe9dlKUKXrvh1H8clIYJWLd0+RPIwTgAuHeQULgkeBqZnBJOhhqamyOv/2IbB # 47Qv9UqBRgdc7D+Cx3OfVWwitTTUiHZuIvq/IxKoxgJn7lZaJLJ1ZjGVB0zJSGNd # 1y5uupjzDfnBqy2iPaX1zJKyUkHqO4Yq40FKLhKIFzZpRKT/6quKamWw9+NyFptS # dBYL3kE3IKjQasG3QC6RYUpHN1wlSMVHLyoM2VJqHNusxgm5Yv1haRVgGZjDXELX # UvpejukvNyJ8KHUCJoeNjzegrszxx9vzxeEXgRIaeagco2rF4Ugzn675ZJdkUukJ # A4rr39RLPnQY/epTLlj8SRUqmhZQaPWTPWvPmD1J0X5V269Hh6GCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV # dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA3MTQwMDE5MTBaMC8GCSqGSIb3DQEJBDEiBCB7l4fk # 2mO1342AHnPBSjmIbkA47YrVn968nQen4OxUFzANBgkqhkiG9w0BAQEFAASCAgAd # q4oGcdYSE6nxhckpJ2qZqqHSNfPCJojRD3Vr8t+J7AE0eo0ZmsUXFlfz4qDZCGM/ # 3EqO4NpW31MkKyrxnUxvgH4eyCTeTiPngeYFUWQLMbnW5yzkX4/oKQ9uKsUc9Qcx # JipfXdzY6kJRxLByBqAUEXoDfmEG11PAudTrP1BhdkabDvvvDGtkGFv94AfH8AxX # KJpV/2fbZEPf5r69R7iEW5DddFQFBW6KJq3mORcep/BxCOvD5xtxvQJX57NP064p # 45kl8GEZx5E3HdOgrglrR2qQ7tcrzAOE7DcIA5aQnMJyv1Je/q6bQpbjhSIxeK0i # KDNa0ib6yYJTEAwXxaEhU6m2m0wdoCQ7Y4PJDQQ60Z+BWhGRsgU15AqP7FrTkZRD # 8Y7vJEA8narSkkjkPWlGAxh/aYH3cDhYiIwrUaQxyqAqe0YE8m3V3brZq9Y64aRQ # Z2vc20xAsXzUYuZZm0rpM5FWfkUfEdhkvnz6KFf+gC4uaF62aKcvGvVv4kBUP7zx # hDthsvS91VgPZ79vlMUbx+Ns9WwmRLUkijB63ytWrXYqh0dZBx5wh6cbpKzcuz8z # JZAALc1PE6/+NgWn55bEMpsaNcIws5zWCWwLWMEI+jTsw3eBMKaaTSUPyOPbVt/l # xXgke9QVEtSbk1lj5ETd8FS2OKiQOnZtShnplD0dOw== # SIG # End signature block |