Juniper Mist Monitoring

Last updated on 23 May, 2023

LogicMonitor offers monitoring for Juniper Mist organizations, sites, and devices. You can monitor organization health, sites and traffic generated by the sites, as well as Juniper Mist-managed devices such as Juniper Mist Access Points, EX-Series Switches, Session and Smart Routers. The Juniper Mist API is used to retrieve site device stats and data.

Requirements

Adding Juniper Mist Resources

The recommended method for adding Juniper Mist-managed devices is to use an Enhanced Script Netscan with the LogicMonitor-provided Groovy script in this article. For a given Juniper Mist Organization, the Enhanced Script Netscan creates a group for the Mist Org and each Mist Site, and a resource for each Mist-managed device in the related group.

  1. Install the Juniper Mist LogicModules. For more information, see Juniper Mist LogicModules.
  2. In LogicMonitor, 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, Juniper Mist.
  4. Select the Collector that will execute the NetScan.
    By default, the NetScan assigns new resources to the collector executing the NetScan. You can override this behavior in Properties later and use multiple collector groups (for scale or logical connectivity). Select either an ungrouped collector or a collector group with failover.

Note: Auto-balanced collector groups (ABCG) are not supported with Juniper Mist. If you choose an ABCG, balancing will eventually assign resources for APs, switches, or gateways to a different collector than the one running the Juniper_Mist_Org DataSource. This DataSource writes data to the collector’s script cache that all other Juniper_Mist_* DataSources refer to for data collection. Running the Juniper_Mist_Org device on multiple collectors for the same Juniper Mist Site is not supported, as it multiplies Mist API calls and can negatively affect monitoring performance, scalability, and reliability.  

  1. Select Enhanced Script NetScan from the Method drop-down list.
  2. From the Enhanced Script section, select Device Credentials > Use custom credentials for this scan.
  3. Add the following properties. Properties provide the NetScan with the required Juniper Mist API credentials and change how and where the NetScan creates and organizes resources.
PropertyDescriptionRequired?
mist.api.orgMist Org IDYes
mist.api.keyMist API Key. The NetScan masks the value of this property.Yes
mist.api.org.folderThe name of the LogicMonitor Resource Group that this NetScan creates or uses if already existing. The value can be a nested child folder; for example, folder/folder/folder.No
mist.api.org.nameYour Mist Org Name. This becomes the name of the root Resource Group for Juniper Mist, or becomes the name of top-level child Resource Group if you use the mist.api.org.folder property to specify an alternate root folder.No
mist.api.org.sitesComma-separated site names to include (others are excluded).No
mist.api.org.collector.sites.csvBy default, new 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 Mist devices to your desired collector(s) based on Mist 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\bin

For more information, see Mapping Mist Sites to LM Envision Collectors.
No
mist.api.urlAlternate region URLs include:

https://api.eu.mist.com/api/v1/
https://api.ac2.mist.com/api/v1/
https://api.gc1.mist.com/api/v1/
No
  1. In the Filters section, enter the following. Filtering gives you control over which sites and device types that the NetScan creates.
PropertyDescription
mist.api.org.site.nameComma-separated names of sites you want to include or exclude. By default (without this filter), the NetScan imports all sites as Resource Groups. Site names with spaces must include a backslash before the space.  For example: Site\ For\ APs, SiteD, Site\ 23
mist.device.typeComma-separated names of device types that the NetScan will include or exclude, depending on the operation setting.  By default (without this filter), the NetScan imports all device types. Valid device types are: ap, switch, and gateway.
  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 may (at their discretion) require you to overwrite your edits with the supported script if problems arise.

/*******************************************************************************
 * © 2007-2023 - LogicMonitor, Inc. All rights reserved.
 ******************************************************************************/
 
import com.logicmonitor.common.sse.utils.GroovyScriptHelper as GSH
import com.logicmonitor.mod.Snippets
import com.santaba.agent.util.Settings
import com.santaba.agent.AgentVersion
import groovy.json.JsonSlurper
import java.text.DecimalFormat
import javax.net.ssl.HttpsURLConnection

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}.")
}

modLoader = GSH.getInstance()._getScript("Snippets", Snippets.getLoader()).withBinding(getBinding())
def emit = modLoader.load("lm.emit", "0")

String organization = netscanProps.get("mist.api.org")
String token = netscanProps.get("mist.api.key")
if (!organization) {
    throw new Exception("Must provide mist.api.org to run this script.  Verify necessary credentials have been provided in Netscan properties.")
}
if (!token) {
    throw new Exception("Must provide mist.api.key credentials to run this script.  Verify necessary credentials have been provided in Netscan properties.")
}

