LogicMonitor + Catchpoint: Enter the New Era of Autonomous IT

Learn more

LogicMonitor’s RUCKUS One AP monitoring package provides comprehensive visibility into cloud-managed wireless access points (APs) deployed through CommScope’s RUCKUS One platform. This package uses the RUCKUS One public REST API to collect performance, inventory, and client telemetry data from APs distributed across multiple venues, giving network teams detailed insight into wireless infrastructure health and usage.

For more information about RUCKUS One APIs, see  Overview (APIs) and Client Management (0.0.1) from RUCKUS. 

Requirements for RUCKUS One AP Monitoring

To use RUCKUS One AP Monitoring, you need the following:

  • RUCKUS One API access enabled for the tenant
  • OAuth 2.0 client credentials configured for API access
    For more information, see Generating Application Token from RUCKUS. 
  • Resources registered in RUCKUS One and assigned to a venue
  • Collector outbound HTTPS access to the appropriate RUCKUS One API region (TCP 443)

Adding RUCKUS One AP Resources into Monitoring

RUCKUS One AP resources can be onboarded into monitoring using one of the following methods:

  • Enhanced Script NetScan (recommended)
  • Manually add resources

For more information on NetScans, see NetScan Overview.

Using the Enhanced Script NetScan to Add Resources

  1. In LogicMonitor, navigate to Modules > Exchange.
  2. Locate and install the RUCKUS One AP module package.
  3. Navigate to Resources > Add > Advanced NetScan
  4. Enter a name for the NetScan (for example, RUCKUS One APs). 
  5. Select the Collector to run the NetScan. 
  6. Select “Enhanced Script NetScan” from the Method dropdown menu. 
  7. In the Enhanced Script section, select Device Credentials > Use custom credentials for this scan. 
  8. Add the following properties to provide the NetScan with the required RUCKUS One credentials and to control how resources are organized.
PropertyDescriptionRequired?
ruckus.one.tenant.idRuckus One Tenant ID to query APs for.Yes
ruckus.one.client.idRuckus One API Client ID.Yes
ruckus.one.client.keyRuckus One API Client Secret/Key.Yes
ruckus.one.regionRuckus One API hostname/region. Defaults to api.ruckus.cloud if not set.Optional
root.folder.nameRoot folder path in LM where devices will be created. Defaults to Ruckus One.Optional
skip.device.dedupeSet to true to skip duplicate device checks via LM API. Defaults to false.Optional
hostname.sourceControls how hostname collisions are resolved: lm, logicmonitor, or netscan.Optional
lmapi.timelimit.secTime limit (30–120 sec) for LM API device lookups. Defaults to 60.Optional
ruckus.one.ap.venues.csvName of a CSV file with custom venue namesOptional

Note: If any of ruckus.one.tenant.id, ruckus.one.client.id, or ruckus.one.client.key are missing, the script throws an error and the NetScan will fail.

  1. 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 can 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-2026 - LogicMonitor, Inc. All rights reserved.
 ******************************************************************************/

import com.santaba.agent.groovy.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

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

// Credentials Check
if (!props.get("ruckus.one.tenant.id") || !props.get("ruckus.one.client.id") || !props.get("ruckus.one.client.key")) {
    throw new Exception("Must provide 'ruckus.one.tenant.id', 'ruckus.one.client.id', and 'ruckus.one.client.key' to run this script. Verify necessary credentials have been provided.")
}

// Optional properties
def rootFolder = props.get("root.folder.name", "Ruckus One")  // root folder can be nested, i.e. 'site1/subfolder1'
Boolean skipDeviceDedupe = props.get("skip.device.dedupe", "false").toBoolean()
String hostnameSource = props.get("hostname.source", "")?.toLowerCase()?.trim()

// Get region & tenant ID
def region = props.get("ruckus.one.region") ?: "api.ruckus.cloud"
def tenantId     = props.get("ruckus.one.tenant.id")

// Cache context
def logCacheContext = "${tenantId}::ruckus-one-ap"

def modLoader   = GSH.getInstance(GroovySystem.version).getScript("Snippets", Snippets.getLoader()).withBinding(getBinding())
def lmEmit      = modLoader.load("lm.emit", "1.1")
def lmDebug     = modLoader.load("lm.debug", "1.0").debugSnippetFactory(out, debug)
def http        = modLoader.load("proto.http", "0").httpSnippetFactory(props)
def cache       = modLoader.load("lm.cache", "0").cacheSnippetFactory(lmDebug, logCacheContext)
def ruckusOneAp = modLoader.load("ruckus.one.ap", "0").create(props, lmDebug, cache, http)

