3 years ago
SDT Dashboard Widget
I would like to see a dashboard widget that can display upcoming SDTs. We have multiple sites and being able to show upcoming maintenance windows for things like circuits would be useful for onsite IT personnel.
I would like to see a dashboard widget that can display upcoming SDTs. We have multiple sites and being able to show upcoming maintenance windows for things like circuits would be useful for onsite IT personnel.
So what I do today I have a dashboard with bunch of widgets and one of them is generic Text widget. Then I created a custom DataSource which runs every 5 minutes and basically makes 2 API calls to your company's API endpoints. One is to fetch all SDTs, then massage the data, in fact turn it into the HTML table and then the second API call is to post the data to the dashboard based on dashboard id and text widget id. Yeah, perhaps little bit kludgy but it works great.
I've seen this done in several other situations and works well.
@mmatec01 would you mind sharing some sanitized code?
Sure. Here's the code. PowerShell script. Should be self-explanatory.
#Requires -Version 5.1 <# Description - Update-WidgetDS_with_SDT.ps1 PS script to interrogate LogicMonitor REST API and modify target text widget with SDT data #> Set-Strictmode -Version Latest $ErrorActionPreference = 'Stop' $Global:ProgressPreference = 'SilentlyContinue' $Global:VerbosePreference = 'SilentlyContinue' $VerbosePreference = 'Continue' ####################### VARS ########################### # Initialize Variables [string]$accessId = '##LM.ACCESS.ID##' [string]$accessKey = '##LM.ACCESS.KEY##' [string]$company = '##LM.COMPANY##' [string]$hostname = '##SYSTEM.HOSTNAME##' [string]$dashboardId = '##DASHBOARD.ID##' [string]$dashboardWidgetID = '##WIDGET.ID##' $script_name = 'Update-WidgetDS_with_SDT.ps1' $version = '1.2a' [string]$date_formatted = (Get-Date -format "MMddyyyy_HHmm").ToString() [string]$limit = 5000 #5 when debugging [pscustomobject]$API_reply = @{} $ReportSDTs = [System.Collections.Generic.List[PSObject]]::new() $style = "<style>BODY{font-family:Tahoma; font-size:14px;}" $style = $style + "TABLE{border: 1px solid black; border-collapse: collapse; font-size:12px;}" $style = $style + "TH{border-bottom: 1px solid #ddd; background: #dddddd; padding: 5px; height: 50px;}" $style = $style + "TD{border-bottom: 1px solid #ddd; padding: 5px; }" $style = $style + "TR:nth-child(even) {background-color: #F5F5F5;}" $style = $style + "TR:hover {background-color: #00FFFF;}" $style = $style + "</style>" # C# class to create callback $code = @" public class SSLHandler { public static System.Net.Security.RemoteCertificateValidationCallback GetSSLHandler() { return new System.Net.Security.RemoteCertificateValidationCallback((sender, certificate, chain, policyErrors) => { return true; }); } } "@ #compile the class if ("SSLHandler" -as [type]) {} else { Add-Type -TypeDefinition $code } #disable checks using new class [System.Net.ServicePointManager]::ServerCertificateValidationCallback = [SSLHandler]::GetSSLHandler() ####################### FUNCTIONS ########################### Function Get-elapsedseconds { [CmdletBinding()] Param( [Parameter(Mandatory=$True)] [int64]$seconds ) [string]$elapsed = '' [string]$s = '' [string]$col = ':' $ts = [timespan]::fromseconds($seconds) if ($ts.Days) { $elapsed = "$($ts.Days)d" } if ($ts.Hours) { If ($ts.Days) { $elapsed += $col }; $elapsed += "$($ts.Hours)h" } if ($ts.Minutes) { If ($ts.Hours -or $ts.Days) { $elapsed += $col }; $elapsed += "$($ts.Minutes)m" } if ($ts.Seconds) { If ($ts.Days -or $ts.Hours -or $ts.Minutes) { } else { $elapsed = "$($ts.Seconds)s" }} $elapsed } Function Construct-request { [CmdletBinding()] Param( [Parameter(Mandatory=$True)] [string]$resource_passed ) Write-Verbose "Received input: '$resource_passed'" <# Use TLS 1.2 #> [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 <# request details #> $httpVerb = 'GET' $resourcePath = $resource_passed $queryParams = '?offset=0' + "&size=$limit" <# Construct URL #> $url = 'https://' + $company + '.logicmonitor.com/santaba/rest' + $resourcePath + $queryParams <# Get current time in milliseconds #> $epoch = [Math]::Round((New-TimeSpan -start (Get-Date -Date "1/1/1970") -end (Get-Date).ToUniversalTime()).TotalMilliseconds) <# Concatenate Request Details #> $requestVars = $httpVerb + $epoch + $resourcePath <# Construct Signature #> $hmac = New-Object System.Security.Cryptography.HMACSHA256 $hmac.Key = [Text.Encoding]::UTF8.GetBytes($accessKey) $signatureBytes = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($requestVars)) $signatureHex = [System.BitConverter]::ToString($signatureBytes) -replace '-' $signature = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($signatureHex.ToLower())) <# Construct Headers #> $auth = 'LMv1 ' + $accessId + ':' + $signature + ':' + $epoch $headers = @{ "Authorization" = "$auth" "Content-Type" = "application/json" "X-Version" = "2" } <# Make Request #> Try { $response = [System.Collections.Generic.List[Object]]::new() $response = Invoke-RestMethod -Uri $url -Method $httpVerb -Header $headers } Catch { Write-Warning "ERROR getting connected!`t$($_.Exception.Message)!"; return } $response } Function Update-widget { [CmdletBinding()] Param( [Parameter(Mandatory=$True)] [string]$resource_passed, [Parameter(Mandatory=$True)] $data_passed ) <# Use TLS 1.2 #> [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 <# request details #> $httpVerb = 'PUT' $resourcePath = $resource_passed $data = $data_passed $queryParams = '?offset=0' + "&size=$limit" <# Construct URL #> $url = 'https://' + $company + '.logicmonitor.com/santaba/rest' + $resourcePath <# Get current time in milliseconds #> $epoch = [Math]::Round((New-TimeSpan -start (Get-Date -Date "1/1/1970") -end (Get-Date).ToUniversalTime()).TotalMilliseconds) <# Concatenate Request Details #> $requestVars = $httpVerb + $epoch + $data + $resourcePath <# Construct Signature #> $hmac = New-Object System.Security.Cryptography.HMACSHA256 $hmac.Key = [Text.Encoding]::UTF8.GetBytes($accessKey) $signatureBytes = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($requestVars)) $signatureHex = [System.BitConverter]::ToString($signatureBytes) -replace '-' $signature = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($signatureHex.ToLower())) <# Construct Headers #> $auth = 'LMv1 ' + $accessId + ':' + $signature + ':' + $epoch $headers = @{ "Authorization" = "$auth" "Content-Type" = "application/json" "X-Version" = "2" } <# Make Request #> Try { $response = [System.Collections.Generic.List[Object]]::new() $response = Invoke-RestMethod -Uri $url -Method $httpVerb -Body $data -Header $headers } Catch { $ret = @{ "errormsg" = $_.Exception.Message "statuscode" = $_.Exception.Response.StatusCode.value__ "statuscodemsg" = $_.Exception.Response.StatusCode.ToString() "statusmsg" = $_.Exception.Response.StatusDescription } Write-Warning "ERROR updating widget! Error: $($ret.errormsg)!"; exit 21 } $response } ####################### GET REPORT ID AND WIDGET ID ###################### if ([string]::IsNullOrWhiteSpace($dashboardId) -or [string]::IsNullOrWhiteSpace($dashboardWidgetID) -or [string]::IsNullOrWhiteSpace($accessId) -or [string]::IsNullOrWhiteSpace($accessKey) -or [string]::IsNullOrWhiteSpace($company) -or [string]::IsNullOrWhiteSpace($hostname)) { Write-Warning "ERROR getting widget locations!"; exit 1 } Write-Verbose "Got this for widget location: '$dashboardId','$dashboardWidgetID'" ####################### GET DATA ###################### Write-Verbose "Running script '$script_name' version $version." Write-Verbose "Getting all SDTs, please wait..." $API_reply = Construct-request '/sdt/sdts' If ($API_reply) { Write-Verbose "OK, received $($API_reply.total) rows of data" } else { Write-Warning "No reply received from API call!"; exit 2 } ####################### MUNCH DATA ###################### # Build the output data: $ReportSDTs.Clear() @($API_reply.items).ForEach({ #[-2] when debugging $curr_row = $_ $obj1 = [pscustomobject]@{ 'id' = $_.id 'Device' = $(If ($curr_row.PSObject.Properties['deviceDisplayName']) { $curr_row.PSObject.Properties['deviceDisplayName'].value.ToString() } else {''}) 'DataSourceInstance'= $(If ($curr_row.PSObject.Properties['dataSourceInstanceName']) { $curr_row.PSObject.Properties['dataSourceInstanceName'].value.ToString() } else {''}) #'GroupName' = $(If ($curr_row.PSObject.Properties['deviceGroupFullPath']) { $curr_row.PSObject.Properties['deviceGroupFullPath'].value.ToString() } else {''}) # no longer in API v2!!! 'Effective' = $_.isEffective 'Type' = $_.type 'Duration' = Get-elapsedseconds ($_.duration*60) #duration of SDT in minutes! 'StartTime' = $_.startDateTimeOnLocal.ToString() -replace '(?xms) :\d\d \s+','' -replace '(?xms) P(?:S|D)T','' 'EndTime' = $_.endDateTimeOnLocal.ToString() -replace '(?xms) :\d\d \s+','' -replace '(?xms) P(?:S|D)T','' 'SDTtype' = $_.sdtType # 1 -one time, 4 - recurring #'timezone' = $_.timezone # it should be 'America/Los_Angeles' always, so don't even bother to show it 'Admin' = $_.admin # name of luser who created SDT 'Comment' = $_.comment } $ReportSDTs.Add($obj1) >$null }) Write-Verbose "OK, got info for $(@($ReportSDTs).Count) SDTs." ####################### OUTPUT DATA ###################### [string]$content = $ReportSDTs | Sort-Object -Property Device | Select-Object -Property Device,DataSourceInstance,Effective,SDTtype,Type,Duration,StartTime,EndTime,Admin,Comment | ConvertTo-Html -Head $style $obj1 = @{} $obj1 = [ordered]@{ 'type' = "text" 'dashboardId' = $dashboardId 'name' = "SDT report as of: $((Get-Date -format "dddd MM/dd h:mm tt").ToString()). Records found: $(@($ReportSDTs).Count). Enjoy." 'content' = $content } $dataJSONtext = $obj1 |ConvertTo-Json -Depth 6 $reply2 = [System.Collections.Generic.List[PSObject]]::new() $reply2 = Update-widget "/dashboard/widgets/$dashboardWidgetID" $dataJSONtext If (!$reply2) { Write-Warning "No data received updating widget, results unknown!"; exit 3 } Write-Host "html_length=$($content.Length)" Write-Host "rows_returned=$(@($ReportSDTs).Count)" exit 0
Nice, and i like how you have a couple datapoints that you can alert on if anything goes wrong (exit code, html length, rows returned). Really good code.
Wow, thanks a bunch. I'll test this out in my environment and see how it does.
@mmatec01 @Stuart Weenig I'm struggling to implement this custom datasource in our environment, it would be helpful for me if you could elaborate the steps to implement this data-source. :)/emoticons/smile@2x.png 2x" title=":)" width="20" />
Could you share the details how the API call is structured? the output is just showing the below.
GET https://##companyname##.logicmonitor.com/santaba/rest/sdt/sdts?offset=0&size=5000 with 0-byte payload
We have added all the parameters including "accessid" and key but still getting the error getting connected.
You need to populate the following properties on the device the DataSource is applied to:
[string]$accessId = '##LM.ACCESS.ID##'
[string]$accessKey = '##LM.ACCESS.KEY##'
[string]$company = '##LM.COMPANY##'
[string]$hostname = '##SYSTEM.HOSTNAME##'
[string]$dashboardId = '##DASHBOARD.ID##'
[string]$dashboardWidgetID = '##WIDGET.ID##'
Once those are set as properties, the script will actually run with proper values. The fact that your get request still has "##companyname##" indicates that the task doesn't have a value for that property. system.hostname is the only one you don't need to populate manually, that one is added by the system.
You could easily make the appliesto for your datasource something like this:
lm.access.id && lm.access.key && lm.company && dashboard.id && widget.id
That would ensure that if the properties are missing, your applies to would error out, clearly indicating the problem you need to fix.
I would also recommend a best practice of specifying a unique API token/key for this datasource. Using lm.access.id and lm.access.key for every API based DataSource makes it difficult to tell which one might be causing issues when/if you run into issues like reaching your query limit. I prepend the datasource name in front of the credential properties so that each one of my datasources has a unique set of API credentials. When/if there is a failure, only that one datasource goes down and it's clear which api id is the offender.
Actually, we already have put these values directly into the script instead of tokens.
We are wondering what else could be wrong.
@Stuart Weenig Actually, we already have put these values directly into the script instead of tokens.
We are wondering what else could be wrong.