String url = netscanProps.get("mist.api.url")?.trim() ?: "https://api.mist.com/api/v1/"
if (!url.endsWith("/")) url += "/"

String organizationDisplayname = netscanProps.get("mist.api.org.name")?.trim() ?: "MistOrganization"
String organizationFolder = netscanProps.get("mist.api.org.folder")?.trim() ? netscanProps.get("mist.api.org.folder") + "/" : ""
def sitesWhitelist = netscanProps.get("mist.api.org.sites")?.tokenize(",")?.collect{ it.trim() }
def collectorSitesCSV = netscanProps.get("mist.api.org.collector.sites.csv")
def collectorSiteInfo
if (collectorSitesCSV) {
    collectorSiteInfo = processCollectorSiteInfoCSV(collectorSitesCSV)
}

Map headers = ["Authorization":"token ${token}", "Accept":"application/json"]

List<Map> resources = []

def facility = "siteMetrics"
def organizationSites = httpGet("${url}orgs/${organization}/sites", headers)
if (organizationSites.statusCode == 200 && organizationSites.data) {    
    if (collectorSiteInfo) {
        def seenCollectors = []
        collectorSiteInfo.each { collectorSite ->
            if (collectorSite.size() == 4) {
                def collectorId = collectorSite[0]
                if (seenCollectors.contains(collectorId)) return // One org device and folder per collector ID
                seenCollectors << collectorId
                def name = collectorSite[1]
                def folder = collectorSite[2]
                def props = ["mist.api.org": organization, "mist.api.key": token, "mist.api.url": url, "system.categories": "JuniperMistOrg", 
                             "mist.api.org.collector.sites": collectorSiteInfo.findAll{ it[0] == collectorId }?.collect{ it[3] }?.toString()?.replace("[", "")?.replace("]", "")]
                Map resource = [
                    "hostname"    : "${name}.invalid",
                    "displayname" : "${name}",
                    "hostProps"   : props,
                    "groupName"   : ["${organizationFolder}${folder}"],
                    "collectorId" : collectorId
                ]
                resources.add(resource)
            }
        }
    } else {
        def props = ["mist.api.org": organization, "mist.api.key": token, "mist.api.url": url, "system.categories": "JuniperMistOrg,NoPing"]
        Map resource = [
            "hostname"    : "${organizationDisplayname}.invalid",
            "displayname" : organizationDisplayname,
            "hostProps"   : props,
            "groupName"   : ["${organizationFolder}${organizationDisplayname}"]
        ]
        resources.add(resource)
    }

    organizationSites = organizationSites.data
    organizationSites.each { organizationSite ->
        def siteName = organizationSite.get("name")   
        if (sitesWhitelist != null && !sitesWhitelist.contains(siteName)) return    
        def site = organizationSite.get("id") 
        def siteDeviceStats = httpGet("${url}sites/${site}/stats/devices?type=all", headers)
        if (siteDeviceStats.statusCode == 200) {
            siteDeviceStats = siteDeviceStats.data
            siteDeviceStats.each { siteDeviceStat ->
                def ip = siteDeviceStat.get("ip")
                def name = siteDeviceStat.get("name")
                def type = siteDeviceStat.get("type")
                def props = ["mist.api.org": organization, "mist.api.site": site, "mist.device.type": type, "mist.api.org.site.name": siteName]
                if (type == "ap") {
                    props.put("system.categories", "JuniperMistAP,NoPing")
                } else if (type == "switch") {
                    props.put("system.categories", "JuniperMistSwitch")
                } else if (type == "gateway") {
                    props.put("system.categories", "JuniperMistGateway")
                }  
                if (ip && name && type && siteName) {
                    if (ip == "127.0.0.1") ip = name
                    if (collectorSiteInfo) {
                        def collectorIdEntry = collectorSiteInfo.find{ it.contains(siteName) }
                        def collectorId = collectorIdEntry[0]
                        def folder = collectorIdEntry[2]                      
                        Map resource = [
                            "hostname"    : ip,
                            "displayname" : name,
                            "hostProps"   : props,
                            "groupName"   : ["${organizationFolder}${folder}/${siteName}"],
                            "collectorId" : collectorId
                        ]
                        resources.add(resource)
                    } else {
                        Map resource = [
                            "hostname"    : ip,
                            "displayname" : name,
                            "hostProps"   : props,
                            "groupName"   : ["${organizationFolder}${organizationDisplayname}/${siteName}"]
                        ]
                        resources.add(resource)
                    }
                }
            }
        } else {
            throw new Exception("Error occurred during sites/${site}/stats/devices?type=all HTTP GET: ${siteDeviceStats}.")
        }
    }

    emit.resource(resources)
} else {
    throw new Exception("Error occurred during orgs/${organization}/sites HTTP GET: ${organizationSites}.")
}