// Only initialize lmApi snippet class if customer has not opted out
def lmApi
if (!skipDeviceDedupe) {
    lmApi = modLoader.load("lm.api", "0").lmApiSnippetFactory(props, http, lmDebug)
}

// 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)
    }
}

// Get all AP information for tenant
def apsEndpoint = "venues/aps/query"
def apsEndpointBody = '{"pageSize": 10000 , "fields": ["serialNumber","venueId","model","macAddress","firmwareVersion", "name", "networkStatus.ipAddress" ] }'
def apsResponseData = null
if(debug){
    apsResponseData = cache.cacheGet(apsEndpoint)
} else {
    apsResponseData = ruckusOneAp.requestDataFromAPI("POST", apsEndpoint, apsEndpointBody)
}
lmDebug.LMDebugPrint("\nResponse Data from ${apsEndpoint}:\n${apsResponseData}\n")
def apData = ruckusOneAp.slurper.parseText(apsResponseData)
lmDebug.LMDebugPrint("AP Data: ${apData}")

// Get venue ID to name mappings
def venueEndpoint = "venues/query"
def venueEndpointBody = '{"pageSize": 10000, "fields": ["id","name"] }'
def venueResponseData = null
if(debug){
    venueResponseData = cache.cacheGet(venueEndpoint)
} else {
    venueResponseData = ruckusOneAp.requestDataFromAPI("POST", venueEndpoint, venueEndpointBody)
}
lmDebug.LMDebugPrint("\nResponse Data from ${apsEndpoint}:\n${venueResponseData}\n")
venueResponseData = ruckusOneAp.slurper.parseText(venueResponseData)
Map<String,String> venueIdToNameMap = venueResponseData?.data?.collectEntries { venue ->
    [(venue.id): venue.name]
}
lmDebug.LMDebugPrint("venueIdToNameMap: ${venueIdToNameMap}")

// Get tenant ID and name
def tenantEndpoint = "tenants/self"
def tenantResponseData = ruckusOneAp.requestDataFromAPI("GET", tenantEndpoint)
lmDebug.LMDebugPrint("\nResponse Data from ${tenantEndpoint}:\n${tenantResponseData}\n")
tenantResponseData = ruckusOneAp.slurper.parseText(tenantResponseData)
def responseTenantId = tenantResponseData?.id
def responseTenantName = tenantResponseData?.name
lmDebug.LMDebugPrint("Tenant Info: 'id'=${responseTenantId}, 'name'=${responseTenantName}")

// Fail safe
if(responseTenantId != tenantId) {
    throw new Exception("Tenant ID provided in credentials (${tenantId}) does not match tenant ID retrieved from API (${responseTenantId}). Verify credentials and try again.")
}

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 display names found within LogicMonitor portal wherein hostname in LM does not match hostname in Netscan output.  Refer to documentation for how to resolve name collisions using 'hostname.source' netscan property.",
        "total" : 0,
        "resources" : []
]

// Loop through data to build device map with proper keys
apData?.data?.each{ device ->

    String displayName = device.name
    Integer collectorId

    // 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)
        }
        else if (pathFlag == "all") {
            deviceMatch = lmApi.checkExistingDevices(displayName, 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 ip = device?.networkStatus?.ipAddress // ip from the endpoint data
        if (ip != deviceMatch.name) {
            def collisionInfo = [
                    (displayName) : [
                            "Netscan" : [
                                    "hostname"    : ip
                            ],
                            "LM" : [
                                    "hostname"    : deviceMatch.name,
                                    "collectorId" : deviceMatch.currentCollectorId
                            ],
                            "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.name
                collectorId = deviceMatch.currentCollectorId
                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
            }

            duplicateResources["resources"].add(collisionInfo)
        }
    }

    // Initialize groupName and assign value based on whether a proper venue name has been provided via CSV
    List<String> groupName = ["${rootFolder}/${responseTenantName}/${venueIdToNameMap[device.venueId]}"]

    def hostPropsToEmit = [
        "ruckus.one.region"    : region,                                // region, defaults to "api.ruckus.cloud" if not set
        "ruckus.one.tenant.name": responseTenantName,                   // tenant name
        "ruckus.one.tenant.id" : responseTenantId,                      // tenant
        "ruckus.one.client.id" : props.get("ruckus.one.client.id"),     // credentials
        "ruckus.one.client.key": props.get("ruckus.one.client.key"),    // credentials
        "ruckus.one.venue.id"  : device.venueId,                        // used to filter data by venue
        "ruckus.one.venue.name": venueIdToNameMap[device.venueId],      // venue name
        "ruckus.one.serial"    : device.serialNumber.toString(),        // used to filter data by AP serial number
    ]

    // Build resource map
    Map resource = [
            "hostname"    : "${device.networkStatus.ipAddress}",   // String
            "displayname" : device.name,            // String
            "hostProps"   : hostPropsToEmit,        // Map<String, String>
            "groupName"   : groupName,              // List<String>
            "collectorId" : collectorId             // Integer
    ]

    // 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) {
        resources.add(resource)
    }
}

