helpers/discovery/DiscoveryProvider-LoadMaster.ps1
|
<#
.SYNOPSIS Kemp LoadMaster discovery provider for infrastructure monitoring. .DESCRIPTION Registers a LoadMaster discovery provider that queries the LoadMaster RESTful APIv2 to discover the appliance, virtual services, sub-virtual services, and real servers, then builds a monitor plan suitable for WhatsUp Gold or standalone use. Hierarchy: LoadMaster (appliance) +-- Virtual Service (VS) +-- Sub-Virtual Service (SubVS) +-- Real Server (RS) Active Monitors (up/down): - Appliance: stats endpoint returns status=ok - VS: showvs returns Status field - RS: showvs returns Rs[].Status field Performance Monitors (stats over time): - Appliance: CPU, Memory, TotalConns, ActiveConns, TPS, SSL_TPS, BytesPerSec - VS: ActiveConns, ConnsPerSec, TotalConns, TotalBytes, BytesRead, BytesWritten - RS: ActiveConns, Conns, Bytes, Pkts Authentication: - API Key (recommended for WUG monitoring) - Basic Auth (bal:password) Prerequisites: 1. LoadMaster API enabled (Certificates & Security > Remote Access) 2. Session Management enabled + Basic Auth disabled (for APIv2) 3. API key generated OR valid bal credentials API Reference: https://loadmasterapiv2.docs.progress.com/ .NOTES Author: Jason Alberino (jason@wug.ninja) Requires: DiscoveryHelpers.ps1 loaded first Encoding: UTF-8 with BOM #> # Ensure DiscoveryHelpers is available if (-not (Get-Command -Name 'Register-DiscoveryProvider' -ErrorAction SilentlyContinue)) { $discoveryPath = Join-Path (Split-Path $MyInvocation.MyCommand.Path -Parent) 'DiscoveryHelpers.ps1' if (Test-Path $discoveryPath) { . $discoveryPath } else { throw "DiscoveryHelpers.ps1 not found. Load it before this provider." } } # ============================================================================ # LoadMaster API Helper # ============================================================================ function Invoke-LoadMasterAPI { <# .SYNOPSIS Calls a LoadMaster APIv2 command via POST to /accessv2. .PARAMETER LMHost LoadMaster hostname or IP. .PARAMETER LMPort API port (default 443). .PARAMETER Command API command name (stats, listvs, showvs, etc.). .PARAMETER Params Additional parameters as a hashtable (e.g. @{ vs = '1' }). .PARAMETER ApiKey API key for authentication. .PARAMETER Credential PSCredential for Basic Auth (bal:password). .PARAMETER IgnoreCertErrors Skip SSL certificate validation. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$LMHost, [int]$LMPort = 443, [Parameter(Mandatory)][string]$Command, [hashtable]$Params, [string]$ApiKey, [PSCredential]$Credential, [bool]$IgnoreCertErrors = $true ) # Enforce TLS 1.2 if ([System.Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 } $uri = "https://${LMHost}:${LMPort}/accessv2" $body = @{ cmd = $Command } if ($ApiKey) { $body['apikey'] = $ApiKey } elseif ($Credential) { $body['apiuser'] = $Credential.UserName $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password) try { $body['apipass'] = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } if ($Params) { foreach ($k in $Params.Keys) { $body[$k] = $Params[$k] } } $jsonBody = $body | ConvertTo-Json -Depth 10 -Compress $irmSplat = @{ Uri = $uri Method = 'POST' Body = $jsonBody ContentType = 'application/json' ErrorAction = 'Stop' } # Handle cert errors — PS 5.1 uses process-wide callback, PS 7+ uses per-request parameter if ($IgnoreCertErrors) { if ($PSVersionTable.PSEdition -eq 'Core') { $irmSplat['SkipCertificateCheck'] = $true } else { # PS 5.1: process-wide SSL bypass via ServerCertificateValidationCallback if (-not $script:_LMCertBypassSet) { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } Write-Warning "SSL certificate validation is disabled for LoadMaster API calls. Use -IgnoreCertErrors `$false to enforce validation." $script:_LMCertBypassSet = $true } } } $resp = Invoke-RestMethod @irmSplat # Some LM commands (showvs, listvs) may return raw JSON string instead of parsed object if ($resp -is [string]) { try { $resp = $resp | ConvertFrom-Json } catch { Write-Verbose "Response was string but not valid JSON" } } if ($resp.code -and [int]$resp.code -ne 200) { throw "LoadMaster API error ($Command): $($resp.message) (code $($resp.code))" } return $resp } # ============================================================================ # LoadMaster Discovery Provider # ============================================================================ Register-DiscoveryProvider -Name 'LoadMaster' ` -MatchAttribute 'DiscoveryHelper.LoadMaster' ` -AuthType 'BasicAuth' ` -DefaultPort 443 ` -DefaultProtocol 'https' ` -IgnoreCertErrors $true ` -DiscoverScript { param($ctx) $items = @() $lmHost = $ctx.DeviceIP $lmPort = if ($ctx.Port) { $ctx.Port } else { 443 } $ignoreCert = if ($ctx.IgnoreCertErrors -eq $false) { $false } else { $true } $ignoreCertStr = if ($ignoreCert) { '1' } else { '0' } # --- Resolve credential --- $apiKey = $null $credential = $null $authMode = 'ApiKey' if ($ctx.Credential) { if ($ctx.Credential -is [string]) { $apiKey = $ctx.Credential } elseif ($ctx.Credential -is [PSCredential]) { $credential = $ctx.Credential $authMode = 'BasicAuth' } elseif ($ctx.Credential.ApiKey) { $apiKey = $ctx.Credential.ApiKey } elseif ($ctx.Credential.UserName -or $ctx.Credential.Username) { $uname = if ($ctx.Credential.UserName) { $ctx.Credential.UserName } else { $ctx.Credential.Username } $pw = $ctx.Credential.Password if ($pw -is [System.Security.SecureString]) { $credential = New-Object System.Management.Automation.PSCredential($uname, $pw) } else { $secPw = ConvertTo-SecureString -String "$pw" -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential($uname, $secPw) } $authMode = 'BasicAuth' } } $apiSplat = @{ LMHost = $lmHost LMPort = $lmPort IgnoreCertErrors = $ignoreCert } if ($apiKey) { $apiSplat['ApiKey'] = $apiKey } if ($credential) { $apiSplat['Credential'] = $credential } # Helper: build monitor URL for WUG polling (GET to /accessv2 with query params) $monBaseUrl = "https://${lmHost}:${lmPort}/accessv2" $authSuffix = '' $useAnonymous = '0' if ($apiKey) { $authSuffix = "&apikey=$([uri]::EscapeDataString($apiKey))" $useAnonymous = '1' } # ================================================================ # Phase 1: System Discovery # ================================================================ Write-Host " Querying LoadMaster system stats..." -ForegroundColor DarkGray $sysStats = $null try { $sysStats = Invoke-LoadMasterAPI @apiSplat -Command 'stats' } catch { Write-Warning "Failed to query system stats: $_" return $items } # Get system info $hostname = '' $version = '' $hamode = 0 $serialNumber = '' try { $infoResp = Invoke-LoadMasterAPI @apiSplat -Command 'getall' if ($infoResp.hostname) { $hostname = "$($infoResp.hostname)" } if ($infoResp.version) { $version = "$($infoResp.version)" } if ($null -ne $infoResp.hamode) { $hamode = [int]$infoResp.hamode } if ($infoResp.serialnumber) { $serialNumber = "$($infoResp.serialnumber)" } } catch { Write-Verbose "Could not fetch system info: $_" # Try individual gets as fallback try { $hostname = "$((Invoke-LoadMasterAPI @apiSplat -Command 'get' -Params @{param='hostname'}).hostname)" } catch { } try { $version = "$((Invoke-LoadMasterAPI @apiSplat -Command 'get' -Params @{param='version'}).version)" } catch { } } if (-not $hostname) { $hostname = $lmHost } Write-Host " Hostname: $hostname" -ForegroundColor DarkGray Write-Host " Version: $version" -ForegroundColor DarkGray Write-Host " Serial: $serialNumber" -ForegroundColor DarkGray Write-Host " HA Mode: $(switch ($hamode) { 0 {'None'} 1 {'Master'} 2 {'Standby'} default {"$hamode"} })" -ForegroundColor DarkGray # --- Helper: extract all numeric properties from a PSObject --- # Returns array of @{ Field; Label; JsonPath; Value } $extractNumericProps = { param([object]$Obj, [string]$JsonPrefix, [string]$LabelPrefix, [string[]]$Skip) $out = @() if ($null -eq $Obj) { return $out } foreach ($p in $Obj.PSObject.Properties) { if ($Skip -contains $p.Name) { continue } $v = $p.Value if ($null -eq $v) { continue } $numVal = $null $isNum = ($v -is [int]) -or ($v -is [long]) -or ($v -is [double]) -or ($v -is [decimal]) if (-not $isNum -and ($v -is [string])) { $isNum = [double]::TryParse($v, [ref]$numVal) } if ($isNum) { if ($null -eq $numVal) { $numVal = [double]$v } $out += @{ Field = $p.Name Label = if ($LabelPrefix) { "${LabelPrefix} $($p.Name)" } else { $p.Name } JsonPath = "${JsonPrefix}.$($p.Name)" Value = $numVal } } } return $out } # ==================================================================== # Discover ALL system metrics from nested stats response # ==================================================================== $systemMetrics = [System.Collections.ArrayList]@() # CPU totals ($.CPU.total.*) if ($sysStats.CPU -and $sysStats.CPU.total) { foreach ($m in (& $extractNumericProps $sysStats.CPU.total '$.CPU.total' 'CPU' @())) { [void]$systemMetrics.Add($m) } } # Memory ($.Memory.*) if ($sysStats.Memory) { foreach ($m in (& $extractNumericProps $sysStats.Memory '$.Memory' 'Mem' @())) { [void]$systemMetrics.Add($m) } } # VS aggregate totals ($.VStotals.*) if ($sysStats.VStotals) { foreach ($m in (& $extractNumericProps $sysStats.VStotals '$.VStotals' 'VSTotals' @())) { [void]$systemMetrics.Add($m) } } # TPS ($.TPS.*) if ($sysStats.TPS) { foreach ($m in (& $extractNumericProps $sysStats.TPS '$.TPS' 'TPS' @())) { [void]$systemMetrics.Add($m) } } # Client limits ($.ClientLimits.Totals.*) if ($sysStats.ClientLimits -and $sysStats.ClientLimits.Totals) { foreach ($m in (& $extractNumericProps $sysStats.ClientLimits.Totals '$.ClientLimits.Totals' 'Clients' @())) { [void]$systemMetrics.Add($m) } } # Network interfaces ($.Network.Interface[i].*) if ($sysStats.Network -and $sysStats.Network.Interface) { $ifaces = @($sysStats.Network.Interface) for ($ni = 0; $ni -lt $ifaces.Count; $ni++) { $iface = $ifaces[$ni] $ifName = if ($iface.Name) { "$($iface.Name)" } else { "if${ni}" } $ifPrefix = '$.Network.Interface[' + $ni + ']' foreach ($m in (& $extractNumericProps $iface $ifPrefix "Net ${ifName}" @('Name', 'ifaceID'))) { [void]$systemMetrics.Add($m) } } } # Disk partitions ($.DiskUsage.partition[i].*) if ($sysStats.DiskUsage -and $sysStats.DiskUsage.partition) { $parts = @($sysStats.DiskUsage.partition) for ($di = 0; $di -lt $parts.Count; $di++) { $part = $parts[$di] $partName = if ($part.name) { "$($part.name)" } else { "disk${di}" } $diskPrefix = '$.DiskUsage.partition[' + $di + ']' foreach ($m in (& $extractNumericProps $part $diskPrefix "Disk ${partName}" @('name'))) { [void]$systemMetrics.Add($m) } } } Write-Host " System metrics: $($systemMetrics.Count) available" -ForegroundColor DarkGray # ---- Pre-build VS and RS stats maps from stats response ---- $vsStatsMap = @{} # Key: VS Index string -> @{ ArrayIdx; Entry; Metrics } if ($sysStats.Vs) { $vsArr = @($sysStats.Vs) for ($vi = 0; $vi -lt $vsArr.Count; $vi++) { $vsE = $vsArr[$vi] $vIdx = "$($vsE.Index)" $vsPrefix = '$.Vs[' + $vi + ']' $vMetrics = & $extractNumericProps $vsE $vsPrefix '' @('Index', 'ErrorCode', 'Enable', 'WafEnable', 'InterceptMode') $vsStatsMap[$vIdx] = @{ ArrayIdx = $vi; Entry = $vsE; Metrics = $vMetrics } } } $rsStatsMap = @{} # Key: "VSIndex:RSIndex" -> @{ ArrayIdx; Entry; Metrics } if ($sysStats.Rs) { $rsArr = @($sysStats.Rs) for ($rsi = 0; $rsi -lt $rsArr.Count; $rsi++) { $rsE = $rsArr[$rsi] $rsKey = "$($rsE.VSIndex):$($rsE.RSIndex)" $rsPrefix = '$.Rs[' + $rsi + ']' $rMetrics = & $extractNumericProps $rsE $rsPrefix '' @('VSIndex', 'RSIndex', 'Enable', 'Persist', 'Port') $rsStatsMap[$rsKey] = @{ ArrayIdx = $rsi; Entry = $rsE; Metrics = $rMetrics } } } Write-Host " VS stats entries: $($vsStatsMap.Count) | RS stats entries: $($rsStatsMap.Count)" -ForegroundColor DarkGray # --- Appliance monitor URL --- $statsUrl = "${monBaseUrl}?cmd=stats${authSuffix}" $applianceAttrs = @{ 'LoadMaster.Hostname' = $hostname 'LoadMaster.Version' = $version 'LoadMaster.Serial' = $serialNumber 'LoadMaster.HAMode' = "$hamode" 'LoadMaster.ApiHost' = $lmHost 'LoadMaster.ApiPort' = "$lmPort" 'LoadMaster.AuthMode' = $authMode 'LoadMaster.DeviceType' = 'Appliance' 'LoadMaster.MetricCount' = "$($systemMetrics.Count)" } # --- Appliance active monitor (health check) --- $items += New-DiscoveredItem ` -Name "LM Health [$hostname]" ` -ItemType 'ActiveMonitor' ` -MonitorType 'RestApi' ` -MonitorParams @{ RestApiUrl = $statsUrl RestApiMethod = 'GET' RestApiTimeoutMs = '10000' RestApiIgnoreCertErrors = $ignoreCertStr RestApiUseAnonymous = $useAnonymous RestApiDownIfResponseCodeIsIn = '[400,401,403,404,500,502,503]' RestApiComparisonList = '[]' } ` -UniqueKey "LM:${lmHost}:active:health" ` -DeviceId 0 ` -Attributes $applianceAttrs ` -Tags @('loadmaster', 'appliance', 'active') # --- Appliance perf monitors (ALL discovered system metrics) --- foreach ($m in $systemMetrics) { $items += New-DiscoveredItem ` -Name "LM $($m.Label) [$hostname]" ` -ItemType 'PerformanceMonitor' ` -MonitorType 'RestApi' ` -MonitorParams @{ RestApiUrl = $statsUrl RestApiJsonPath = $m.JsonPath RestApiHttpMethod = 'GET' RestApiHttpTimeoutMs = '10000' RestApiIgnoreCertErrors = $ignoreCertStr RestApiUseAnonymousAccess = $useAnonymous } ` -UniqueKey "LM:${lmHost}:perf:sys:$($m.JsonPath)" ` -DeviceId 0 ` -Tags @('loadmaster', 'appliance', 'performance', $m.Field.ToLower()) } # ================================================================ # Phase 2: Virtual Service Discovery # ================================================================ Write-Host " Listing virtual services..." -ForegroundColor DarkGray $vsList = @() try { $vsResp = Invoke-LoadMasterAPI @apiSplat -Command 'listvs' if ($vsResp.VS) { $vsList = @($vsResp.VS) } } catch { Write-Warning "Failed to list virtual services: $_" } Write-Host " Virtual services: $($vsList.Count)" -ForegroundColor DarkGray # ================================================================ # Phase 3: VS + RS Drill-Down # ================================================================ $vsDeviceIdx = 1 $rsDeviceIdx = 1000 foreach ($vs in $vsList) { $vsIndex = "$($vs.Index)" $vsNick = if ($vs.NickName) { "$($vs.NickName)" } else { '' } $vsAddr = if ($vs.VSAddress) { "$($vs.VSAddress)" } else { '' } $vsPort = if ($vs.VSPort) { "$($vs.VSPort)" } else { '' } $vsProto = if ($vs.VSProtocol) { "$($vs.VSProtocol)" } else { '' } $vsStatus = if ($vs.Status) { "$($vs.Status)" } else { 'Unknown' } $vsEnable = if ($vs.Enable) { "$($vs.Enable)" } else { '' } $vsLabel = if ($vsNick) { $vsNick } else { "${vsAddr}:${vsPort}" } Write-Host " VS ${vsIndex}: $vsLabel (${vsAddr}:${vsPort} $vsProto) - $vsStatus" -ForegroundColor DarkGray # --- Get VS stats from the pre-built stats map --- $vsStatsEntry = $vsStatsMap[$vsIndex] $vsMetrics = if ($vsStatsEntry) { @($vsStatsEntry.Metrics) } else { @() } Write-Host " Stats metrics: $($vsMetrics.Count)" -ForegroundColor DarkGray # --- Get RS list from showvs (now properly parsed) --- $rsArray = @() $subVSArray = @() try { $vsDetail = Invoke-LoadMasterAPI @apiSplat -Command 'showvs' -Params @{ vs = $vsIndex } # showvs response has Rs at root level (no .VS wrapper) if ($vsDetail.Rs) { $rsArray = @($vsDetail.Rs) } if ($vsDetail.SubVS) { $subVSArray = @($vsDetail.SubVS) } } catch { Write-Verbose "Could not get details for VS ${vsIndex}: $_" } # --- VS monitor URL (showvs for health check) --- $vsShowUrl = "${monBaseUrl}?cmd=showvs&vs=${vsIndex}${authSuffix}" # --- VS device attributes --- $vsAttrs = @{ 'LoadMaster.DeviceType' = 'VirtualService' 'LoadMaster.VSIndex' = $vsIndex 'LoadMaster.VSAddress' = $vsAddr 'LoadMaster.VSPort' = $vsPort 'LoadMaster.VSProtocol' = $vsProto 'LoadMaster.VSNickName' = $vsNick 'LoadMaster.VSStatus' = $vsStatus 'LoadMaster.VSEnable' = $vsEnable 'LoadMaster.ApiHost' = $lmHost 'LoadMaster.ApiPort' = "$lmPort" 'LoadMaster.ParentAppliance' = $hostname 'LoadMaster.RSCount' = "$($rsArray.Count)" 'LoadMaster.SubVSCount' = "$($subVSArray.Count)" 'LoadMaster.MetricCount' = "$($vsMetrics.Count)" } # --- VS active monitor --- $items += New-DiscoveredItem ` -Name "LM VS Health [$vsLabel]" ` -ItemType 'ActiveMonitor' ` -MonitorType 'RestApi' ` -MonitorParams @{ RestApiUrl = $vsShowUrl RestApiMethod = 'GET' RestApiTimeoutMs = '10000' RestApiIgnoreCertErrors = $ignoreCertStr RestApiUseAnonymous = $useAnonymous RestApiDownIfResponseCodeIsIn = '[400,401,403,404,500,502,503]' RestApiComparisonList = '[]' } ` -UniqueKey "LM:${lmHost}:active:vs:${vsIndex}" ` -DeviceId $vsDeviceIdx ` -Attributes $vsAttrs ` -Tags @('loadmaster', 'vs', 'active') # --- VS perf monitors (from stats.Vs[] via stats endpoint) --- foreach ($m in $vsMetrics) { $items += New-DiscoveredItem ` -Name "LM VS $($m.Label) [$vsLabel]" ` -ItemType 'PerformanceMonitor' ` -MonitorType 'RestApi' ` -MonitorParams @{ RestApiUrl = $statsUrl RestApiJsonPath = $m.JsonPath RestApiHttpMethod = 'GET' RestApiHttpTimeoutMs = '10000' RestApiIgnoreCertErrors = $ignoreCertStr RestApiUseAnonymousAccess = $useAnonymous } ` -UniqueKey "LM:${lmHost}:perf:vs:${vsIndex}:$($m.Field)" ` -DeviceId $vsDeviceIdx ` -Tags @('loadmaster', 'vs', 'performance', $m.Field.ToLower()) } $vsDeviceIdx++ # ============================================================ # Phase 3a: SubVS Drill-Down (if present) # ============================================================ if ($subVSArray.Count -gt 0) { Write-Host " SubVS: $($subVSArray.Count)" -ForegroundColor DarkGray foreach ($subVS in $subVSArray) { $subVSIndex = "$($subVS.VSIndex)" if (-not $subVSIndex -and $subVS.Index) { $subVSIndex = "$($subVS.Index)" } if (-not $subVSIndex) { continue } $subVSLabel = if ($subVS.NickName) { "$($subVS.NickName)" } else { "SubVS-$subVSIndex" } # Get SubVS stats from stats map (SubVS have their own Index) $subVSStats = $vsStatsMap[$subVSIndex] $subVSMetrics = if ($subVSStats) { @($subVSStats.Metrics) } else { @() } $subVSShowUrl = "${monBaseUrl}?cmd=showvs&vs=${subVSIndex}${authSuffix}" $subVSAttrs = @{ 'LoadMaster.DeviceType' = 'SubVirtualService' 'LoadMaster.VSIndex' = $subVSIndex 'LoadMaster.ParentVSIndex' = $vsIndex 'LoadMaster.ParentVSName' = $vsLabel 'LoadMaster.ApiHost' = $lmHost 'LoadMaster.ApiPort' = "$lmPort" 'LoadMaster.ParentAppliance' = $hostname 'LoadMaster.MetricCount' = "$($subVSMetrics.Count)" } # SubVS active monitor $items += New-DiscoveredItem ` -Name "LM SubVS Health [$subVSLabel]" ` -ItemType 'ActiveMonitor' ` -MonitorType 'RestApi' ` -MonitorParams @{ RestApiUrl = $subVSShowUrl RestApiMethod = 'GET' RestApiTimeoutMs = '10000' RestApiIgnoreCertErrors = $ignoreCertStr RestApiUseAnonymous = $useAnonymous RestApiDownIfResponseCodeIsIn = '[400,401,403,404,500,502,503]' RestApiComparisonList = '[]' } ` -UniqueKey "LM:${lmHost}:active:subvs:${subVSIndex}" ` -DeviceId $vsDeviceIdx ` -Attributes $subVSAttrs ` -Tags @('loadmaster', 'subvs', 'active') # SubVS perf monitors (from stats.Vs[] via stats endpoint) foreach ($m in $subVSMetrics) { $items += New-DiscoveredItem ` -Name "LM SubVS $($m.Label) [$subVSLabel]" ` -ItemType 'PerformanceMonitor' ` -MonitorType 'RestApi' ` -MonitorParams @{ RestApiUrl = $statsUrl RestApiJsonPath = $m.JsonPath RestApiHttpMethod = 'GET' RestApiHttpTimeoutMs = '10000' RestApiIgnoreCertErrors = $ignoreCertStr RestApiUseAnonymousAccess = $useAnonymous } ` -UniqueKey "LM:${lmHost}:perf:subvs:${subVSIndex}:$($m.Field)" ` -DeviceId $vsDeviceIdx ` -Tags @('loadmaster', 'subvs', 'performance', $m.Field.ToLower()) } $vsDeviceIdx++ } } # ============================================================ # Phase 3b: Real Server Drill-Down # ============================================================ if ($rsArray.Count -gt 0) { Write-Host " RS: $($rsArray.Count)" -ForegroundColor DarkGray } foreach ($rs in $rsArray) { $rsAddr = if ($rs.Addr) { "$($rs.Addr)" } else { '' } $rsPort = if ($rs.Port) { "$($rs.Port)" } else { '' } $rsStatus = if ($rs.Status) { "$($rs.Status)" } else { 'Unknown' } $rsWeight = if ($null -ne $rs.Weight) { "$($rs.Weight)" } else { '' } $rsLabel = "${rsAddr}:${rsPort}" # Match RS to stats.Rs[] using VSIndex:RSIndex $rsVSIdx = if ($rs.VSIndex) { "$($rs.VSIndex)" } else { $vsIndex } $rsRSIdx = if ($rs.RsIndex) { "$($rs.RsIndex)" } else { if ($rs.RSIndex) { "$($rs.RSIndex)" } else { '' } } $rsKey = "${rsVSIdx}:${rsRSIdx}" $rsStatsEntry = $rsStatsMap[$rsKey] $rsMetrics = if ($rsStatsEntry) { @($rsStatsEntry.Metrics) } else { @() } Write-Host " RS: $rsLabel ($rsStatus) Weight=$rsWeight Metrics=$($rsMetrics.Count)" -ForegroundColor DarkGray $rsAttrs = @{ 'LoadMaster.DeviceType' = 'RealServer' 'LoadMaster.RSAddress' = $rsAddr 'LoadMaster.RSPort' = $rsPort 'LoadMaster.RSStatus' = $rsStatus 'LoadMaster.RSWeight' = $rsWeight 'LoadMaster.RSIndex' = $rsRSIdx 'LoadMaster.ParentVSIndex' = $vsIndex 'LoadMaster.ParentVSName' = $vsLabel 'LoadMaster.ApiHost' = $lmHost 'LoadMaster.ApiPort' = "$lmPort" 'LoadMaster.ParentAppliance' = $hostname 'LoadMaster.MetricCount' = "$($rsMetrics.Count)" } # RS perf monitors (from stats.Rs[] via stats endpoint) foreach ($m in $rsMetrics) { $items += New-DiscoveredItem ` -Name "LM RS $($m.Label) [$rsLabel on $vsLabel]" ` -ItemType 'PerformanceMonitor' ` -MonitorType 'RestApi' ` -MonitorParams @{ RestApiUrl = $statsUrl RestApiJsonPath = $m.JsonPath RestApiHttpMethod = 'GET' RestApiHttpTimeoutMs = '10000' RestApiIgnoreCertErrors = $ignoreCertStr RestApiUseAnonymousAccess = $useAnonymous } ` -UniqueKey "LM:${lmHost}:perf:rs:${rsVSIdx}:${rsRSIdx}:$($m.Field)" ` -DeviceId $rsDeviceIdx ` -Tags @('loadmaster', 'rs', 'performance', $m.Field.ToLower()) } $rsDeviceIdx++ } } # ================================================================ # Summary # ================================================================ $activeCount = @($items | Where-Object { $_.ItemType -eq 'ActiveMonitor' }).Count $perfCount = @($items | Where-Object { $_.ItemType -eq 'PerformanceMonitor' }).Count $rsTotal = @($rsStatsMap.Keys).Count Write-Host " Discovery complete: $activeCount active monitors, $perfCount perf monitors" -ForegroundColor Green Write-Host " Hierarchy: 1 appliance, $($vsList.Count) VS, $rsTotal RS" -ForegroundColor DarkGray return $items } # ============================================================================ # LoadMaster Discovery Dashboard Export # ============================================================================ function Export-LoadMasterDashboardHtml { <# .SYNOPSIS Generates a LoadMaster discovery dashboard from live API data. .PARAMETER LMHost LoadMaster hostname or IP. .PARAMETER LMPort API port (default 443). .PARAMETER ApiKey API key for authentication. .PARAMETER Credential PSCredential for Basic Auth. .PARAMETER OutputPath Path for the generated HTML file. .PARAMETER Title Dashboard title. .PARAMETER IgnoreCertErrors Skip SSL validation (default $true). #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$LMHost, [int]$LMPort = 443, [string]$ApiKey, [PSCredential]$Credential, [Parameter(Mandatory)][string]$OutputPath, [string]$Title = 'LoadMaster Discovery Dashboard', [bool]$IgnoreCertErrors = $true ) $apiSplat = @{ LMHost = $LMHost LMPort = $LMPort IgnoreCertErrors = $IgnoreCertErrors } if ($ApiKey) { $apiSplat['ApiKey'] = $ApiKey } if ($Credential) { $apiSplat['Credential'] = $Credential } # Gather live data $rows = @() # System stats try { $stats = Invoke-LoadMasterAPI @apiSplat -Command 'stats' $hostname = '' try { $hostname = "$((Invoke-LoadMasterAPI @apiSplat -Command 'get' -Params @{param='hostname'}).hostname)" } catch { $hostname = $LMHost } $version = '' try { $version = "$((Invoke-LoadMasterAPI @apiSplat -Command 'get' -Params @{param='version'}).version)" } catch { } # Extract nested stats values $cpuUser = if ($stats.CPU -and $stats.CPU.total) { "$($stats.CPU.total.User)%" } else { 'N/A' } $memInfo = if ($stats.Memory) { "$($stats.Memory.MBused)/$($stats.Memory.MBtotal) MB ($($stats.Memory.percentmemused)%)" } else { 'N/A' } $vsActiveConns = if ($stats.VStotals -and $null -ne $stats.VStotals.TotalConns) { "$($stats.VStotals.TotalConns)" } else { 'N/A' } $tpsTotal = if ($stats.TPS) { "$($stats.TPS.Total)" } else { 'N/A' } $tpsSSL = if ($stats.TPS) { "$($stats.TPS.SSL)" } else { 'N/A' } $bytesPerSec = if ($stats.VStotals -and $null -ne $stats.VStotals.BytesPerSec) { "$($stats.VStotals.BytesPerSec)" } else { 'N/A' } $rows += [PSCustomObject]@{ Type = 'Appliance' Name = $hostname Address = $LMHost Port = "$LMPort" Status = 'Up' Version = $version CPU = $cpuUser Memory = $memInfo ActiveConns = $vsActiveConns TotalConns = if ($stats.VStotals) { "$($stats.VStotals.TotalConns)" } else { 'N/A' } TPS = $tpsTotal SSL_TPS = $tpsSSL BytesPerSec = $bytesPerSec ParentVS = '' Weight = '' } } catch { Write-Warning "Failed to get system stats for dashboard: $_" } # VS + RS data try { $vsResp = Invoke-LoadMasterAPI @apiSplat -Command 'listvs' if ($vsResp.VS) { foreach ($vs in @($vsResp.VS)) { $vsIndex = "$($vs.Index)" $vsNick = if ($vs.NickName) { "$($vs.NickName)" } else { '' } $vsAddr = if ($vs.VSAddress) { "$($vs.VSAddress)" } else { '' } $vsPort = if ($vs.VSPort) { "$($vs.VSPort)" } else { '' } $vsLabel = if ($vsNick) { $vsNick } else { "${vsAddr}:${vsPort}" } $vsStatus = if ($vs.Status) { "$($vs.Status)" } else { 'Unknown' } $rows += [PSCustomObject]@{ Type = 'Virtual Service' Name = $vsLabel Address = $vsAddr Port = $vsPort Status = $vsStatus Version = '' CPU = '' Memory = '' ActiveConns = if ($null -ne $vs.ActiveConns) { "$($vs.ActiveConns)" } else { 'N/A' } TotalConns = if ($null -ne $vs.TotalConns) { "$($vs.TotalConns)" } else { 'N/A' } TPS = if ($null -ne $vs.ConnsPerSec) { "$($vs.ConnsPerSec)" } else { 'N/A' } SSL_TPS = '' BytesPerSec = '' ParentVS = '' Weight = '' } # Get RS for this VS try { $vsDetail = Invoke-LoadMasterAPI @apiSplat -Command 'showvs' -Params @{ vs = $vsIndex } if ($vsDetail.Rs) { foreach ($rs in @($vsDetail.Rs)) { $rsAddr = if ($rs.Addr) { "$($rs.Addr)" } else { '' } $rsPort = if ($rs.Port) { "$($rs.Port)" } else { '' } $rsStatus = if ($rs.Status) { "$($rs.Status)" } else { 'Unknown' } $rsWeight = if ($null -ne $rs.Weight) { "$($rs.Weight)" } else { '' } $rows += [PSCustomObject]@{ Type = 'Real Server' Name = "${rsAddr}:${rsPort}" Address = $rsAddr Port = $rsPort Status = $rsStatus Version = '' CPU = '' Memory = '' ActiveConns = if ($null -ne $rs.ActiveConns) { "$($rs.ActiveConns)" } else { 'N/A' } TotalConns = if ($null -ne $rs.Conns) { "$($rs.Conns)" } else { 'N/A' } TPS = '' SSL_TPS = '' BytesPerSec = '' ParentVS = $vsLabel Weight = $rsWeight } } } } catch { Write-Verbose "Could not get RS for VS ${vsIndex}: $_" } } } } catch { Write-Warning "Failed to get VS data for dashboard: $_" } # Generate HTML using dynamic dashboard if available $dynDashFunc = Get-Command -Name 'Export-DynamicDashboardHtml' -ErrorAction SilentlyContinue if ($dynDashFunc) { Export-DynamicDashboardHtml -Data $rows ` -ReportTitle $Title ` -OutputPath $OutputPath ` -StatusField 'Status' Write-Host " Dashboard saved: $OutputPath" -ForegroundColor Green } else { # Fallback: simple HTML table $htmlRows = $rows | ConvertTo-Html -Fragment $html = @" <!DOCTYPE html> <html><head><title>$Title</title> <style>body{font-family:Arial,sans-serif;margin:20px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:8px;text-align:left}th{background:#4a90d9;color:#fff}tr:nth-child(even){background:#f2f2f2}</style> </head><body><h1>$Title</h1><p>Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</p> $htmlRows </body></html> "@ $html | Out-File -FilePath $OutputPath -Encoding utf8 -Force Write-Host " Dashboard saved: $OutputPath" -ForegroundColor Green } } # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBr8anwMzAli2vf # Mul8DO3yQS/PaMI4ZZSgpkNmJhBefqCCJQ0wggVvMIIEV6ADAgECAhBI/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 # BCA/N/TMxh5JxaVSm4k/OUoZkLK2CdBLoUf4AojegUCFrjANBgkqhkiG9w0BAQEF # AASCAgBtnuY78kA/V3D6l9FWMbKJcccRd/GayZt9IGnjH1gwUqUTIKnchF+6V4dB # +t8JfeLiLBcearV59K82d67bV00fo6bsuA9McTxYjIYNBCH4RTTB2M1KfoK5uU+n # 9sFnCbSYTOCsYLK0DoTUW3p6yHIUTXWzq/3BTMMvGq4inz/vbmo5mShTGa78R3M1 # 5fJrXySFOWznI5fiwYOnOVJAf4kvrpLq2wyvih7s/wI/pLbodFseIwnA+0iaUIA3 # ZZNf22Rsa2Glb7YK9MW+M8GR2Mj2Fq1IOBL/KCZ92EDjGetKn2KH0YVpORNaNTp3 # m7+YCoJeVu+9CUWiCl/96vKNhGTYvBUScV6xmmIiIUnKhV86Lp709IBXoCKQ4SrV # 7bAajGdOKv5nuMTQD/+M7bjWKXqTarKma6i6cs/yBw8yLBfWJrnbie67ouCEKY/p # oraZjrQp+lES2/L1vbHGgAZ7wnaUTWQarH8S2gIiTIoLqv6ZbdUQE4NUoPRsOc6W # +ClhNvRvTxu8rBjAjDZ2vTQva7+ciDcxru0F6ro4b1PCmOe6bfbEJSUUojph9Buk # WWvE5yi/K4Z1J3zAnfg7JfAp9srSqrEesneVdqFaScu+l5h/SqJ3RTVtCOnNqS/7 # sXIfV5UJIpAmDED+1kZe1VsHkb962M85uNmnMhAf1t6UPdd986GCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV # dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA1MjQxNzQwNTRaMC8GCSqGSIb3DQEJBDEiBCDwBucj # PzvfZcqadS4xjl290gDDmN8GujDYPG7myDw3ezANBgkqhkiG9w0BAQEFAASCAgAp # DR0nMB8iII8m6dUeymLasZOQ2DxXz0GxqVqZYxlTjGQJgSCMRQqLEwZnninDivH2 # 7dj/8+VpPjYpoywNennJcnpLZA9eCGvGnW++lW6OuL14vJ87UKF62DTEEBucE5ki # 5V8/tC2BWte+9SnTDlJCgQz3sd8kWwI1wUF7U1iK2NQp8w9QKU6MRUT/0xvpODHE # 0CASH35KlsIwvyo4DC1Tr1NN6eQBL/fSPci4kOhfwIs1337ka1+4cr8smR3I/YsN # XHGsnhVQr2q1UgHynSxXTKsfqZZcE/Vw83E8KEInD/bWVA3onbMqj+IpJIPkzXaY # OMq+/KMviHB2lqg0L+Q7xkZKWdwYdo3uILEhhjHBGV4xjv3gFcU9h/K3cjtU5Zin # OZnLnGzdiN2gXjdQkMa9yOb7Y1RC3bwCEUk5Ev4B6pt2oAoI3Xd+nGVAaRgN5cxD # OOi6i6Hy+opGyVFsd9CrqPH1/Cy/+bmPZfenjavfoH4Qx8zws8gMP1UQJf/RkHyO # hkjrd46xpzQTReGTNVaGcug3ejdSdPjf3k3tGBrzj0pC8vrl/32P4Xyh46icpPgy # qiUXs/0m+x30itGslEBSuMprgqdWAENRRKBaCKCZRVjvLIyrb3GE+lT21pVmnC+I # wErPuQWVuW5Y9J064Ok+wwP6Usydh32TbcCbNvugfw== # SIG # End signature block |