return 0

def httpGet(String endpoint, Map headers, Integer retryLen = 5) {
    Random random = new Random()
    Integer waitPeriod = 5000 + Math.round((3000 * random.nextDouble())) // adding randomness to wait time
    Double waitTime = 0
    Map returnItem = [:]
    Integer retryCount = 0
    ArrayList validRetries = [429, 500]
    HttpsURLConnection response = null
    while (retryCount <= retryLen) {
        retryCount++
        response = rawHttpGet(endpoint, headers)
        returnItem["statusCode"] = response.getResponseCode()
        if (!validRetries.contains(returnItem["statusCode"])) {
            if (returnItem["statusCode"] == 200) {
                returnItem["rawData"] = response.inputStream.text
                returnItem["data"] = new JsonSlurper().parseText(returnItem["rawData"])
                sleep(200)
            } else {
                returnItem["data"] = null
            }
            returnItem["waitTime"] = waitTime
            return returnItem
        }
        sleep(waitPeriod)
        waitTime = waitTime + waitPeriod
    }
    returnItem["statusCode"] = -1  // unknown status code
    returnItem["data"] = null
    returnItem["waitTime"] = waitTime
    returnItem["rawData"] = null
    returnItem["errMsg"] = response.getErrorStream()
    return returnItem
}

def rawHttpGet(String url, Map headers) {
    HttpsURLConnection request = null
    Map proxyInfo = getProxyInfo()
    if (proxyInfo){
        request = url.toURL().openConnection(proxyInfo.proxy)
    } else {
        request = url.toURL().openConnection()
    }
    headers.each { request.addRequestProperty(it.key, it.value) }
    return request
}

Map getProxyInfo() {
    Boolean deviceProxy    =  this.netscanProps.get("proxy.enable")?.toBoolean() ?: true  // default to true in absence of property to use collectorProxy as determinant
    Boolean collectorProxy = Settings.getSetting("proxy.enable")?.toBoolean() ?: false // if settings are not present, value should be false
    Map proxyInfo = [:]

    if (deviceProxy && collectorProxy) {
        proxyInfo = [
            enabled : true,
            host : this.netscanProps.get("proxy.host") ?: Settings.getSetting("proxy.host"),
            port : this.netscanProps.get("proxy.port") ?: Settings.getSetting("proxy.port") ?: 3128,
            user : Settings.getSetting("proxy.user"),
            pass : Settings.getSetting("proxy.pass")
        ]

        proxyInfo["proxy"] = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyInfo.host, proxyInfo.port.toInteger()))
    }

    return proxyInfo
}

// Helper function to process a collector id, site organization device name, folder, and site CSV
def processCollectorSiteInfoCSV(String filename) {
    // Ensure relative filepath is complete with extension type
    def filepath
    if (!filename.contains("./")) {
        filepath = "./${filename}"
    }
    if (!filename.contains(".csv")) {
        filepath = "${filename}.csv"
    }

    // Read file into memory and split into list of lists
    def csv = new File(filepath)
    def rows = csv.readLines()*.split(",")
    def data

    // 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("site organization device name") && maybeHeaders.contains("folder") && maybeHeaders.contains("site")) {
        // Remove headers from dataset
        data = rows[1..-1]
    }
    // 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\", \"Site Organization Device Name\", \"Folder\", and \"Site\" (case insensitive).  Headers provided: \"${rows[0]}\"")
    }

    return data
}
  1.  In the Schedule section, select Run this NetScan on a schedule. Since the NetScan uses the rate limited Mist API for discovery, schedule the NetScan to run only as often as necessary. The recommended frequency is once a week.

Note: Subsequent NetScan runs will add or move resources or resource groups based on changes in your Juniper Mist Dashboard. However, the NetScan does not have the ability to delete resources.

  1. Select Save or Save & Run.

Note: 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.

Juniper Mist Properties

Additional attributes and values for Juniper Mist.

Proxy Settings

The modules in this suite support HTTP connections using a proxy server. You can configure this in the Collector settings, see Configuring your Collector for use with HTTP Proxies, or with the following device host properties. Device host properties take precedence over Collector settings for proxy configurations.

