Forum Discussion

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

NOC Rollup Status Dashboards for MSPs

LM doesn't come with it out of the box, so I built the NOC Dashboard I've wanted.  It provides high level, at-a-glance health indicators for each of our client environments we manage.  This makes a great "big board" for a NOC room or a second screen status board for work from home NOC/Support folks.  I do have three examples in this code for ways to filter for specific teams/purposes.  This all collapses for ease of reference correctly in Powershell ISE on windows.

Line 282 references a dataSource I wrote that counts frequency of specific eventlog events to illustrate potential brute force attempts (CTM are my initials, we tag our scripts to make finding the best source of answers faster in the future - old habit from pen & paper change logs from a previous job).

As any screenshots would contain client names, I'm unable to post any screen shots of the results of this, but my current settings for my Main dashboard are (This is the first Dashboard I've made that looks better in UIv4 than 3):

...

 

#!!! These two need to be changed.  First is a string, second an integer
#!!! See the comment block below for instructions

# The first chunk of your company's logicmonitor URL
$company   = "yourCompanyNameHere"

# ID of the group to be used as a source for the NOC widget items
$parentGroupID = <parentGroupID>

<#
    Netgain Technology, llc ( https://netgaincloud.com )
    2/26/2024 - Developed by Cole McDonald

    Disclaimer: Neither Netgain nor Cole McDonald are not responsible for
    any unexpected results this script may cause in your environment.

    To deploy this:
        - COLLECTOR:
            you will need a collector for scripting, this will be the single applies to target.
            You may need to increase the script timeout depending on the size of your device deployment.
        - DASHBOARD:
            you will need a Dashboard with a NOC widget on it.  The name can be whatever you'd like,
            there will be a name change in the "name" property for the initial array.
            In the case of the first example here, "NOC - Master"
        - PARENT GROUP:
            you will need to identify the ID# of the group you wish to use as the source for the subgroup list
            and set the $parentGroupID to the appropriate ID#

    Purpose: Create an auto-updating high level NOC dashboard that can show
        - Rollup state for a list of client subgroups from our \Clients group
        - Group Indicators for a specific dataSource
        - Group indicators for a subset of devices within each group
        
    After the API region, there are three separate dashboards referenced to illustrate the 3 methods for using this dataSource.

    NOTE: my code uses backticks for line continuation.  Where possible in my code, 
    each line indicates a single piece of information about the script's
    algorithm and the first character in each line from a block indicates
    the line's relationship to the one above it.
#>

#region Rest API Initialization and Functions
# Init variables used in the RESTApi functions
$URLBase   = "https://$company.logicmonitor.com/santaba/rest"

$accessID  = "##ApiAccessID.key##"
$accessKey = "##ApiAccessKey.key##"
#-------- The Functions ----------

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
}
# Get Dashboards
$resourcePath         = "/dashboard/dashboards"
$dashboards           = Get-LMRestAPIObjectListing `
    -resourcePathRoot $resourcePath                `
    -accessKey        $accessKey                   `
    -accessId         $accessID                    `
    -URLBase          $URLBase
# Get Widgets
$resourcePath         = "/dashboard/widgets"
$widgets              = Get-LMRestAPIObjectListing `
    -resourcePathRoot $resourcePath                `
    -accessKey        $accessKey                   `
    -accessId         $accessID                    `
    -URLBase          $URLBase
# Get Groups
$resourcePath         = "/device/groups"
$Groups               = Get-LMRestAPIObjectListing `
    -resourcePathRoot $resourcePath                `
    -accessKey        $accessKey                   `
    -accessId         $accessID                    `
    -URLBase          $URLBase
#endregion

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
}

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

#ID Master Dashboard

# declare dashboard name and set default id and widgetid to use in the loop later
$masterDash    = @{ id=0; widgetid=0; name="NOC - Master"   }

$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"

}

#ID Network Dashboard

# declare dashboard name and set default id and widgetid to use in the loop later
$networkDash   = @{ id=0; widgetid=0; name="NOC - Network"  }

# preset filters for specific dashboard targeting by device
$networkDeviceDisplayNameString      = "*(meraki|kemp)*"

$network = $dashboards  | ? name -eq $networkDash.name
if (($network.name).count  -eq 1) {
    $networkDash.id        = $network.id
    $networkDash.widgetid  = $network.widgetsConfig[0].psobject.Properties.name

    $outputJSON = generateJSON          `
        -dashInfo          $networkDash `
        -clientnames       $clientnames `
        -deviceDisplayName $networkDeviceDisplayNameString

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

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

#ID Security Dashboard

# declare dashboard name and set default id and widgetid to use in the loop later
$securityDash  = @{ id=0; widgetid=0; name="NOC - Security" }

# preset filters for specific dashboard targeting by datasource
$securityDataSourceDisplayNameString = "Event Frequency Sec:4625 CTM"

$security = $dashboards | ? name -eq $securityDash.name
if (($security.name).count -eq 1) {
    $securityDash.id        = $security.id
    $securityDash.widgetid  = $security.widgetsConfig[0].psobject.Properties.name

    $outputJSON = generateJSON       `
        -dashInfo      $securityDash `
        -clientnames   $clientnames  `
        -DSDisplayName $securityDataSourceDisplayNameString

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

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

 

  • From a workflow standpoint, this allows for a clickthrough to one of our client's main environment group where we have all of their devices organized by function.  This allows for an overall view of their alerts specifically... or navigation to individual devices to follow the external link to our RMM tool for that device to address any alerting that pops the NOC item out of green.

  • Also note... in UIv4, SDTs don't filter out, even if explicitly turned off in the widget config... but they do filter out in UIv3.

  • Can't edit OP, so adding a piece of info... I have the DS set to run 1/day.  The applies to only targets our scripting collector.  It's oversized and has the script timeouts set longer to allow things like this to run.  Nothing reports to it other than itself, it's only used to run larger scripts against.