LogicMonitor + Catchpoint: 自律型ITの新時代へ

さらに詳しく

LogicMonitorのRUCKUS One AP監視パッケージは、CommScopeのRUCKUS Oneプラットフォームを通じて導入されたクラウド管理型ワイヤレスアクセスポイント(AP)の包括的な可視性を提供します。このパッケージは、RUCKUS OneパブリックREST APIを使用して、複数の拠点に分散されたAPからパフォーマンス、インベントリ、クライアントテレメトリデータを収集し、ネットワークチームにワイヤレスインフラストラクチャの健全性と使用状況に関する詳細な情報を提供します。

RUCKUS One APIの詳細については、以下を参照してください。  概要(API) の三脚と クライアント管理 (0.0.1) RUCKUSより。 

RUCKUS One AP モニタリングの要件

RUCKUS One AP モニタリングを使用するには、次のものが必要です。

  • テナントに対してRUCKUS One APIアクセスが有効
  • API アクセス用に構成された OAuth 2.0 クライアント資格情報
    詳細については、を参照してください。 アプリケーショントークンの生成 RUCKUSより。 
  • RUCKUS One に登録され、会場に割り当てられたリソース
  • 適切な RUCKUS One API リージョンへのコレクターのアウトバウンド HTTPS アクセス (TCP 443)

RUCKUS One AP リソースを監視に追加する

RUCKUS One AP リソースは、次のいずれかの方法で監視対象にオンボードできます。

  • 拡張スクリプト NetScan (推奨)
  • 手動でリソースを追加する

NetScan の詳細については、次を参照してください。 NetScan の概要。

拡張スクリプトNetScanを使用してリソースを追加する

  1. LogicMonitorで、次の場所に移動します モジュール > 応募者と.
  2. RUCKUS One AP モジュール パッケージを見つけてインストールします。
  3. MFAデバイスに移動する リソース > 追加 > 高度なネットスキャン
  4. NetScan の名前を入力します (例: RUCKUS One AP)。 
  5. NetScan を実行するコレクタを選択します。 
  6. 「拡張スクリプトNetScan」を選択します。 方法 ドロップダウンメニュー。 
  7. 拡張スクリプトセクションで、 デバイスの資格情報 > このスキャンにはカスタム資格情報を使用します。 
  8. NetScan に必要な RUCKUS One 資格情報を提供し、リソースの編成方法を制御するには、次のプロパティを追加します。
プロパティ 詳細説明 必須?
ruckus.one.tenant.idAP を照会する Ruckus One テナント ID。あり
ruckus.one.client.idRuckus One API クライアント ID。あり
ruckus.one.client.keyRuckus One API クライアント シークレット/キー。あり
ruckus.one.regionRuckus One APIのホスト名/リージョン。デフォルトは api.ruckus.cloud 設定されていない場合。オプション
root.folder.nameデバイスが作成される LM 内のルートフォルダパス。デフォルトは Ruckus One です。オプション
skip.device.dedupeに設定され true LM API 経由の重複デバイスチェックをスキップします。デフォルトは false です。オプション
hostname.sourceホスト名の衝突を解決する方法を制御します: lm、logicmonitor、または netscan。オプション
lmapi.timelimit.secLM APIデバイス検索の時間制限(30~120秒)。デフォルトは60秒です。オプション
ruckus.one.ap.venues.csvカスタム会場名を含む CSV ファイルの名前オプション

注意: ruckus.one.tenant.idのいずれかの場合、 ruckus.one.client.id、または ruckus.one.client.key がない場合、スクリプトはエラーをスローし、NetScan は失敗します。

  1. 選択する Groovyスクリプトを埋め込む 次のスクリプトを埋め込みます。

警告: スクリプトを編集しないでください。 編集された拡張スクリプト NetScan はサポートされていません。 LogicMonitor が提供するスクリプトを編集した場合、問題が発生した場合、LogicMonitor サポートはサポートされているスクリプトで編集内容を上書きするよう要求する場合があります。 拡張スクリプト NetScan は、LM Envision リソースの作成を 600 時間あたり 600 以下に制限します。 XNUMX を超えるリソースを作成するには、すべてのリソースが追加されるまで NetScan を XNUMX 時間ごとに繰り返すようにスケジュールします。