PropertyValue
proxy.enable(Optional) This suite is written to use collector proxy settings for HTTP calls configured by the user. To enable, add this device property with the value set to true. Set to false to override the use of configured collector proxy settings and connect without a proxy.
proxy.host(Optional) Configure a proxy host to connect through that is different from collector configuration.
proxy.port(Optional) Configure a proxy port to connect through that is different from collector configuration.

Mapping Mist Sites to LM Envision Collectors

Collector IDSite Organization Device NameFolderSites
11Region_East_SitesRegion_EastNYC
11Region_East_SitesRegion_EastMIA
11Region_East_SitesRegion_EastCLT
11Region_East_SitesRegion_EastATL
11Region_East_SitesRegion_EastBOS
11Region_East_SitesRegion_EastCHS
1Region_West_SitesRegion_WestLAX
1Region_West_SitesRegion_WestLAS
1Region_West_SitesRegion_WestSEA

Comma-separated values for Collector ID, Site Organization Device Name, Folder, and Sites.

Collector ID,Site Organization Device Name,Folder,Sites
11,Region_East_Sites,Region_East,NYC
11,Region_East_Sites,Region_East,MIA
11,Region_East_Sites,Region_East,CLT
11,Region_East_Sites,Region_East,ATL
11,Region_East_Sites,Region_East,BOS
11,Region_East_Sites,Region_East,CHS
1,Region_West_Sites,Region_West,LAX
1,Region_West_Sites,Region_West,LAS
1,Region_West_Sites,Region_West,SEA

Juniper Mist Troubleshooting

  • This suite relies on Collector Script Caching to continuously retrieve and store data from the Juniper Mist API to minimize rate limiting constraints. Continuous data collection is maintained on the Mist Organization device through the Juniper_Mist_Org DataSource, which maintains a background thread that writes API responses to the collector script cache. addCategory_Juniper_Mist_Organization PropertySource must run first to set the proper category on this device. Juniper_Mist_Org DataSource must be running successfully for all other modules in this package to be successful.
  • During onboarding, you may wish to run Active Discovery manually on additional PropertySources in this package after Juniper_Mist_Org begins collecting data to expedite monitoring and topology mapping.
  • If data gaps are seen, verify Juniper_Mist_Org 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 Juniper_Mist_Org on your Mist Organization device in order to check if the API is unreachable or monitoring has hit the API rate limit.

Juniper Mist LogicModules

Install or update the following LogicModules. For more information, see Modules Installation.

Display NameTypeDescription
Mist AP BeaconsDataSourceHealth and performance of Mist-managed AP Beacons.
Mist AP HealthDataSourceHealth and availability of Juniper Mist-managed wireless access points.
Mist AP InterfacesDataSourceStatus and network transmission and receive statistics for Juniper Mist-managed AP interfaces.
Mist AP PerformanceDataSourcePerformance of Juniper Mist-managed wireless access points.
Mist AP RadiosDataSourceMist-managed AP radio power and usage.
Mist ClientsDataSourceAn Org’s Juniper Mist clients by type.
Mist DevicesDataSourceAn Org’s Juniper Mist devices by type and state.
Mist OrganizationDataSourceHealth of the monitored Mist Organization and the Mist API connection.
Mist SitesDataSourceAn Org’s Juniper Mist sites by client count and network transmission.
Mist Switch HealthDataSourceHealth of Mist-managed switches.
Mist Switch PerformanceDataSourcePerformance of Mist-managed switches.
Mist WAN Edge HealthDataSourceHealth of Mist-managed session smart routers.
Mist WAN Edge PerformanceDataSourcePerformance of Mist-managed session smart routers.
addCategory_Juniper_Mist_DevicePropertySourceAdds the JuniperMist<device type> system.category to devices managed by Juniper Mist Requires Juniper_Mist_Org DataSource (watchdog) in order to collect data.
addCategory_Juniper_Mist_OrganizationPropertySourceAdds the JuniperMistOrg system.category to devices responsible for collecting data for the Juniper Mist organization.
addERI_Juniper_Mist_DevicePropertySourceSets Juniper Mist device MAC address ERI and device type ERT. Requires Juniper_Mist_Org DataSource (watchdog) in order to collect data.
addERI_Juniper_Mist_OrganizationPropertySourceSets Juniper Mist organization UUID ERI and Cluster ERT.
Juniper_Mist_TopologyTopologySourceTopology of a Mist Org. Sites, and managed-devices.

Note: After the LogicModules are imported, the PropertySource addCategory_Juniper_Mist_Organization runs automatically and sets the required properties. After the Mist Organization device has started collecting data in the DataSource Juniper_Mist_Org, data collection begins for all devices.

In This Article