Versa SD-WAN Monitoring

Last updated on 08 July, 2024

LogicMonitor offers out-of-the-box monitoring for Versa SDWAN appliances. It is designed to monitor organization health, sites, and traffic generated by the sites, Versa SDWAN appliances, and uses the Versa Director API to retrieve site device stats and data.


  • Collector version 32.400 or higher
  • An API User and Password using the Versa Director portal or Versa Director API.

Add Resources into Monitoring

You can either add Versa SDWAN resources by running a NetScan or add the resources manually. We recommend running a NetScan. NetScan creates devices under device groups for each Versa organization site.
For more information, see What is NetScan.

Using Advanced NetScan to add Cisco Catalyst SD-WAN Resources

For more information about using Advanced NetScan, see Enhanced Script Netscan.

1. In your LogicMonitor Portal > Modules > Exchange, install the Versa SD-WAN LogicModules.

2. Navigate to Resources > Add > Several Devices > Advanced NetScan.
The Add Advanced NetScan page is displayed.

3. Enter a name that you want to associate with this NetScan. For example, “Versa SD-WAN”. 

4. Select the Collector to execute the NetScan.
By default, the NetScan assigns new resources to the collector running the NetScan.

5. Select Enhanced Script NetScan from the Method drop-down list.

6. From the Enhanced Script section, select Device Credentials > Use custom credentials for this scan.

7. Add the following properties that provide the NetScan with the required Versa API credentials and change how and where the NetScan creates and organizes resources:

versa.directorVersa Director hostname.Yes
versa.userVersa API user. The NetScan masks the value of this property.Yes
versa.passVersa API password. The NetScan masks the value of this property.Yes
versa.folderThe name of the LogicMonitor Resource Group that this NetScan creates or uses if already exists. The value can be a nested child folder; for example, folder/folder/folder.No
versa.organizationYour Versa Org Name. This becomes the name of the root Resource Group for Versa SD-WAN or becomes the name of the top-level child Resource Group if you use the versa.organization property to specify an alternate root folder.No
versa.sitesComma-separated site IDs to include (others are excluded).No
versa.collector.sites.csvNew resources created by the NetScan are assigned to the collector that runs the NetScan. This property uses a CSV file to override the default behavior and assign Versa appliance devices to your desired collector(s) based on Versa Site and LM Collector names.The CSV file must be stored on the collector that executes the NetScan: Linux: /usr/local/logicmonitor/agent/bin Windows: C:\Program Files\LogicMonitor\Agent\binFor more information, see Mapping Sites to LM Envision Collectors.No
versa.urlAlternate URL for API calls.Yes

8. Select Embed a Groovy script and embed the following script:

Warning: Do not edit the script. Edited Enhanced Script NetScans are not supported. If the LogicMonitor-provided script is edited, LogicMonitor Support may (at their discretion) require you to overwrite your edits with the supported script if problems arise. The Enhanced Script NetScan limits LM Envision Resource creation to <= 600 per hour. To create more than 600 Resources, schedule the NetScan to recur each hour until all resources are added.

 * © 2007-2024 - LogicMonitor, Inc. All rights reserved.

import com.logicmonitor.common.sse.utils.GroovyScriptHelper as GSH
import com.logicmonitor.mod.Snippets
import com.santaba.agent.AgentVersion
import java.text.DecimalFormat
import groovy.json.JsonOutput
import groovy.json.JsonSlurper

// To run in debug mode, set to true
Boolean debug = false
// To enable logging, set to true
Boolean log = false