/*******************************************************************************
 * © 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. 「スケジュール」セクションで、「この NetScan をスケジュールに従って実行する」を選択します。動的環境の場合、NetScan を 1 時間ごとに実行するようにスケジュールできます。
  2. 選択する Save or 保存して実行

NetScan を実行した後、追加されたリソースの数、または NetScan がリソースを作成しない場合はエラー メッセージの履歴を確認します。

RUCKUS One AP リソースの手動追加

  1. RUCKUS One AP リソース グループを作成します。
    詳細については、を参照してください。 リソースグループの追加
  2. AP を RUCKUS One AP リソース グループにリソースとして追加します。 
  3. リソースまたはリソース グループに次のプロパティを追加するか、設定されていることを確認します。
プロパティ 詳細説明
system.hostnameAP管理IPまたは解決可能なホスト名
ruckus.one.tenant.idテナントID
ruckus.one.client.idOAuthクライアントID
ruckus.one.client.keyOAuthクライアントシークレット
ruckus.one.serialAPシリアル番号
ruckus.one.venue.id会場識別子
ruckus.one.regionAPI リージョン(該当する場合)

プロパティの設定の詳細については、を参照してください。 リソースとインスタンスのプロパティ.

LogicModulesをインポートする

に含まれるすべてのLogicModuleをインポートします RUCKUS One AP モニタリング パッケージ LM Exchange から。

これらのLogicModuleが既に存在する場合は、最新バージョンに更新されていることを確認してください。インポート後、検出とデータ収集が自動的に開始されます。

トラブルシューティング

RUCKUS One AP モニタリング パッケージの導入または操作時に発生する一般的な問題を診断および解決するには、次の表を参照してください。

問題解像度
データは収集されません必要なすべてのプロパティが構成されていることを確認し、コレクターからの送信 HTTPS 接続を確認します。
モジュールが適用されない確認する addCategory_Ruckus_One_AP PropertySource は正しいシステム カテゴリを割り当てています。
認証エラーAPI 資格情報とテナント ID 構成を確認します。

パッケージ内のLogicModules

LogicMonitorのRUCKUS One APモニタリングパッケージは、以下のLogicModuleで構成されています。完全なカバレッジを得るには、以下のLogicModuleがすべてLogicMonitorプラットフォームにインポートされていることを確認してください。

表示名タイプ 詳細説明
Ruckus_One_AP_APIデータソースRUCKUS One クラウド API に接続して、他の RUCKUS One AP モジュールで使用するためにアクセス ポイント、会場のインベントリ、およびヘルス メトリックを収集してキャッシュします。
Ruckus_One_AP_PerformanceデータソースRUCKUS One の会場とシリアル番号に応じてクライアントの数を処理します。
Ruckus_One_AP_SSIDsデータソースRUCKUS One アクセス ポイント上の各 SSID を検出し、SSID ごとのクライアント負荷、トラフィック、RF 品質 (RSSI、SNR、ノイズ フロア) を監視します。
Ruckus_One_AP_RadiosデータソースRUCKUS One アクセス ポイント上の各無線帯域を検出し、無線ごとのクライアント負荷、トラフィック、RF 品質 (RSSI、SNR、ノイズ フロア) を監視します。
addCategory_Ruckus_One_APプロパティソース割り当て system.categories の値 RuckusOneAP RUCKUS One アクセス ポイント デバイスにその他の必要なプロパティを追加します。
Ruckus One WebhooksログソースRUCKUS One からの Webhook イベントを関連する LogicMonitor リソースにマッピングします。

このパッケージの DataSource によって追跡されるさまざまなメトリクスに静的なデータポイントのしきい値を設定する場合、LogicMonitor はテクノロジー所有者のベスト プラクティス KPI 推奨事項に従います。

推奨事項: 必要に応じて、環境固有のニーズに合わせてこれらの事前定義されたしきい値を調整します。 データポイントのしきい値の調整の詳細については、を参照してください。 データポイントの静的しきい値.

14日間フルアクセス LogicMonitor プラットフォーム