helpers/discovery/Setup-CUCM-Discovery.ps1
|
<#
.SYNOPSIS CUCM phone inventory discovery, dashboard generation, and optional WUG monitor sync. .DESCRIPTION Uses the WhatsUp Gold SNMP COM API to walk Cisco's ccmPhoneTable on one or more CUCM servers. The script keeps two outputs separate: - inventory rows for dashboards, exports, and console review - a framework-aligned WUG monitor plan for shared SNMPTable phone status monitoring plus aggregate CUCM count attributes on the device Actions: PushToWUG Create or reuse WUG devices and SNMP credentials, then sync monitors. ExportJSON Export phone inventory plus monitor-plan metadata to JSON. ExportCSV Export phone inventory rows to CSV. ShowTable Show a concise phone table in the console. Dashboard Generate one HTML report per CUCM target. DashboardAndPush Generate dashboards, then push the monitor plan to WUG. None Discover only. .PARAMETER Target One or more CUCM IP addresses or hostnames. .PARAMETER Action What to do after discovery. .PARAMETER SnmpVersion SNMP version to use with Initialize4. Valid values: 1, 2, 3. .PARAMETER Community Read community for SNMP v1/v2. .PARAMETER SnmpUsername SNMP v3 username. .PARAMETER SnmpContext SNMP v3 context. .PARAMETER SnmpAuthProtocol SNMP v3 auth protocol. Valid values: 'None', 'MD5', 'SHA', 'SHA1', 'SHA256', 'SHA384', 'SHA512'. .PARAMETER SnmpAuthPassword SNMP v3 auth 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 timeout in milliseconds. Default: 5000. .PARAMETER Retries SNMP retry count. Default: 1. .PARAMETER VaultName Optional DPAPI vault bundle name for SNMP settings. Default: CUCM.Snmp. .PARAMETER WUGServer Optional WhatsUp Gold server address for PushToWUG actions. .PARAMETER WUGCredential Optional WhatsUp Gold credential for PushToWUG actions. .PARAMETER DeviceGroupId Optional WUG device group for newly created devices. Default: 0. .PARAMETER PollingIntervalSeconds Polling interval for WUG active monitors. Default: 300. .PARAMETER OutputPath Output directory for exports and dashboards. .PARAMETER NonInteractive Suppress prompts. Requires direct SNMP parameters or a saved vault bundle. .EXAMPLE .\Setup-CUCM-Discovery.ps1 -Target 192.168.75.33,192.168.75.34 -Action Dashboard .EXAMPLE .\Setup-CUCM-Discovery.ps1 -Target 192.168.75.33 -SnmpVersion 2 -Community public -Action PushToWUG -NonInteractive .NOTES Requires: PowerShell 5.1+, WhatsUpGoldPS.Snmp module (SharpSnmpLib). Encoding: UTF-8 with BOM #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [string[]]$Target, [ValidateSet('PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'None')] [string]$Action, [ValidateSet(1, 2, 3)] [int]$SnmpVersion = 2, [string]$Community, [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 = 5000, [int]$Retries = 1, [string]$VaultName = 'CUCM.Snmp', [string]$WUGServer, [PSCredential]$WUGCredential, [int]$DeviceGroupId = 0, [ValidateRange(60, 86400)] [int]$PollingIntervalSeconds = 300, [string]$OutputPath, [switch]$NonInteractive ) $script:InputSnmpVersionSpecified = $PSBoundParameters.ContainsKey('SnmpVersion') $script:InputSnmpAuthProtocolSpecified = $PSBoundParameters.ContainsKey('SnmpAuthProtocol') $script:InputSnmpPrivacyProtocolSpecified = $PSBoundParameters.ContainsKey('SnmpPrivacyProtocol') function ConvertTo-CUCMPlainText { [CmdletBinding()] param( [Parameter()] $Value ) if ($null -eq $Value) { return '' } # Handle both SecureString and plain string inputs 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) } } else { return [string]$Value } } function ConvertTo-CUCMSecureString { [CmdletBinding()] param( [Parameter()] [string]$Value ) if ([string]::IsNullOrEmpty($Value)) { $empty = New-Object System.Security.SecureString $empty.MakeReadOnly() return $empty } return (ConvertTo-SecureString -String $Value -AsPlainText -Force) } function ConvertTo-WUGAuthProtocolCode { <# .SYNOPSIS Converts friendly SNMP auth protocol names to WUG numeric codes. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Protocol ) $map = @{ 'None' = '0' 'MD5' = '1' 'SHA' = '3' 'SHA1' = '3' 'SHA256' = '5' 'SHA384' = '6' 'SHA512' = '7' } if ($map.ContainsKey($Protocol)) { return $map[$Protocol] } throw "Unsupported SNMP auth protocol '$Protocol' for WUG. Supported: $($map.Keys -join ', ')" } function ConvertTo-WUGPrivProtocolCode { <# .SYNOPSIS Converts friendly SNMP privacy protocol names to WUG numeric codes. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Protocol ) $map = @{ 'None' = '0' 'DES' = '1' '3DES' = '2' 'TripleDES' = '2' 'AES' = '3' 'AES128' = '3' 'AES192' = '4' 'AES256' = '5' } if ($map.ContainsKey($Protocol)) { return $map[$Protocol] } throw "Unsupported SNMP privacy protocol '$Protocol' for WUG. Supported: $($map.Keys -join ', ')" } function Get-CUCMDashboardRows { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [object[]]$PhoneRows ) $rows = @() foreach ($row in $PhoneRows) { $attrs = $row.Attributes $rows += [PSCustomObject]@{ CallManager = $row.DeviceName PhoneName = $attrs['CUCM.PhoneName'] Description = $attrs['CUCM.PhoneDescription'] UserName = $attrs['CUCM.PhoneUserName'] Status = $attrs['CUCM.PhoneStatus'] Protocol = $attrs['CUCM.PhoneProtocol'] IPAddress = $attrs['CUCM.PhoneIpAddress'] IPv4Address = $attrs['CUCM.PhoneInetAddressIPv4'] MACAddress = $attrs['CUCM.PhonePhysicalAddress'] LastRegistered = $attrs['CUCM.PhoneTimeLastRegistered'] LastStatusUpdate = $attrs['CUCM.PhoneTimeLastStatusUpdt'] LoadID = $attrs['CUCM.PhoneLoadID'] Target = $attrs['CUCM.Target'] } } return $rows } function Get-CUCMResolvedSnmpSettings { [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' } $savedAuth = 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) { $requestedAuth = if ($SnmpVersion -eq 3) { 'SNMP v3' } else { "SNMP v$SnmpVersion" } Write-Host " Vault: $VaultName" -ForegroundColor DarkGray Write-Host " Found: $savedAuth settings (switching to $requestedAuth)" -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: $savedAuth settings" -ForegroundColor Green Write-Host ' Using existing credential.' -ForegroundColor Green } else { Write-Host " Vault: $VaultName" -ForegroundColor DarkGray Write-Host " Found: $savedAuth settings" -ForegroundColor Green $savedChoice = Read-Host -Prompt ' Use existing? [Y]es / [R]eset / [N]o skip' switch -Regex ($savedChoice) { '^[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 } } } } $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-CUCMPlainText $SnmpAuthPassword if ([string]::IsNullOrWhiteSpace($resolvedAuthPassword) -and $saved -and $saved.ContainsKey('AuthPassword')) { $resolvedAuthPassword = [string]$saved['AuthPassword'] } $resolvedPrivacyPassword = ConvertTo-CUCMPlainText $SnmpPrivacyPassword if ([string]::IsNullOrWhiteSpace($resolvedPrivacyPassword) -and $saved -and $saved.ContainsKey('PrivacyPassword')) { $resolvedPrivacyPassword = [string]$saved['PrivacyPassword'] } if ($resolvedVersion -in @(1, 2) -and [string]::IsNullOrWhiteSpace($resolvedCommunity)) { if ($NonInteractive) { throw 'SNMP community is required for non-interactive SNMP v1/v2 discovery.' } $communitySecure = Read-Host -AsSecureString -Prompt 'SNMP read community' $resolvedCommunity = ConvertTo-CUCMPlainText $communitySecure } 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 ($resolvedAuthProtocol -gt 0 -and [string]::IsNullOrWhiteSpace($resolvedAuthPassword)) { if ($NonInteractive) { throw 'SNMP v3 auth password is required when SnmpAuthProtocol is set.' } $resolvedAuthPassword = ConvertTo-CUCMPlainText (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 ($resolvedPrivacyProtocol -gt 0 -and [string]::IsNullOrWhiteSpace($resolvedPrivacyPassword)) { if ($NonInteractive) { throw 'SNMP v3 privacy password is required when SnmpPrivacyProtocol is set.' } $resolvedPrivacyPassword = ConvertTo-CUCMPlainText (Read-Host -AsSecureString -Prompt 'SNMP v3 privacy password') } } else { # For SNMP v1/v2, ignore all v3-only settings. $resolvedUsername = '' $resolvedContext = '' $resolvedAuthProtocol = 0 $resolvedAuthPassword = '' $resolvedPrivacyProtocol = 0 $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 } Write-Verbose "[SNMP Settings Resolved] Version: $resolvedVersion Community: $(if([string]::IsNullOrWhiteSpace($resolvedCommunity)) { '(empty)' } else { '*****' }) Port: $SnmpPort Timeout: ${TimeoutMs}ms Retries: $Retries" if ($resolvedVersion -eq 3) { Write-Verbose "[SNMP v3] Username: $(if([string]::IsNullOrWhiteSpace($resolvedUsername)) { '(empty)' } else { $resolvedUsername }) Context: $(if([string]::IsNullOrWhiteSpace($resolvedContext)) { '(empty)' } else { $resolvedContext }) AuthProtocol: $resolvedAuthProtocol PrivacyProtocol: $resolvedPrivacyProtocol" } if (-not $NonInteractive -and $VaultName) { $fields = [ordered]@{ SnmpVersion = ConvertTo-CUCMSecureString -Value ([string]$settings.SnmpVersion) } if ([int]$settings.SnmpVersion -in @(1, 2)) { if (-not [string]::IsNullOrWhiteSpace($settings.Community)) { $fields['Community'] = ConvertTo-CUCMSecureString -Value $settings.Community } } else { if (-not [string]::IsNullOrWhiteSpace($settings.Username)) { $fields['Username'] = ConvertTo-CUCMSecureString -Value $settings.Username } if (-not [string]::IsNullOrWhiteSpace($settings.Context)) { $fields['Context'] = ConvertTo-CUCMSecureString -Value $settings.Context } $fields['AuthProtocol'] = ConvertTo-CUCMSecureString -Value ([string]$settings.AuthProtocol) $fields['PrivacyProtocol'] = ConvertTo-CUCMSecureString -Value ([string]$settings.PrivacyProtocol) if (-not [string]::IsNullOrWhiteSpace($settings.AuthPassword)) { $fields['AuthPassword'] = ConvertTo-CUCMSecureString -Value $settings.AuthPassword } if (-not [string]::IsNullOrWhiteSpace($settings.PrivacyPassword)) { $fields['PrivacyPassword'] = ConvertTo-CUCMSecureString -Value $settings.PrivacyPassword } } Save-DiscoveryCredential -Name $VaultName -Fields $fields -Description 'CUCM SNMP settings' -Force | Out-Null } return $settings } function New-CUCMBatchAttributeBody { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [hashtable]$Attributes ) $items = @() foreach ($name in $Attributes.Keys) { $items += @{ op = 'add'; name = $name; value = [string]$Attributes[$name] } } return (@{ items = $items } | ConvertTo-Json -Depth 6) } function Export-CUCMInventoryJson { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [hashtable]$DevicePlan ) $inventoryRows = @() $monitorPlan = @() $summaries = @() foreach ($device in $DevicePlan.Values) { $inventoryRows += @(Get-CUCMDashboardRows -PhoneRows @($device.PhoneRows)) $monitorPlan += @(New-CUCMDiscoveryPlanFromPhoneInventory -DeviceId 0 -DeviceName $device.Name -TargetAddress $device.IP -PhoneRows @($device.PhoneRows)) $summaries += [PSCustomObject]@{ DeviceName = $device.Name Target = $device.IP Phones = $device.Summary.total Registered = $device.Summary.registered Unregistered = $device.Summary.unregistered Rejected = $device.Summary.rejected PartiallyRegistered = $device.Summary.partiallyregistered Unknown = $device.Summary.unknown } } $payload = [PSCustomObject]@{ GeneratedAt = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') Targets = $Target Summary = $summaries Inventory = $inventoryRows MonitorPlan = $monitorPlan } $payload | ConvertTo-Json -Depth 8 | Set-Content -Path $Path } function Import-CUCMWUGModule { [CmdletBinding()] param() if (Get-Command -Name 'Get-WUGDevice' -ErrorAction SilentlyContinue) { return } $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 } } function Connect-CUCMWUG { [CmdletBinding()] param() if ($WUGCredential) { if (-not $WUGServer) { throw 'WUGServer is required when WUGCredential is supplied.' } Connect-WUGServer -serverUri $WUGServer -Credential $WUGCredential -IgnoreSSLErrors | Out-Null return } if ($WUGServer) { Connect-WUGServer -serverUri $WUGServer -IgnoreSSLErrors | Out-Null } else { Connect-WUGServer -AutoConnect -IgnoreSSLErrors | Out-Null } } function Resolve-CUCMWUGDevice { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$TargetAddress ) $matches = @(Get-WUGDevice -SearchValue $TargetAddress) $device = $matches | Where-Object { $_.networkAddress -eq $TargetAddress -or $_.hostName -eq $TargetAddress -or $_.displayName -eq $TargetAddress } | Select-Object -First 1 if (-not $device) { Add-WUGDevice -IpOrName $TargetAddress -GroupId $DeviceGroupId -UseAllCredentials:$false -ForceAdd:$true -Confirm:$false | Out-Null $matches = @(Get-WUGDevice -SearchValue $TargetAddress) $device = $matches | Where-Object { $_.networkAddress -eq $TargetAddress -or $_.hostName -eq $TargetAddress -or $_.displayName -eq $TargetAddress } | Select-Object -First 1 } if (-not $device) { throw "Could not resolve or create WUG device for $TargetAddress." } return $device } function Get-CUCMSnmpCredentialName { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$TargetAddress, [Parameter(Mandatory = $true)] [hashtable]$SnmpSettings ) return "CUCM SNMP [$TargetAddress] v$($SnmpSettings.SnmpVersion)" } function Ensure-CUCMSnmpCredential { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$TargetAddress, [Parameter(Mandatory = $true)] [hashtable]$SnmpSettings ) $credentialType = switch ([int]$SnmpSettings.SnmpVersion) { 1 { 'snmpV1' } 2 { 'snmpV2' } 3 { 'snmpV3' } default { throw "Unsupported SNMP version '$($SnmpSettings.SnmpVersion)' for WUG push." } } $credentialName = Get-CUCMSnmpCredentialName -TargetAddress $TargetAddress -SnmpSettings $SnmpSettings $existing = @(Get-WUGCredential -SearchValue $credentialName -Type $credentialType -View basic) | Where-Object { $_.name -eq $credentialName } | Select-Object -First 1 if ($existing) { return $existing } switch ($credentialType) { 'snmpV1' { return (Add-WUGCredential -Name $credentialName -Type snmpV1 -SnmpReadCommunity $SnmpSettings.Community -Confirm:$false) } 'snmpV2' { return (Add-WUGCredential -Name $credentialName -Type snmpV2 -SnmpReadCommunity $SnmpSettings.Community -Confirm:$false) } 'snmpV3' { if ([string]::IsNullOrWhiteSpace($SnmpSettings.Username) -or [string]::IsNullOrWhiteSpace($SnmpSettings.AuthPassword)) { throw 'PushToWUG requires SNMPv3 username and auth password so a WUG credential can be created.' } # Convert friendly protocol names to WUG numeric codes $authProtocol = ConvertTo-WUGAuthProtocolCode -Protocol $SnmpSettings.AuthProtocol $encryptProtocol = ConvertTo-WUGPrivProtocolCode -Protocol $SnmpSettings.PrivacyProtocol return (Add-WUGCredential -Name $credentialName -Type snmpV3 ` -SnmpV3Username $SnmpSettings.Username ` -SnmpV3Context $SnmpSettings.Context ` -SnmpV3AuthPassword $SnmpSettings.AuthPassword ` -SnmpV3AuthProtocol $authProtocol ` -SnmpV3EncryptPassword $SnmpSettings.PrivacyPassword ` -SnmpV3EncryptProtocol $encryptProtocol ` -Confirm:$false) } } } function Get-CUCMCredentialIdentifier { [CmdletBinding()] param( [Parameter(Mandatory = $true)] $CredentialObject ) if ($null -eq $CredentialObject) { throw 'WUG credential creation failed (returned null). Check that the WUG server is reachable and credentials are valid.' } if ($CredentialObject.PSObject.Properties['resourceId']) { return [string]$CredentialObject.resourceId } if ($CredentialObject.PSObject.Properties['id']) { return [string]$CredentialObject.id } throw 'Could not determine the WUG credential identifier.' } function Push-CUCMPlanToWUG { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [hashtable]$DevicePlan, [Parameter(Mandatory = $true)] [hashtable]$SnmpSettings ) Import-CUCMWUGModule Connect-CUCMWUG $pushPlan = @() foreach ($deviceEntry in $DevicePlan.Values) { $resolvedDevice = Resolve-CUCMWUGDevice -TargetAddress $deviceEntry.IP $wugCredential = Ensure-CUCMSnmpCredential -TargetAddress $deviceEntry.IP -SnmpSettings $SnmpSettings $wugCredentialId = Get-CUCMCredentialIdentifier -CredentialObject $wugCredential $assignedSnmpCreds = @(Get-WUGDeviceCredential -DeviceId $resolvedDevice.id -Type snmp) if ($assignedSnmpCreds.Count -gt 0) { $alreadyAssigned = $assignedSnmpCreds | Where-Object { $_.id -eq $wugCredentialId -or $_.credentialId -eq $wugCredentialId -or $_.name -eq $wugCredential.name } | Select-Object -First 1 if (-not $alreadyAssigned) { Write-Warning "Device $($resolvedDevice.displayName) already has SNMP credential assignments. Adding the CUCM credential without removing the existing ones." } } Set-WUGDeviceCredential -DeviceId $resolvedDevice.id -CredentialId $wugCredentialId -Assign -Confirm:$false | Out-Null Set-WUGDeviceAttribute -DeviceId $resolvedDevice.id -Name 'DiscoveryHelper.CUCM' -Value 'true' -Confirm:$false | Out-Null $pushPlan += @(New-CUCMDiscoveryPlanFromPhoneInventory -DeviceId ([int]$resolvedDevice.id) -DeviceName $resolvedDevice.displayName -TargetAddress $deviceEntry.IP -PhoneRows @($deviceEntry.PhoneRows)) } if (-not $pushPlan -or $pushPlan.Count -eq 0) { Write-Warning 'No CUCM monitor items were generated for WUG sync.' return $null } if (-not $PSCmdlet.ShouldProcess("$($pushPlan.Count) CUCM monitor plan item(s)", 'Sync monitors to WhatsUp Gold')) { return $null } return (Invoke-WUGDiscoverySync -Plan $pushPlan -PollingIntervalSeconds $PollingIntervalSeconds -PerfPollingIntervalMinutes 5) } 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 $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent try { . (Join-Path $scriptDir 'DiscoveryHelpers.ps1') } catch { Write-Error "Failed to load DiscoveryHelpers.ps1: $_" return } try { . (Join-Path $scriptDir 'DiscoveryProvider-CUCM.ps1') } catch { Write-Error "Failed to load DiscoveryProvider-CUCM.ps1: $_" return } $dynDashPath = Join-Path (Split-Path $scriptDir -Parent) 'reports\Export-DynamicDashboardHtml.ps1' if (Test-Path $dynDashPath) { . $dynDashPath } # Prompt for targets interactively if none provided if (-not $Target -and -not $NonInteractive) { Write-Host 'Enter CUCM 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 ' CUCM 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 } } Write-Host '=== CUCM Phone Inventory Discovery ===' -ForegroundColor Cyan Write-Host "Targets: $($Target -join ', ')" -ForegroundColor Cyan Write-Host '' $snmpSettings = Get-CUCMResolvedSnmpSettings -Verbose if (-not $snmpSettings) { Write-Error 'No CUCM SNMP settings available. Exiting.' return } Write-Host 'Walking ccmPhoneTable via WhatsUp Gold SNMP API...' -ForegroundColor Cyan Write-Host " SNMP Version: $($snmpSettings.SnmpVersion) Port: $($snmpSettings.Port) Timeout: $($snmpSettings.TimeoutMs)ms Retries: $($snmpSettings.Retries)" -ForegroundColor DarkGray if ($snmpSettings.SnmpVersion -eq 3) { Write-Host " SNMP v3 User: $($snmpSettings.Username) AuthProto: $($snmpSettings.AuthProtocol) PrivProto: $($snmpSettings.PrivacyProtocol)" -ForegroundColor DarkGray } else { Write-Host " Community: $($snmpSettings.Community)" -ForegroundColor DarkGray } $devicePlan = [ordered]@{} foreach ($targetAddress in $Target) { Write-Host " Walking $targetAddress ..." -ForegroundColor DarkGray -NoNewline try { $phoneRows = @(Get-CUCMPhoneInventory -DeviceName $targetAddress -TargetAddress $targetAddress -Credential $snmpSettings -Verbose) } catch { Write-Host ' FAILED' -ForegroundColor Red Write-Warning "SNMP walk error for ${targetAddress}: $_" $phoneRows = @() } Write-Host " $($phoneRows.Count) row(s)" -ForegroundColor $(if ($phoneRows.Count -gt 0) { 'Green' } else { 'Yellow' }) if ($phoneRows.Count -eq 0) { continue } $summary = Get-CUCMPhoneStatusCounts -PhoneRows $phoneRows $devicePlan[$targetAddress] = [ordered]@{ Name = $targetAddress IP = $targetAddress PhoneRows = $phoneRows Summary = $summary } } if ($devicePlan.Count -eq 0) { Write-Host '' Write-Host '!!! DIAGNOSTIC INFO !!!' -ForegroundColor Yellow Write-Host "Targets: $($Target -join ', ')" -ForegroundColor Yellow Write-Host "SNMP Version: $($snmpSettings.SnmpVersion)" -ForegroundColor Yellow Write-Host "SNMP Port: $($snmpSettings.Port)" -ForegroundColor Yellow Write-Host "SNMP Timeout: $($snmpSettings.TimeoutMs)ms" -ForegroundColor Yellow if ($snmpSettings.SnmpVersion -eq 3) { Write-Host "SNMP v3 Username: $($snmpSettings.Username)" -ForegroundColor Yellow Write-Host "SNMP v3 Context: $($snmpSettings.Context)" -ForegroundColor Yellow Write-Host "SNMP v3 AuthProtocol: $($snmpSettings.AuthProtocol) PrivacyProtocol: $($snmpSettings.PrivacyProtocol)" -ForegroundColor Yellow } else { Write-Host "SNMP v$($snmpSettings.SnmpVersion) Community: $($snmpSettings.Community)" -ForegroundColor Yellow } Write-Host "Re-run with -Verbose flag for detailed SNMP walk diagnostics." -ForegroundColor Yellow Write-Host '' Write-Warning 'No CUCM phone rows were discovered.' return } Write-Host '' Write-Host 'Discovery complete!' -ForegroundColor Green foreach ($device in $devicePlan.Values) { Write-Host " $($device.Name) ($($device.IP)) Phones=$($device.Summary.total) Registered=$($device.Summary.registered) Unregistered=$($device.Summary.unregistered)" -ForegroundColor White } $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 '' Write-Host 'What would you like to do?' -ForegroundColor Cyan Write-Host ' [1] Push CUCM monitor plan to WhatsUp Gold' Write-Host ' [2] Export inventory to JSON file' Write-Host ' [3] Export inventory to CSV file' Write-Host ' [4] Show phone table in console' Write-Host ' [5] Generate HTML dashboards' Write-Host ' [6] Exit (do nothing)' Write-Host ' [7] Dashboard + Push to WUG' Write-Host '' $choice = Read-Host -Prompt 'Choice [1-7]' } if ($choice -eq '7') { $actionsToRun = @('5', '1') } else { $actionsToRun = @($choice) } foreach ($currentChoice in $actionsToRun) { switch ($currentChoice) { '1' { $syncResult = Push-CUCMPlanToWUG -DevicePlan $devicePlan -SnmpSettings $snmpSettings if ($syncResult) { Write-Host '' Write-Host 'WUG sync complete!' -ForegroundColor Green Write-Host " Active monitors created: $($syncResult.ActiveCreated)" -ForegroundColor White Write-Host " Performance monitors created: $($syncResult.PerfCreated)" -ForegroundColor White Write-Host " Assigned to devices: $($syncResult.Assigned)" -ForegroundColor White Write-Host " Skipped: $($syncResult.Skipped)" -ForegroundColor White Write-Host " Attributes updated: $($syncResult.AttrsUpdated)" -ForegroundColor White if ($syncResult.Failed -gt 0) { Write-Host " Failed: $($syncResult.Failed)" -ForegroundColor Red } } } '2' { $jsonPath = Join-Path $OutputDir "CUCM-Inventory-$(Get-Date -Format yyyyMMdd-HHmmss).json" Export-CUCMInventoryJson -Path $jsonPath -DevicePlan $devicePlan Write-Host "JSON exported: $jsonPath" -ForegroundColor Green } '3' { $csvPath = Join-Path $OutputDir "CUCM-Inventory-$(Get-Date -Format yyyyMMdd-HHmmss).csv" $allRows = @() foreach ($device in $devicePlan.Values) { $allRows += @(Get-CUCMDashboardRows -PhoneRows @($device.PhoneRows)) } $allRows | Export-Csv -Path $csvPath -NoTypeInformation Write-Host "CSV exported: $csvPath" -ForegroundColor Green } '4' { foreach ($device in $devicePlan.Values) { Get-CUCMDashboardRows -PhoneRows @($device.PhoneRows) | Select-Object CallManager, PhoneName, Description, Status, Protocol, IPAddress, LastRegistered | Format-Table -AutoSize } } '5' { Write-Host '' Write-Host 'Generating dashboards...' -ForegroundColor Cyan # Aggregate all phone rows $allRows = @() foreach ($device in $devicePlan.Values) { $allRows += @(Get-CUCMDashboardRows -PhoneRows @($device.PhoneRows)) } if ($allRows.Count -eq 0) { Write-Warning 'No phone inventory to export.' break } # Create summary JSON file in output directory $summaryJsonPath = Join-Path $OutputDir 'CUCM-phone-inventory-summary.json' $allRows | ConvertTo-Json -Depth 20 | Out-File -LiteralPath $summaryJsonPath -Encoding UTF8 Write-Host " Summary JSON: $summaryJsonPath ($($allRows.Count) phones)" -ForegroundColor DarkGray # Call dashboard exporter $exporterPath = Join-Path $PSScriptRoot '..\cisco-cucm\Export-CUCM-Dashboard.ps1' if (-not (Test-Path -LiteralPath $exporterPath)) { Write-Warning "Dashboard exporter not found: $exporterPath" } else { & $exporterPath -SummaryDirectory $OutputDir -OutputDirectory $OutputDir } $dashboardFiles = @(Get-ChildItem $OutputDir -Filter 'cucm-dashboard-*.html' -ErrorAction SilentlyContinue) Write-Host "Generated $($dashboardFiles.Count) dashboard(s)" -ForegroundColor Green # Copy to WUG NmConsole if available $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 } 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: $_" } } Deploy-DashboardWebConfig -Path $wugDashDir } } default { Write-Host 'Discovery completed with no output action selected.' -ForegroundColor Yellow } } } # ---- END OF SCRIPT (do not remove this line or the closing braces ab # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDHcoaRbYJCyL4F # dTxw92Kb1+gqUGpTye/t5aQHdvmgAqCCJQ0wggVvMIIEV6ADAgECAhBI/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 # BCBrL9TDCmuGaDNGqs0b/zl7Dxnu/PHk0gydDLJv8/nVojANBgkqhkiG9w0BAQEF # AASCAgBblDsbBPfGkMb6pypTj3HzcraKx73NodvJkWdtgtDy1akzVDe0UhvZxF48 # V19/z3lXC7awkDd7Wabpth8PvriyGgOzerTSd1ywKXiZGJ0zI6AC/XrJKLldXEP+ # ZC+LAfYaMlEnAS2RSflUiHTAAyeNKDnJOQsO+p/F9MTmp/KgotdBj8qpN8xsFbfi # BPh01MDBYHUB5yVtz3H6ZPygiU/vYT3xHlL1BV5PDp0ibajmAsZYz7L+EgGq8R9d # kgS9V4NZN06KTjmG1g/pkWSBs2FRJJdeKVELV23Ws/GVePC9h6QqbOiDLQzBr7As # r3Cr36Cu16k4OaxsjkhaBVraob+r8xVA539TqlVXz4TeHJ5wSsLOtN3kLXgzpXhm # nwvrFryjuuyE4oGVch3kzaP2Og5IXxv0ad8fVcqJV2Sifdc/ut/2sCtveP6kn6CX # VTDQk618I+xVSJHIJoPlfx0YEg0WdFVUPsFtb0gp/e6RNgIjKd7Kt1SlRUTNbgNO # SLXd7Cg7JuhSDiJRjxBe28ziRD+Q7nUZ0NPlQjk+58y3G/7HX+FFNopwbaw9gqT2 # ffpuY7wr5/YRC3UXBQdgTXo/VDzUq9QPjlXDATNxEky5RaqOEE9uuWDm2dgpQVsz # exTtcGTpKXnJVTigSBvpjN82zKUTl9N50SY1J3JnCvoVGmre2aGCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV # dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA3MTQwMDE5MDhaMC8GCSqGSIb3DQEJBDEiBCCNhRMR # D6xiMY72Snx72owNkQoiWrpzjzD+sLRsi0TvrDANBgkqhkiG9w0BAQEFAASCAgDJ # EIIi41kOiCY2urkQVh0Pkxc8LDteGIdCAkh5b4uWUIVTXNJa1IS4RGj6EbSTr3lo # ZqR0BAgFPd7NV7BGkTXPCfyjRTK6aIBPnH6pWJFv+SzNKwy6zvLZgyvn6VDQNFa8 # aPbKZS2st+RMU4x3eus3JRKpSoXfatsLIrVN7ApyFekzvp0eIZ/m1tBhcAtUvCAp # evKU3F9QSvrVGKMsEPHPK9byN4qwrzdcJfOaOGf0qruk2JV/9lS+8zFgjM4w1BTa # xg5yyhtgFtzU72cY5rZP9xmhdZbtSQNvOckeOL5F8Ct4oRSOCS6IsBMA1ig0gIQR # DhZQkmrw+niW0ROXg9TqGEIm2PrEGPOIWjdZfej45C5Pl7kEqGwklAy6foJk/6Kr # G1FJDu0YQV8iSrKxea3yWEgOfoX3YWTHzROBMv08RqJ8Ydotq5iWQGG8k6kIsVgq # nt7CrELvcBBQVuUU5ZfIEW2BjVzJD7iX7bPJRVYZt2aesEXwZkG6HU6yI2PBDEr+ # XS2XzLXwSVQeBa/jNDcfkguOtXQIgge7NByiBJNCYp5fHvKnW2cBDHuWbRcLLhsB # nASVCD0gCUwlGz9hrGyK4upag0+YNToiltb9DskhISEQnh5qoSytmLIIlKbn99/V # Azw/Tp1ITEMntTfJ6TcUuB2WEnyzAd4BYuD2By9zHw== # SIG # End signature block |