// Set props object based on whether or not we are running inside a netscan or debug console
def props
try {
    props = hostProps
    debug = true  // set debug to true so that we can ensure we do not print sensitive properties
catch (MissingPropertyException) {
    props = netscanProps

def director = props.get("versa.director")
def user = props.get("versa.user")
def pass = props.get("versa.pass")
if (!director) {
    throw new Exception("Must provide versa.director to run this script.  Verify necessary credentials have been provided in Netscan properties.")
if (!user) {
    throw new Exception("Must provide versa.user to run this script.  Verify necessary credentials have been provided in Netscan properties.")
if (!pass) {
    throw new Exception("Must provide versa.pass credentials to run this script.  Verify necessary credentials have been provided in Netscan properties.")

def logCacheContext = "versa-sdwan"
Boolean skipDeviceDedupe = props.get("skip.device.dedupe", "false").toBoolean()
if (debug) skipDeviceDedupe = true
String hostnameSource    = props.get("hostname.source", "")?.toLowerCase()?.trim()

Integer collectorVersion = AgentVersion.AGENT_VERSION.toInteger()
// Bail out early if we don't have the correct minimum collector version to ensure netscan runs properly
if (collectorVersion < 32400) {
    def formattedVer = new DecimalFormat("00.000").format(collectorVersion / 1000)
    throw new Exception("Upgrade collector running netscan to 32.400 or higher to run full featured enhanced netscan. Currently running version ${formattedVer}.")

def modLoader = GSH.getInstance()._getScript("Snippets", Snippets.getLoader()).withBinding(getBinding())
def emit        = modLoader.load("lm.emit", "1.1")
def lmDebugSnip = modLoader.load("lm.debug", "1")
def lmDebug     = lmDebugSnip.debugSnippetFactory(out, debug, log, logCacheContext)
def httpSnip    = modLoader.load("proto.http", "0")
def http        = httpSnip.httpSnippetFactory(props)
def cacheSnip   = modLoader.load("lm.cache", "0")
def cache       = cacheSnip.cacheSnippetFactory(lmDebug, logCacheContext)
// Only initialize lmApi snippet class if customer has not opted out
def lmApi
if (!skipDeviceDedupe) {
    def lmApiSnippet = modLoader.load("lm.api", "0")
    lmApi = lmApiSnippet.lmApiSnippetFactory(props, http, lmDebug)
def versaSnip = modLoader.load("versa.sdwan", "0")
def versa    = versaSnip.versaSnippetFactory(props, lmDebug, cache, http)

String rootFolder   = props.get("versa.folder", "Versa SD-WAN")
String url          = props.get("versa.url")
def organization = props.get("versa.organization")
def sitesWhitelist      = props.get("versa.sites")?.tokenize(",")?.collect{ it.trim() }
def collectorSitesCSV   = props.get("versa.collector.sites.csv")
def collectorSitesInfo
if (collectorSitesCSV) collectorSitesInfo = processCollectorSiteInfoCSV(collectorSitesCSV)

// Get information about devices that already exist in LM portal
List fields = ["name", "currentCollectorId", "displayName"]
Map args = ["size": 1000, "fields": fields.join(",")]
def lmDevices
// But first determine if the portal size is within a range that allows us to get all devices at once
def pathFlag, portalInfo, timeLimitSec, timeLimitMs
if (!skipDeviceDedupe) {
    portalInfo = lmApi.apiCallInfo("Devices", args)
    timeLimitSec = props.get("lmapi.timelimit.sec", "60").toInteger()
    timeLimitMs = (timeLimitSec) ? Math.min(Math.max(timeLimitSec, 30), 120) * 1000 : 60000 // Allow range 30-120 sec if configured; default to 60 sec

    if (portalInfo.timeEstimateMs > timeLimitMs) {
        lmDebug.LMDebugPrint("Estimate indicates LM API calls would take longer than time limit configured.  Proceeding with individual queries by display name for each device to add.")
        lmDebug.LMDebugPrint("\t${portalInfo}\n\tNOTE:  Time limit is set to ${timeLimitSec} seconds.  Adjust this limit by setting the property lmapi.timelimit.sec.  Max 120 seconds, min 30 seconds.")
        pathFlag = "ind"
    else {
        lmDebug.LMDebugPrint("Response time indicates LM API calls will complete in a reasonable time range.  Proceeding to collect info on all devices to cross reference and prevent duplicate device creation.\n\t${portalInfo}")
        pathFlag = "all"
        lmDevices = lmApi.getPortalDevices(args)

List<Map> resources = []

def now = new Date()
def dateFormat = "yyyy-MM-dd'T'HH:mm:ss.s z"
TimeZone tz = TimeZone.getDefault()
Map duplicateResources = [
    "date" : now.format(dateFormat, tz),
    "message" : "Duplicate device names and display names, keyed by display name that would be assigned by the netscan, found within LogicMonitor portal.  Refer to documentation for how to resolve name collisions using 'hostname.source' netscan property.",
    "total" : 0,
    "resources" : []

// Gather data from cache if running in debug otherwise make API requests
if (!organization) {
    if (debug) {
        organization = cache.cacheGet("${director}::organization")
        if (organization) organization = versa.slurper.parseText(organization)?.find { }?.name
    } else {
        organization = versa.http.rawGet(versa.url + "/nextgen/organization", versa.headers)?.content?.text
        if (organization) organization = versa.slurper.parseText(organization)?.find { }?.name

def assets
if (debug) {
    assets = cache.cacheGet("${director}::assets")
    if (assets) assets = versa.slurper.parseText(assets)
} else {
    assets = versa.http.rawGet(versa.url + "/nextgen/inventory/assets/", versa.headers)?.content?.text
    if (assets) assets = versa.slurper.parseText(assets)

def appliances
if (debug) {
    appliances = cache.cacheGet("${director}::appliances")
    if (appliances) appliances = versa.slurper.parseText(appliances)
} else {
    appliances = versa.http.rawGet(versa.url + "/nextgen/appliance/status", versa.headers)?.content?.text
    if (appliances) appliances = versa.slurper.parseText(appliances)

appliances.each { appliance ->
    def ip = appliance.nodesStatusList.first().get("hostIP")
    def uuid = appliance.get("applianceUUID")
    def name = appliance.get("applianceName")
    if (ip == "NOT-APPLICABLE") ip = name
    String displayName = name

    def asset = assets.find { == name }
    def siteName = asset."site-name"
    if (sitesWhitelist != null && !sitesWhitelist.contains(siteName)) return

    // Check for existing device in LM portal with this displayName; set to false initially and update to true when dupe found
    def deviceMatch = false
    // If customer has opted out of device deduplication checks, we skip the lookups where we determine if a match exists and proceed as false
    if (!skipDeviceDedupe) {
        if (pathFlag == "ind") {
            deviceMatch = lmApi.findPortalDevice(displayName, args)
            if (!deviceMatch) deviceMatch = lmApi.findPortalDeviceByName(ip, args)
        else if (pathFlag == "all") {
            deviceMatch = lmApi.checkExistingDevices(displayName, lmDevices)
            if (!deviceMatch) deviceMatch = lmApi.checkExistingDevicesByName(ip, lmDevices)
    if (deviceMatch) {
        // Log duplicates that would cause additional devices to be created; unless these entries are resolved, they will not be added to resources for netscan output
        def collisionInfo = [
            (displayName) : [
                "Netscan" : [
                    "hostname"    : ip,
                    "displayName" : displayName
                "LM" : [
                    "hostname"    :,
                    "collectorId" : deviceMatch.currentCollectorId,
                    "displayName" : deviceMatch.displayName
                "Resolved" : false

        // If user specified to use LM hostname on display name match, update hostname variable accordingly
        // and flag it as no longer a match since we have resolved the collision with user's input
        if (hostnameSource == "lm" || hostnameSource == "logicmonitor") {
            ip =
            deviceMatch = false
            collisionInfo[displayName]["Resolved"] = true
        // If user specified to use netscan data for hostname, update the display name to make it unique
        // and flag it as no longer a match since we have resolved the collision with user's input
        else if (hostnameSource == "netscan") {
            // Update the resolved status before we change the displayName
            collisionInfo[displayName]["Resolved"] = true
            displayName = "${displayName} - ${ip}"
            deviceMatch = false


    // Verify we have minimum requirements for device creation
    if (name) {
        if (ip == "") ip = name
        def deviceProps = [
            "versa.director"  : director,
            "versa.user"      : user,
            "versa.pass"      : pass,
            "system.categories" : "VersaAppliance",
            "versa.organization" : emit.sanitizePropertyValue(organization),
            "" : emit.sanitizePropertyValue(name),
            "versa.uuid" : emit.sanitizePropertyValue(uuid)

        if (url) deviceProps.put("versa.url", url)

        if (asset.location) deviceProps.put("location", emit.sanitizePropertyValue(asset.location))
        if (asset.latitude) deviceProps.put("latitude", asset.latitude)
        if (asset.longitude) deviceProps.put("longitude", asset.longitude)
        if (asset."site-id") deviceProps.put("", asset."site-id")

        if (sitesWhitelist != null) deviceProps.put("versa.sites", emit.sanitizePropertyValue(sitesWhitelist))
        if (siteName) deviceProps.put("", emit.sanitizePropertyValue(siteName))

        // Set group and collector ID based on user CSV inputs if provided
        def collectorId = null
        Map resource = [:]
        if (collectorSitesInfo) {
            collectorId = collectorSitesInfo[siteId]["collectorId"]
            def folder      = collectorSitesInfo[siteId]["folder"]
            siteName = collectorSitesInfo[siteId]["site"]
            resource = [
                "hostname"    : ip,
                "displayname" : name,
                "hostProps"   : deviceProps,
                "groupName"   : ["${rootFolder}/${folder}/${siteName}"],
                "collectorId" : collectorId
        } else {
            resource = [
                "hostname"    : ip,
                "displayname" : name,
                "hostProps"   : deviceProps,
                "groupName"   : ["${rootFolder}/${organization}/${siteName}"]

        // Only add the collectorId field to resource map if we found a collector ID above
        if (collectorId) {
            resource["collectorId"] = collectorId
            duplicateResources["resources"][displayName]["Netscan"][0]["collectorId"] = collectorId

        if (!deviceMatch) {

lmDebug.LMDebugPrint("Duplicate Resources:")
duplicateResources.resources.each {

emit.resource(resources, debug)

return 0

 * Processes a CSV with headers collector id, folder, and site name
 * @param filename String
 * @return collectorInfo Map with site id as key and Map of additional attributes as value
Map processCollectorSitesInfoCSV(String filename) {
    // Read file into memory and split into list of lists
    def csv = newFile(filename, "csv")
    def rows = csv.readLines()*.split(",")
    def collectorInfo = [:]

    // Verify whether headers are present and expected values
    // Sanitize for casing and extra whitespaces while gathering headers
    def maybeHeaders = rows[0]*.toLowerCase()*.trim()
    if (maybeHeaders.contains("collector id") && maybeHeaders.contains("folder") && maybeHeaders.contains("site")) {
        Map headerIndices = [:]
        maybeHeaders.eachWithIndex{ val, i ->
            headerIndices[val] = i
        // Index values for headers to ensure we key the correct index regardless of order
        def ni = headerIndices["site"]
        def ci = headerIndices["collector id"]
        def fi = headerIndices["folder"]

        // Remove headers from dataset
        def data = rows[1..-1]
        // Build a map indexed by site for easy lookups later
        data.each{ entry ->
            collectorInfo[entry[ni]] = [
                    "collectorId" : entry[ci],
                    "folder"      : entry[fi]
    // Bail out early if we don't have the expected headers in the provided CSV
    else {
        throw new Exception(" Required headers not provided in CSV.  Please provide \"Collector ID\", \"Folder Name, \"and Site Name (case insensitive).  Headers provided: \"${rows[0]}\"")

    return collectorInfo

 * Sanitizes filepath and instantiates File object
 * @param filename String
 * @param fileExtension String
 * @return File object using sanitized relative filepath
File newFile(String filename, String fileExtension) {
    // Ensure relative filepath is complete with extension type
    def filepath
    if (!filename.startsWith("./")) {
        filepath = "./${filename}"
    if (!filepath.endsWith(".${fileExtension}")) {
        filepath = "${filepath}.${fileExtension}"

    return new File(filepath)

9. In the Schedule section, select Run this NetScan on a schedule.

For dynamic environments, you can schedule the NetScan to run as frequently as hourly.

Note: Subsequent NetScan runs will add or move resources or resource groups based on changes in Cisco Catalyst SD-WAN. However, the NetScan does not have the ability to delete resources.

10. Select Save or Save & Run.

Manually Adding Resources

After running the NetScan, review the history for the number of resources added, or for error messages if the NetScan does not create any resources.

1. Create a Versa SDWAN device group.
For more information, see Adding Device Groups.

versa.directorVersa Director hostname
versa.userVersa API user
versa.passVersa API password

2. Add the devices to the Versa SDWAN device group.

3. Add or verify that the following properties and are set on the resource under the Versa SDWAN Resource Group:

versa.uuidVersa SDWAN appliance UUID

Import LogicModules

From the LogicMonitor public repository, import all Versa SDWAN LogicModules, which are listed in the LogicModules in Package. If these LogicModules are already present, ensure you have the most recent versions.

Once the LogicModules are imported, the addCategory_Versa_SDWAN_Appliance PropertySource must run to set the required properties to begin monitoring. Data collection will automatically begin for all devices after an elected device has started collecting data successfully in the Versa_SDWAN_API DataSource.

For more information on importing modules, see LM Exchange.


  • This suite relies on collector script cache to continuously retrieve and store data from the Versa Director API to minimize rate-limiting constraints. Continuous data collection is maintained on an elected Versa device through the Versa_SDWAN_API DataSource, which writes API responses to the collector script cache. addCategory_Versa_SDWAN_Appliance PropertySource must run first to set the proper category on this device. Versa_SDWAN_API DataSource must be running successfully for all other modules in this package to be successful.
  • During onboarding, you may which to run Active Discovery manually on additional PropertySources in this package after Versa_SDWAN_API begins collecting data to expedite monitoring and topology mapping.
  • If data gaps are seen, verify Versa_SDWAN_API is functioning successfully and check script cache health in the LogicMonitor_Collector_ScriptCache DataSource.

Note: The API used to pull data has rate limits. Check Versa_SDWAN_API on any Versa appliance to check if the API is unreachable or if, monitoring has hit the API rate limit.

LogicModules in Package

LogicMonitor’s package for Versa consists of the following LogicModules. For full coverage, import all of the following LogicModules into your LogicMonitor platform:

Display NameTypeDescription
Versa APIDataSourceMonitors Versa SDWAN Director API usage.
Versa SDWAN TunnelsDataSourceMaps Versa SDWAN topologies.
Versa SDWAN Appliance HealthDataSourceMonitors Versa SDWAN appliance health.
Versa SDWAN Appliance PerformanceDataSourceMonitors Versa SDWAN appliance performance.
Versa SDWAN Appliance InterfacesDataSourceMonitors Versa SDWAN appliance interfaces.
addCategory_Versa_SDWAN_AppliancePropertySourceAdds the VersaAppliance system category to devices managed by Versa, as well as other auto-properties.
addERI_Versa_SDWAN_AppliancePropertySourceDiscover and add Versa-specific ERI’s for Versa resources.
Versa SDWAN TopologyTopologySourceMaps Versa SDWAN topologies.

When setting static datapoint thresholds on the various metrics tracked by this package’s DataSources, LogicMonitor follows the technology owner’s best practice KPI recommendations.

Recommendation: If necessary, we encourage you to adjust these predefined thresholds to meet the unique needs of your environment. For more information on tuning datapoint thresholds, see Tuning Static Thresholds for Datapoints.

In This Article