Forum Discussion

Cole_McDonald's avatar
Cole_McDonald
Icon for Professor rankProfessor
7 months ago
Solved

Creating a propertySource to populate a NOC widget in a dashboard... need ## in a string.

The NOC widget items have a field that requires me to have the string "##RESOURCEGROUP##" pushed through the JSON into the NOC Item… since I’m using a propertySource to run the script on a schedule (I have a larger VM with a collector with a longer script timeout just for doing deeper scripted work through the API or Full Domain sweep types of things that will take more time), The LM System is going to try to replace that at run time rather than returning the explicit string.

Who knows the correct escape sequence for turning that into a string literal on its way into the RestApi Patch?

Scripting questions through support is best effort, and I don’t usually come with easy questions.

5 Replies

  • Changed it to a dataSource with datapoints of execution time and exitCode… I may add a count of the client folders added.

  • Some testing shows that backticks are the winners:

    “testoutput=`#`#RESOURCEGROUP`#`#”

    shows ##RESOURCEGROUP## in the output window.

  • <#
    ------------------------------
    LogicMonitor Subgroup NOC Item population
    for NOC Widget Dashboards
    ------------------------------
    Created by Cole McDonald (Netgain Technologies, LLC)
    17 October 2023
    ------------------------------
    Netgain and the Author are not responsible for any issues this may occur in your environment due
    to the use of this script in whole or part. Please retain this comment block in your deployment.
    ------------------------------
    This DS should have an appliesTo() that targets a single collector
    The collector should have its script timeouts increased to account for
    longer run times reaching out through the web to the API.

    You can have this update multiple dashboards as it's grabbing all of the API data necessary,
    minimizing the # of times you have to grab that data will reduce resource consumption on the collector.
    ------------------------------
    #>

    # Add your logicmonitor portal company name for the URL
    $company = ""

    $URLBase = "https://$company.logicmonitor.com/santaba/rest"

    # These properties are defined somewhere that can be picked up by the device targeted by the appliesTo()
    $accessID = "##ApiAccessID.key##"
    $accessKey = "##ApiAccessKey.key##"

    # This is the ID (integer) of the group containing the subgroups you want displayed.
    # This needs to be changed to target based on name/path
    $parentGroupID = 0

    # The "name" property is the name of the Dashboard being targeted.
    #!!! If you have more than one dashboard to update, duplicate this and
    #!!! change the $masterdash and name. If you have questions on this,
    #!!! reach out on the LM Community and I can show you what I've done.
    $masterDash = @{ id=0; widgetid=0; name="NOC - Master" }

    #region Initialization and Functions
    #-------- The Functions ----------
    function generateJSON {
    param(
    $dashInfo,
    $clientnames,
    $deviceDisplayName = "*",
    $DSDisplayName = "*"
    )

    $itemArray = @()

    foreach ($name in $clientnames) {
    $itemArray += @{
    "type" = "device"
    "deviceGroupFullPath" = "Clients/$name"
    "deviceDisplayName" = $deviceDisplayName
    "dataSourceDisplayName" = $DSDisplayName
    "instanceName" = "*"
    "dataPointName" = "*"
    "groupBy" = "deviceGroup"
    "name" = "`#`#RESOURCEGROUP`#`#"
    }
    }

    # Write JSON back to the API for that widget
    $outputJSON = "`n`t{`n`t`t`"items`" : [`n"

    foreach ($item in $itemArray) {
    $elementJSON = @"
    {
    `"type`" : `"$($item.type)`",
    `"dataPointName`" : `"$($item.dataPointName)`",
    `"instanceName`" : `"$($item.instanceName)`",
    `"name`" : `"$($item.name)`",
    `"dataSourceDisplayName`" : `"$($item.dataSourceDisplayName)`",
    `"groupBy`" : `"$($item.groupBy)`",
    `"deviceGroupFullPath`" : `"$($item.deviceGroupFullPath)`",
    `"deviceDisplayName`" : `"$($item.deviceDisplayName)`"
    }
    "@
    if ($item -ne $itemArray[-1]) {
    $outputJSON += "$elementJSON,`n"
    } else {
    # Last Item
    $outputJSON += "$elementJSON`n`t`t]`n`t}"
    }
    }

    write-output $outputJSON
    }

    function Send-Request {
    param (
    $cred ,
    $URL ,
    $accessid = $null,
    $accesskey = $null,
    $data = $null,
    $version = '3' ,
    $httpVerb = "GET"
    )
    if ( $accessId -eq $null) { exit 1 }

    <# Use TLS 1.2 #>
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    <# 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 = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
    $headers.Add( "Authorization", $auth )
    $headers.Add( "Content-Type" , 'application/json' )
    # uses version 2 of the API
    $headers.Add( "X-version" , $version )

    <# Make Request #>
    $response = Invoke-RestMethod `
    -Uri $URL `
    -Method $httpVerb `
    -Body $data `
    -Header $headers `
    -erroraction SilentlyContinue `
    -warningaction SilentlyContinue

    Return $response
    }
    function Get-LMRestAPIObjectListing {
    param (
    $URLBase ,
    $resourcePathRoot , # "/device/devices"
    $size = 1000 ,
    $accessKey ,
    $accessId ,
    $version = '2'
    )

    $output = @()
    $looping = $true
    $counter = 0

    while ($looping) {
    #re-calc offset based on iteration
    $offset = $counter * $size
    $resourcePath = $resourcePathRoot
    $queryParam = "?size=$size&offset=$offset"
    $url = $URLBase + $resourcePath + $queryParam
    # Make Request
    $response = Send-Request `
    -accesskey $accessKey `
    -accessid $accessId `
    -URL $url `
    -version $version
    if ( $response.items.count -eq $size ) {
    # Return set is full, more items to retrieve
    $output += $response.items
    $counter++
    } elseif ( $response.items.count -gt 0 ) {
    # Return set is not full, store date, end loop
    $output += $response.items
    $looping = $false
    } else {
    # Return set is empty, no data to store, end loop
    $looping = $false
    }
    }

    write-output $output
    }
    #endregion

    # This could be more concise a grab with stronger filters,
    # that's a future problem, this works and isn't terribly slow

    #region Get Dashboards
    $resourcePath = "/dashboard/dashboards"
    $dashboards = Get-LMRestAPIObjectListing `
    -resourcePathRoot $resourcePath `
    -accessKey $accessKey `
    -accessId $accessID `
    -URLBase $URLBase
    #endregion
    #region Get Widgets
    $resourcePath = "/dashboard/widgets"
    $widgets = Get-LMRestAPIObjectListing `
    -resourcePathRoot $resourcePath `
    -accessKey $accessKey `
    -accessId $accessID `
    -URLBase $URLBase
    #endregion
    #region Get Groups
    $resourcePath = "/device/groups"
    $Groups = Get-LMRestAPIObjectListing `
    -resourcePathRoot $resourcePath `
    -accessKey $accessKey `
    -accessId $accessID `
    -URLBase $URLBase
    #endregion

    # Get Client Names from groups
    $clientnames = (
    $groups `
    | where parentid -eq $parentGroupID `
    | where name -notmatch "^\."
    ).name | sort

    #!!! If you have more than one dashboard, duplicate this and change the $masterdash
    #!!! to other variables defined at the start of the script.

    # ID Master Dashboard
    $master = $dashboards | ? name -eq $masterDash.name
    if (($master.name).count -eq 1) {

    $masterDash.id = $master.id
    $masterDash.widgetid = $master.widgetsConfig[0].psobject.Properties.name

    $outputJSON = generateJSON `
    -dashInfo $masterDash `
    -clientnames $clientnames

    $resourcePath = "/dashboard/widgets/$($masterDash.widgetid)"
    $url = $URLBase + $resourcePath

    $widget = Send-Request `
    -accessKey $accessKey `
    -accessId $accessID `
    -data $outputJSON `
    -URL $URL `
    -httpVerb "PATCH"
    }

    Here’s the code I’m using to populate these Dashboards.  The new UI (v4) displays this better if you expand/maximize the widget to fill the page (although filtering out SDT’d alerts doesn’t work currently).  Dashboard is just a single widget dashboard with a NOC widget that fills the page.  The only two non-dynamic parts are the name of the dashboard, which goes in $masterDash.name and the ID of the group you want as your source for the subgroups… in our case it’s the “/Clients” group… so the ID number from that goes in $parentGroupID

    This is built off of my old API exploration / report generation powershell functions, which is, in turn, built from the API + Powershell documentation from LM.  This doesn't require any external modules to be added to use the API, so can be deployed against a collector with no prior software installs needed beyond what you’ll already have done.

    I recommend targeting your dataSource to a single collector, then increasing the timeout for the scripts.  At some point, I’m going to add pre-filtering to the initial API requests for groups and dashboards to speed those parts a little bit, but it’s not too bad currently while grabbing everything.

    DS runs 1/day to populate the widget, feel free to have it hit more frequently if needed.  Only two datapoints currently, exitCode and executionTime.  I’m probably going to add groupCount at some point.

    Enjoy.

  • … and warning!  I use backticks as line continuations frequently to allow for a single “thing” per line in my scripts.  I dev in Powershell ISE, so it’ll look best there with everything all lined up pretty.  Style and spacing is closer to python than .net / C#