// Output validated data in JSON format
lmEmit.resource(resources, debug)

// Report devices that already exist in LM via log file named after root folder
if (duplicateResources["resources"].size() > 0) {
    def netscanDupLog = new File("../logs/NetscanDuplicates/${rootFolder.replaceAll(" ", "_")}.json")
    new File(netscanDupLog.getParent()).mkdirs()
    duplicateResources["total"] = duplicateResources["resources"].size()
    def json = JsonOutput.prettyPrint(JsonOutput.toJson(duplicateResources))
    netscanDupLog.write(json)
    if (hostnameSource) {
        lmDebug.LMDebug("${duplicateResources["resources"].size()} devices found that were resolved with hostname.source=${hostnameSource} in netscan output.  See LogicMonitor/Agent/logs/NetscanDuplicates/${rootFolder.replaceAll(" ", "_")}.json for details.")
    }
    else {
        lmDebug.LMDebug("${duplicateResources["resources"].size()} devices found that were not reported in netscan output.  See LogicMonitor/Agent/logs/NetscanDuplicates/${rootFolder.replaceAll(" ", "_")}.json for details.")
    }
}

return 0
  1. 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.
  2. Select Save or Save & Run

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.

Manually Adding RUCKUS One AP Resources

  1. Create a RUCKUS One AP Resource Group.
    For more information, see Adding Resource Groups
  2. Add the APs as resources in your RUCKUS One AP Resource Group. 
  3. Add or verify that the following properties are set on the resources or in your resources group:
PropertyDescription
system.hostnameAP management IP or resolvable hostname
ruckus.one.tenant.idTenant ID
ruckus.one.client.idOAuth client ID
ruckus.one.client.keyOAuth client secret
ruckus.one.serialAP serial number
ruckus.one.venue.idVenue identifier
ruckus.one.regionAPI region (if applicable)

For more information on setting properties, see Resource and Instance Properties.

Import LogicModules

Import all LogicModules included in the RUCKUS One AP Monitoring package from LM Exchange.

If these LogicModules are already present, ensure they are updated to the most recent versions. After import, discovery and data collection begin automatically.

Troubleshooting

Use the following table to help diagnose and resolve common issues encountered when deploying or operating the RUCKUS One AP Monitoring package:

IssueResolution
No data collectedVerify that all required properties are configured and confirm outbound HTTPS connectivity from the Collector.
Modules not applyingConfirm the addCategory_Ruckus_One_AP PropertySource is assigning the correct system category.
Authentication errorsVerify API credentials and tenant ID configuration.

LogicModules in Package

LogicMonitor’s package for RUCKUS One AP Monitoring consists of the following LogicModules. For full coverage, ensure all of the following LogicModules are imported into your LogicMonitor platform:

Display NameTypeDescription
Ruckus_One_AP_APIDataSourceConnects to the RUCKUS One cloud API to collect and cache access point, venue inventory, and health metrics for use by the other RUCKUS One AP modules.
Ruckus_One_AP_PerformanceDataSourceProcesses the number of clients according to venue and serial number for RUCKUS One.
Ruckus_One_AP_SSIDsDataSourceDiscovers each SSID on your RUCKUS One Access Point and monitors per-SSID client load, traffic, and RF quality (RSSI, SNR, noise floor).
Ruckus_One_AP_RadiosDataSourceDiscovers each radio band on your RUCKUS One Access Point and monitors per-radio client load, traffic, and RF quality (RSSI, SNR, noise floor).
addCategory_Ruckus_One_APPropertySourceAssigns system.categories value of RuckusOneAP and other required properties to RUCKUS One Access Point devices.
Ruckus One WebhooksLogSourceMaps webhook events from RUCKUS One to the related LogicMonitor resources.

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, adjust these predefined thresholds to meet the unique needs of your environment. For more information on tuning datapoint thresholds, see Static Thresholds for Datapoints.

14-day access to the full LogicMonitor platform