Add CUCM plugin, since it was lying around in another repo.
This commit is contained in:
		
							parent
							
								
									0833ae7a16
								
							
						
					
					
						commit
						de64c02488
					
				@ -0,0 +1,68 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# Parses and checks non-phone devices from CUCM.
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
from cmk.base.plugins.agent_based.agent_based_api.v1 import register, Result, Service, State
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Convert JSON entries into dictionaries indexed by name.
 | 
			
		||||
def parse_cucm(string_table):
 | 
			
		||||
    lookup = {}
 | 
			
		||||
 | 
			
		||||
    for row in string_table:
 | 
			
		||||
        device = json.loads(row[0])
 | 
			
		||||
        name = device["name"]
 | 
			
		||||
        lookup[name] = device
 | 
			
		||||
 | 
			
		||||
    return lookup
 | 
			
		||||
 | 
			
		||||
register.agent_section(
 | 
			
		||||
    name="cucm_chk",
 | 
			
		||||
    parse_function=parse_cucm
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Produce a list of services based on the parsed devices.
 | 
			
		||||
def discover_cucm(section):
 | 
			
		||||
    for name, details in sorted(section.items()):
 | 
			
		||||
        model_name = details["model_name"]
 | 
			
		||||
        ip = details.get("ip")
 | 
			
		||||
        gui_name = "%s %s (%s)" % (model_name, name, ip)
 | 
			
		||||
        yield Service(item=gui_name, parameters={"name": name})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Given a specific device, look it up in the parsed devices, and produce
 | 
			
		||||
# results on that service based upon the devices' status.
 | 
			
		||||
def check_cucm(item, params, section):
 | 
			
		||||
    name = params["name"]
 | 
			
		||||
    device = section.get(name)
 | 
			
		||||
 | 
			
		||||
    if device is None:
 | 
			
		||||
        yield Result(state=State.WARN, summary="Not appearing in CUCM API")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    status = device.get("status")
 | 
			
		||||
 | 
			
		||||
    if status is None:
 | 
			
		||||
        yield Result(state=State.WARN, summary="No status for this in CUCM API")
 | 
			
		||||
    elif status == "Registered":
 | 
			
		||||
        yield Result(state=State.OK, summary="Registered")
 | 
			
		||||
    elif ["Unregistered", "Rejected", "PartiallyRegistered", "Unknown"].count(status) == 1:
 | 
			
		||||
        summary = status
 | 
			
		||||
        msg = device.get("status_reason")
 | 
			
		||||
        if msg:
 | 
			
		||||
            summary += " " + msg
 | 
			
		||||
        yield Result(state=State.WARN, summary=summary)
 | 
			
		||||
    else:
 | 
			
		||||
        yield Result(state=State.WARN, summary="Unknown status: %s" % status)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
register.check_plugin(
 | 
			
		||||
    name="cucm_chk",
 | 
			
		||||
    service_name="CUCM %s",
 | 
			
		||||
    discovery_function=discover_cucm,
 | 
			
		||||
    check_function=check_cucm,
 | 
			
		||||
    check_default_parameters={},
 | 
			
		||||
    check_ruleset_name="cucm_chk",
 | 
			
		||||
)
 | 
			
		||||
@ -0,0 +1,44 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
# Parses and inventories phones.
 | 
			
		||||
 | 
			
		||||
# XXX for the inventory plugin, if mac/serial/model is None, do not update the inventory
 | 
			
		||||
# XXX for checkmk, add a last_seen. If last_seen is older than six months, remove it.
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
from cmk.base.plugins.agent_based.agent_based_api.v1 import register, TableRow
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Convert JSON entries into dictionaries indexed by name.
 | 
			
		||||
def parse_cucm(string_table):
 | 
			
		||||
    lookup = {}
 | 
			
		||||
 | 
			
		||||
    for row in string_table:
 | 
			
		||||
        phone = json.loads(row[0])
 | 
			
		||||
        name = phone["name"]
 | 
			
		||||
        lookup[name] = phone
 | 
			
		||||
 | 
			
		||||
    return lookup
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Produce a table of all phones parsed earlier.
 | 
			
		||||
def inventory_cucm(section):
 | 
			
		||||
    path = ["phones"]
 | 
			
		||||
    for name, details in sorted(section.items()):
 | 
			
		||||
        details.pop("name")
 | 
			
		||||
        yield TableRow(
 | 
			
		||||
            path=path,
 | 
			
		||||
            key_columns={"name": name},
 | 
			
		||||
            inventory_columns=details
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
register.agent_section(
 | 
			
		||||
    name="cucm_inv",
 | 
			
		||||
    parse_function=parse_cucm
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
register.inventory_plugin(
 | 
			
		||||
    name="cucm_inv",
 | 
			
		||||
    inventory_function=inventory_cucm,
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										340
									
								
								check_mk-cucm/local/share/check_mk/agents/special/agent_cucm_chk
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										340
									
								
								check_mk-cucm/local/share/check_mk/agents/special/agent_cucm_chk
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,340 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# Contact CUCM to fetch the status of non-phone devices, and return the results
 | 
			
		||||
# of each device as a JSON line.
 | 
			
		||||
#
 | 
			
		||||
# This file imports code from agent_cucm_inv, so much of the important logic
 | 
			
		||||
# is found there.
 | 
			
		||||
 | 
			
		||||
from importlib.util import spec_from_loader, module_from_spec
 | 
			
		||||
from importlib.machinery import SourceFileLoader
 | 
			
		||||
import os, sys, json, urllib.request
 | 
			
		||||
 | 
			
		||||
# Since Python doesn't import files without .py extensions, we need to do a
 | 
			
		||||
# little tapdance to import agent_cucm_inv.
 | 
			
		||||
file = 'agent_cucm_inv'
 | 
			
		||||
path = os.path.dirname(__file__) + '/' + file
 | 
			
		||||
spec = spec_from_loader(file, SourceFileLoader(file, path))
 | 
			
		||||
inv = module_from_spec(spec)
 | 
			
		||||
spec.loader.exec_module(inv)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Call the CUCM RisPort70 API synchronously, using a SOAP query to fetch
 | 
			
		||||
# information about devices matching the requested device type. It returns
 | 
			
		||||
# XML, which we parse.
 | 
			
		||||
#
 | 
			
		||||
# Be aware that the API will return information about a maximum of 2000 devices,
 | 
			
		||||
# and provides no means of pagination. Having more than 2000 non-phone devices
 | 
			
		||||
# would be quite exceptional, so we don't handle that here, but if you ever
 | 
			
		||||
# need to support more than that look into how agent_cucm_inv uses AXL to
 | 
			
		||||
# do pagination.
 | 
			
		||||
#
 | 
			
		||||
# See query_cucm_risport() in agent_cucm_inv for more info.
 | 
			
		||||
def query_cucm(addr, port, user, password, insecure, device):
 | 
			
		||||
    url = 'https://%s:%s/realtimeservice2/services/RISService70/' % (addr, port)
 | 
			
		||||
    headers = [('Content-Type', 'text/plain')]
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        return inv.get_url(url, user, password, insecure, headers, f"""
 | 
			
		||||
            <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 | 
			
		||||
                              xmlns:soap="http://schemas.cisco.com/ast/soap">
 | 
			
		||||
              <soapenv:Header/>
 | 
			
		||||
              <soapenv:Body>
 | 
			
		||||
                <soap:selectCmDevice>
 | 
			
		||||
                  <soap:StateInfo></soap:StateInfo>
 | 
			
		||||
                  <soap:CmSelectionCriteria>
 | 
			
		||||
                    <soap:MaxReturnedDevices>2000</soap:MaxReturnedDevices>
 | 
			
		||||
                    <soap:DeviceClass>{device}</soap:DeviceClass>
 | 
			
		||||
                    <soap:Model>255</soap:Model>
 | 
			
		||||
                    <soap:Status></soap:Status>
 | 
			
		||||
                    <soap:NodeName></soap:NodeName>
 | 
			
		||||
                    <soap:SelectBy>Name</soap:SelectBy>
 | 
			
		||||
                    <soap:SelectItems></soap:SelectItems>
 | 
			
		||||
                    <soap:Protocol>Any</soap:Protocol>
 | 
			
		||||
                    <soap:DownloadStatus>Any</soap:DownloadStatus>
 | 
			
		||||
                  </soap:CmSelectionCriteria>
 | 
			
		||||
                </soap:selectCmDevice>
 | 
			
		||||
              </soapenv:Body>
 | 
			
		||||
            </soapenv:Envelope>
 | 
			
		||||
        """)
 | 
			
		||||
    except urllib.error.HTTPError as e:
 | 
			
		||||
        sys.stderr.write("CUCM error: %s\n" % e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Statuses listed here: https://developer.cisco.com/docs/sxml/#!risport70-api-reference/ReasonCode
 | 
			
		||||
status_reason_lookup = {
 | 
			
		||||
    "0":  None,
 | 
			
		||||
    "1":  "Unknown",
 | 
			
		||||
    "6":  "ConnectivityError",
 | 
			
		||||
    "8":  "DeviceInitiatedReset",
 | 
			
		||||
    "9":  "CallManagerReset",
 | 
			
		||||
    "10": "DeviceUnregistered",
 | 
			
		||||
    "11": "MalformedRegisterMsg",
 | 
			
		||||
    "12": "SCCPDeviceThrottling",
 | 
			
		||||
    "13": "KeepAliveTimeout",
 | 
			
		||||
    "14": "ConfigurationMismatch",
 | 
			
		||||
    "15": "CallManagerRestart",
 | 
			
		||||
    "16": "DuplicateRegistration",
 | 
			
		||||
    "17": "CallManagerApplyConfig",
 | 
			
		||||
    "18": "DeviceNoResponse",
 | 
			
		||||
    "19": "EMLoginLogout",
 | 
			
		||||
    "20": "EMCCLoginLogout",
 | 
			
		||||
    "25": "RegistrationSequenceError",
 | 
			
		||||
    "26": "InvalidCapabilities",
 | 
			
		||||
    "28": "FallbackInitiated",
 | 
			
		||||
    "29": "DeviceSwitch",
 | 
			
		||||
    "30": "DeviceWipe",
 | 
			
		||||
    "31": "DeviceForcedReset",
 | 
			
		||||
    "33": "LowBattery",
 | 
			
		||||
    "34": "ManualPowerOff",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Model names listed here: https://developer.cisco.com/docs/sxml/#!risport70-api-reference/risport70-api-reference
 | 
			
		||||
model_name_lookup = {
 | 
			
		||||
    "1": "Cisco 30 SP+",
 | 
			
		||||
    "2": "Cisco 12 SP+",
 | 
			
		||||
    "3": "Cisco 12 SP",
 | 
			
		||||
    "4": "Cisco 12 S",
 | 
			
		||||
    "5": "Cisco 30 VIP",
 | 
			
		||||
    "6": "Cisco 7910",
 | 
			
		||||
    "7": "Cisco 7960",
 | 
			
		||||
    "8": "Cisco 7940",
 | 
			
		||||
    "9": "Cisco 7935",
 | 
			
		||||
    "10": "Cisco VGC Phone",
 | 
			
		||||
    "11": "Cisco VGC Virtual Phone",
 | 
			
		||||
    "12": "Cisco ATA 186",
 | 
			
		||||
    "15": "EMCC Base Phone",
 | 
			
		||||
    "20": "SCCP Phone",
 | 
			
		||||
    "30": "Analog Access",
 | 
			
		||||
    "40": "Digital Access",
 | 
			
		||||
    "42": "Digital Access+",
 | 
			
		||||
    "43": "Digital Access WS-X6608",
 | 
			
		||||
    "47": "Analog Access WS-X6624",
 | 
			
		||||
    "48": "VGC Gateway",
 | 
			
		||||
    "50": "Conference Bridge",
 | 
			
		||||
    "51": "Conference Bridge WS-X6608",
 | 
			
		||||
    "52": "Cisco IOS Conference Bridge (HDV2)",
 | 
			
		||||
    "53": "Cisco Conference Bridge (WS-SVC-CMM)",
 | 
			
		||||
    "61": "H.323 Phone",
 | 
			
		||||
    "62": "H.323 Gateway",
 | 
			
		||||
    "70": "Music On Hold",
 | 
			
		||||
    "71": "Device Pilot",
 | 
			
		||||
    "72": "CTI Port",
 | 
			
		||||
    "73": "CTI Route Point",
 | 
			
		||||
    "80": "Voice Mail Port",
 | 
			
		||||
    "83": "Cisco IOS Software Media Termination Point (HDV2)",
 | 
			
		||||
    "84": "Cisco Media Server (WS-SVC-CMM-MS)",
 | 
			
		||||
    "85": "Cisco Video Conference Bridge (IPVC-35xx)",
 | 
			
		||||
    "86": "Cisco IOS Heterogeneous Video Conference Bridge",
 | 
			
		||||
    "87": "Cisco IOS Guaranteed Audio Video Conference Bridge",
 | 
			
		||||
    "88": "Cisco IOS Homogeneous Video Conference Bridge",
 | 
			
		||||
    "90": "Route List",
 | 
			
		||||
    "100": "Load Simulator",
 | 
			
		||||
    "110": "Media Termination Point",
 | 
			
		||||
    "111": "Media Termination Point Hardware",
 | 
			
		||||
    "112": "Cisco IOS Media Termination Point (HDV2)",
 | 
			
		||||
    "113": "Cisco Media Termination Point (WS-SVC-CMM)",
 | 
			
		||||
    "115": "Cisco 7941",
 | 
			
		||||
    "119": "Cisco 7971",
 | 
			
		||||
    "120": "MGCP Station",
 | 
			
		||||
    "121": "MGCP Trunk",
 | 
			
		||||
    "122": "GateKeeper",
 | 
			
		||||
    "124": "7914 14-Button Line Expansion Module",
 | 
			
		||||
    "125": "Trunk",
 | 
			
		||||
    "126": "Tone Announcement Player",
 | 
			
		||||
    "131": "SIP Trunk",
 | 
			
		||||
    "132": "SIP Gateway",
 | 
			
		||||
    "133": "WSM Trunk",
 | 
			
		||||
    "134": "Remote Destination Profile",
 | 
			
		||||
    "227": "7915 12-Button Line Expansion Module",
 | 
			
		||||
    "228": "7915 24-Button Line Expansion Module",
 | 
			
		||||
    "229": "7916 12-Button Line Expansion Module",
 | 
			
		||||
    "230": "7916 24-Button Line Expansion Module",
 | 
			
		||||
    "232": "CKEM 36-Button Line Expansion Module",
 | 
			
		||||
    "253": "SPA8800",
 | 
			
		||||
    "254": "Unknown MGCP Gateway",
 | 
			
		||||
    "255": "Unknown",
 | 
			
		||||
    "302": "Cisco 7985",
 | 
			
		||||
    "307": "Cisco 7911",
 | 
			
		||||
    "308": "Cisco 7961G-GE",
 | 
			
		||||
    "309": "Cisco 7941G-GE",
 | 
			
		||||
    "335": "Motorola CN622",
 | 
			
		||||
    "336": "Third-party SIP Device (Basic)",
 | 
			
		||||
    "348": "Cisco 7931",
 | 
			
		||||
    "358": "Cisco Unified Personal Communicator",
 | 
			
		||||
    "365": "Cisco 7921",
 | 
			
		||||
    "369": "Cisco 7906",
 | 
			
		||||
    "374": "Third-party SIP Device (Advanced)",
 | 
			
		||||
    "375": "Cisco TelePresence",
 | 
			
		||||
    "376": "Nokia S60",
 | 
			
		||||
    "404": "Cisco 7962",
 | 
			
		||||
    "412": "Cisco 3951",
 | 
			
		||||
    "431": "Cisco 7937",
 | 
			
		||||
    "434": "Cisco 7942",
 | 
			
		||||
    "435": "Cisco 7945",
 | 
			
		||||
    "436": "Cisco 7965",
 | 
			
		||||
    "437": "Cisco 7975",
 | 
			
		||||
    "446": "Cisco 3911",
 | 
			
		||||
    "468": "Cisco Unified Mobile Communicator",
 | 
			
		||||
    "478": "Cisco TelePresence 1000",
 | 
			
		||||
    "479": "Cisco TelePresence 3000",
 | 
			
		||||
    "480": "Cisco TelePresence 3200",
 | 
			
		||||
    "481": "Cisco TelePresence 500-37",
 | 
			
		||||
    "484": "Cisco 7925",
 | 
			
		||||
    "486": "Syn-Apps Virtual Phone",
 | 
			
		||||
    "493": "Cisco 9971",
 | 
			
		||||
    "495": "Cisco 6921",
 | 
			
		||||
    "496": "Cisco 6941",
 | 
			
		||||
    "497": "Cisco 6961",
 | 
			
		||||
    "503": "Cisco Unified Client Services Framework",
 | 
			
		||||
    "505": "Cisco TelePresence 1300-65",
 | 
			
		||||
    "520": "Cisco TelePresence 1100",
 | 
			
		||||
    "521": "Transnova S3",
 | 
			
		||||
    "522": "BlackBerry MVS VoWifi",
 | 
			
		||||
    "527": "IPTrade TAD",
 | 
			
		||||
    "537": "Cisco 9951",
 | 
			
		||||
    "540": "Cisco 8961",
 | 
			
		||||
    "547": "Cisco 6901",
 | 
			
		||||
    "548": "Cisco 6911",
 | 
			
		||||
    "550": "Cisco ATA 187",
 | 
			
		||||
    "557": "Cisco TelePresence 200",
 | 
			
		||||
    "558": "Cisco TelePresence 400",
 | 
			
		||||
    "562": "Cisco Dual Mode for iPhone",
 | 
			
		||||
    "564": "Cisco 6945",
 | 
			
		||||
    "575": "Cisco Dual Mode for Android",
 | 
			
		||||
    "577": "Cisco 7926",
 | 
			
		||||
    "580": "Cisco E20",
 | 
			
		||||
    "582": "Generic Single Screen Room System",
 | 
			
		||||
    "583": "Generic Multiple Screen Room System",
 | 
			
		||||
    "584": "Cisco TelePresence EX90",
 | 
			
		||||
    "585": "Cisco 8945",
 | 
			
		||||
    "586": "Cisco 8941",
 | 
			
		||||
    "588": "Generic Desktop Video Endpoint",
 | 
			
		||||
    "590": "Cisco TelePresence 500-32",
 | 
			
		||||
    "591": "Cisco TelePresence 1300-47",
 | 
			
		||||
    "592": "Cisco 3905",
 | 
			
		||||
    "593": "Cisco Cius",
 | 
			
		||||
    "594": "VKEM 36-Button Line Expansion Module",
 | 
			
		||||
    "596": "Cisco TelePresence TX1310-65",
 | 
			
		||||
    "597": "Cisco TelePresence MCU",
 | 
			
		||||
    "598": "Ascom IP-DECT Device",
 | 
			
		||||
    "599": "Cisco TelePresence Exchange System",
 | 
			
		||||
    "604": "Cisco TelePresence EX60",
 | 
			
		||||
    "606": "Cisco TelePresence Codec C90",
 | 
			
		||||
    "607": "Cisco TelePresence Codec C60",
 | 
			
		||||
    "608": "Cisco TelePresence Codec C40",
 | 
			
		||||
    "609": "Cisco TelePresence Quick Set C20",
 | 
			
		||||
    "610": "Cisco TelePresence Profile 42 (C20)",
 | 
			
		||||
    "611": "Cisco TelePresence Profile 42 (C60)",
 | 
			
		||||
    "612": "Cisco TelePresence Profile 52 (C40)",
 | 
			
		||||
    "613": "Cisco TelePresence Profile 52 (C60)",
 | 
			
		||||
    "614": "Cisco TelePresence Profile 52 Dual (C60)",
 | 
			
		||||
    "615": "Cisco TelePresence Profile 65 (C60)",
 | 
			
		||||
    "616": "Cisco TelePresence Profile 65 Dual (C90)",
 | 
			
		||||
    "617": "Cisco TelePresence MX200",
 | 
			
		||||
    "619": "Cisco TelePresence TX9000",
 | 
			
		||||
    "621": "Cisco 7821",
 | 
			
		||||
    "620": "Cisco TelePresence TX9200",
 | 
			
		||||
    "622": "Cisco 7841",
 | 
			
		||||
    "623": "Cisco 7861",
 | 
			
		||||
    "626": "Cisco TelePresence SX20",
 | 
			
		||||
    "627": "Cisco TelePresence MX300",
 | 
			
		||||
    "628": "IMS-integrated Mobile (Basic)",
 | 
			
		||||
    "631": "Third-party AS-SIP Endpoint",
 | 
			
		||||
    "632": "Cisco Cius SP",
 | 
			
		||||
    "633": "Cisco TelePresence Profile 42 (C40)",
 | 
			
		||||
    "634": "Cisco VXC 6215",
 | 
			
		||||
    "635": "CTI Remote Device",
 | 
			
		||||
    "640": "Usage Profile",
 | 
			
		||||
    "642": "Carrier-integrated Mobile",
 | 
			
		||||
    "645": "Universal Device Template",
 | 
			
		||||
    "647": "Cisco DX650",
 | 
			
		||||
    "648": "Cisco Unified Communications for RTX",
 | 
			
		||||
    "652": "Cisco Jabber for Tablet",
 | 
			
		||||
    "659": "Cisco 8831",
 | 
			
		||||
    "682": "Cisco TelePresence SX10",
 | 
			
		||||
    "683": "Cisco 8841",
 | 
			
		||||
    "684": "Cisco 8851",
 | 
			
		||||
    "685": "Cisco 8861",
 | 
			
		||||
    "688": "Cisco TelePresence SX80",
 | 
			
		||||
    "689": "Cisco TelePresence MX200 G2",
 | 
			
		||||
    "690": "Cisco TelePresence MX300 G2",
 | 
			
		||||
    "20000": "Cisco 7905",
 | 
			
		||||
    "30002": "Cisco 7920",
 | 
			
		||||
    "30006": "Cisco 7970",
 | 
			
		||||
    "30007": "Cisco 7912",
 | 
			
		||||
    "30008": "Cisco 7902",
 | 
			
		||||
    "30016": "Cisco IP Communicator",
 | 
			
		||||
    "30018": "Cisco 7961",
 | 
			
		||||
    "30019": "Cisco 7936",
 | 
			
		||||
    "30027": "Analog Phone",
 | 
			
		||||
    "30028": "ISDN BRI Phone",
 | 
			
		||||
    "30032": "SCCP gateway virtual phone",
 | 
			
		||||
    "30035": "IP-STE",
 | 
			
		||||
    "36041": "Cisco TelePresence Conductor",
 | 
			
		||||
    "36042": "Cisco DX80",
 | 
			
		||||
    "36043": "Cisco DX70",
 | 
			
		||||
    "36049": "BEKEM 36-Button Line Expansion Module",
 | 
			
		||||
    "36207": "Cisco TelePresence MX700",
 | 
			
		||||
    "36208": "Cisco TelePresence MX800",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Given CUCM XML, use XPath to extract relevant details for each device
 | 
			
		||||
# searching based on device type. Return a list of devices' information.
 | 
			
		||||
def get_device_details(xml, device):
 | 
			
		||||
    namespace = {"ns1": "http://schemas.cisco.com/ast/soap"}
 | 
			
		||||
    items = xml.findall(f".//ns1:DeviceClass[.='{device}']/..", namespace)
 | 
			
		||||
 | 
			
		||||
    names_seen = {}
 | 
			
		||||
    trunk_details = []
 | 
			
		||||
    for item in items:
 | 
			
		||||
        ip     = item.find(".//ns1:IP",  namespace).text
 | 
			
		||||
        name   = item.find("ns1:Name",   namespace).text
 | 
			
		||||
        model  = item.find("ns1:Model",  namespace).text
 | 
			
		||||
        status = item.find("ns1:Status", namespace).text
 | 
			
		||||
        reason = item.find("ns1:StatusReason", namespace).text
 | 
			
		||||
 | 
			
		||||
        if not names_seen.get(name):
 | 
			
		||||
            trunk_details.append({
 | 
			
		||||
                "ip": ip,
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "status": status,
 | 
			
		||||
                "status_reason": status_reason_lookup.get(reason),
 | 
			
		||||
                "type": device,
 | 
			
		||||
                "model_name": model_name_lookup.get(model) or "Unknown"
 | 
			
		||||
            })
 | 
			
		||||
            names_seen[name] = True
 | 
			
		||||
 | 
			
		||||
    return trunk_details
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Contact CUCM and query it for device information for the following device
 | 
			
		||||
# types: SIP trunks, hunt lists, H323 and media resources (e.g. IVR). Return
 | 
			
		||||
# a list for devices' information.
 | 
			
		||||
def get_devices(addr, port, user, password, insecure):
 | 
			
		||||
    devices = []
 | 
			
		||||
 | 
			
		||||
    for device in ["SIPTrunk", "MediaResources", "H323", "HuntList"]:
 | 
			
		||||
        cucm_xml = query_cucm(addr, port, user, password, insecure, device)
 | 
			
		||||
        details = get_device_details(cucm_xml, device)
 | 
			
		||||
        devices.extend(details)
 | 
			
		||||
 | 
			
		||||
    return devices
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Parse args, contact CUCM, check status of non-hone devices, and then print
 | 
			
		||||
# results
 | 
			
		||||
def main(argv=None):
 | 
			
		||||
    if argv is None:
 | 
			
		||||
        argv = sys.argv[1:]
 | 
			
		||||
 | 
			
		||||
    args = inv.parse_arguments(argv)
 | 
			
		||||
    devices = get_devices(args.hostname, args.port, args.user, args.password,
 | 
			
		||||
                          args.insecure)
 | 
			
		||||
    inv.print_out(devices, "cucm_chk")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    sys.exit(main())
 | 
			
		||||
							
								
								
									
										434
									
								
								check_mk-cucm/local/share/check_mk/agents/special/agent_cucm_inv
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										434
									
								
								check_mk-cucm/local/share/check_mk/agents/special/agent_cucm_inv
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,434 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# Given a CUCM host, query the CUCM to get a complete list of phones, then
 | 
			
		||||
# query all those phones concurrently for additional details, then print
 | 
			
		||||
# the results out in a format CheckMK understands.
 | 
			
		||||
#
 | 
			
		||||
# Run the command on the console for a complete list of options.
 | 
			
		||||
#
 | 
			
		||||
# This script is designed to work with up to 90K phones, and ideally below 10K.
 | 
			
		||||
# If more than 10K phones are queried, this script should be modified to
 | 
			
		||||
# perform connection reuse to improve performance. Beyond 90K support for
 | 
			
		||||
# paginating the AXL API must be added.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
phone_query_timeout = 10    # max time to query a single phone
 | 
			
		||||
phone_queries_timeout = 45  # max time to query all phones
 | 
			
		||||
cucm_page_size = 1000       # CUCM will not return pages larger than 2000.
 | 
			
		||||
                            # Larger page sizes cause notably longer queries,
 | 
			
		||||
                            # so a default of 1000 devices per query is a
 | 
			
		||||
                            # safer number.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import urllib.request, base64, sys, argparse, asyncio, re, json, ssl, html
 | 
			
		||||
from xml.etree import ElementTree
 | 
			
		||||
from textwrap import wrap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Create a TLS context for client connections.
 | 
			
		||||
def create_ssl_ctx():
 | 
			
		||||
    ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
 | 
			
		||||
    ctx.check_hostname = False
 | 
			
		||||
    ctx.verify_mode = ssl.CERT_NONE
 | 
			
		||||
    return ctx
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Typical GET or POST HTTP with Basic auth (using user and password
 | 
			
		||||
# credientials). Returns data structure parsed from XML.
 | 
			
		||||
def get_url(url, user, password, insecure, headers, data):
 | 
			
		||||
    request = urllib.request.Request(url, data=bytes(data, 'ascii'))
 | 
			
		||||
 | 
			
		||||
    for header, value in headers:
 | 
			
		||||
        request.add_header(header, value)
 | 
			
		||||
 | 
			
		||||
    if user and password:
 | 
			
		||||
        auth_str = base64.b64encode(bytes('%s:%s' % (user, password), 'ascii'))
 | 
			
		||||
        request.add_header('Authorization', 'Basic %s' % auth_str.decode('utf-8'))
 | 
			
		||||
 | 
			
		||||
    ctx = None
 | 
			
		||||
    if insecure:
 | 
			
		||||
        ctx = create_ssl_ctx()
 | 
			
		||||
 | 
			
		||||
    with urllib.request.urlopen(request, context=ctx) as conn:
 | 
			
		||||
        xml_data = conn.read()
 | 
			
		||||
 | 
			
		||||
    return ElementTree.fromstring(xml_data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Call the CUCM AXL API synchronously, using a SOAP query to fetch the names of
 | 
			
		||||
# all phones. It returns XML, which we parse. We call the AXL API because the
 | 
			
		||||
# RisPort70 API (see query_cucm_risport() below) does not support pagination,
 | 
			
		||||
# so we need to get a full list of phone names from AXL first, then do multiple
 | 
			
		||||
# queries on RisPort70 using subsets of the phone name list found from AXL.
 | 
			
		||||
#
 | 
			
		||||
# The AXL API has a return limit of 8MB, which is around 90K phones, so we
 | 
			
		||||
# don't bother paginating the AXL API itself; if more than that is needed, add
 | 
			
		||||
# pagination here.
 | 
			
		||||
#
 | 
			
		||||
# References:
 | 
			
		||||
# https://developer.cisco.com/docs/axl/
 | 
			
		||||
# https://github.com/reillychase/How-to-return-Cisco-RIS-with-more-than-1000-results/blob/master/main.py
 | 
			
		||||
def query_cucm_axl(addr, port, user, password, insecure):
 | 
			
		||||
    url = 'https://%s:%s/axl/' % (addr, port)
 | 
			
		||||
    headers = [
 | 
			
		||||
        ('Content-Type', 'text/xml'),
 | 
			
		||||
        ('Accept', 'text/xml'),
 | 
			
		||||
        ('SOAPAction', 'CUCM:DB ver=12.5'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        return get_url(url, user, password, insecure, headers, """
 | 
			
		||||
            <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 | 
			
		||||
                              xmlns:ns="http://www.cisco.com/AXL/API/12.5">
 | 
			
		||||
               <soapenv:Header/>
 | 
			
		||||
               <soapenv:Body>
 | 
			
		||||
                  <ns:listPhone>
 | 
			
		||||
                     <searchCriteria>
 | 
			
		||||
                       <name>%</name>
 | 
			
		||||
                     </searchCriteria>
 | 
			
		||||
                     <returnedTags>
 | 
			
		||||
                        <name/>
 | 
			
		||||
                     </returnedTags>
 | 
			
		||||
                  </ns:listPhone>
 | 
			
		||||
               </soapenv:Body>
 | 
			
		||||
            </soapenv:Envelope>
 | 
			
		||||
        """)
 | 
			
		||||
    except urllib.error.HTTPError as e:
 | 
			
		||||
        sys.stderr.write("AXL error: %s\n" % e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Call the CUCM RisPort70 API synchronously, using a SOAP query to fetch
 | 
			
		||||
# information about the phones with ids listed in the phone_ids arg. It returns
 | 
			
		||||
# XML, which we parse.
 | 
			
		||||
#
 | 
			
		||||
# Be aware that the API will return information about a maximum of 2000 devices,
 | 
			
		||||
# and provides no means of pagination. In order to do pagination, we first need
 | 
			
		||||
# to query the AXL API for a list of phone names, then all this function
 | 
			
		||||
# repeatedly with a different subset of 2000 phones from that complete list.
 | 
			
		||||
#
 | 
			
		||||
# Although this function will allow pages for 2000 devices, it's recommended to
 | 
			
		||||
# use less for each call to avoid timeouts. The default maximum size of
 | 
			
		||||
# phone_ids is 1000, although this can be varied by changing the cucm_page_size
 | 
			
		||||
# at the top of this file.
 | 
			
		||||
#
 | 
			
		||||
# References:
 | 
			
		||||
# https://developer.cisco.com/docs/sxml/#!risport70-api-reference
 | 
			
		||||
# https://paultursan.com/2018/12/getting-cucm-real-time-data-via-risport70-with-python-and-zeep-cisco-serviceability-api/
 | 
			
		||||
def query_cucm_risport(addr, port, user, password, insecure, phone_ids):
 | 
			
		||||
    assert len(phone_ids) <= 2000
 | 
			
		||||
 | 
			
		||||
    url = 'https://%s:%s/realtimeservice2/services/RISService70/' % (addr, port)
 | 
			
		||||
    headers = [('Content-Type', 'text/plain')]
 | 
			
		||||
 | 
			
		||||
    id_query = ''.join([f'<soap:item><soap:Item>{id}</soap:Item></soap:item>' for id in phone_ids])
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        return get_url(url, user, password, insecure, headers, f"""
 | 
			
		||||
            <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 | 
			
		||||
                              xmlns:soap="http://schemas.cisco.com/ast/soap">
 | 
			
		||||
               <soapenv:Header/>
 | 
			
		||||
               <soapenv:Body>
 | 
			
		||||
                  <soap:selectCmDevice>
 | 
			
		||||
                     <soap:StateInfo></soap:StateInfo>
 | 
			
		||||
                     <soap:CmSelectionCriteria>
 | 
			
		||||
                        <soap:MaxReturnedDevices>2000</soap:MaxReturnedDevices>
 | 
			
		||||
                        <soap:DeviceClass>Any</soap:DeviceClass>
 | 
			
		||||
                        <soap:Model>255</soap:Model>
 | 
			
		||||
                        <soap:Status>Registered</soap:Status>
 | 
			
		||||
                        <soap:NodeName></soap:NodeName>
 | 
			
		||||
                        <soap:SelectBy>Name</soap:SelectBy>
 | 
			
		||||
                        <soap:SelectItems>
 | 
			
		||||
                           {id_query}
 | 
			
		||||
                        </soap:SelectItems>
 | 
			
		||||
                        <soap:Protocol>Any</soap:Protocol>
 | 
			
		||||
                        <soap:DownloadStatus>Any</soap:DownloadStatus>
 | 
			
		||||
                     </soap:CmSelectionCriteria>
 | 
			
		||||
                  </soap:selectCmDevice>
 | 
			
		||||
               </soapenv:Body>
 | 
			
		||||
            </soapenv:Envelope>
 | 
			
		||||
        """)
 | 
			
		||||
    except urllib.error.HTTPError as e:
 | 
			
		||||
        sys.stderr.write("CUCM error: %s\n" % e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Given AXL XML, use XPath to extract names for all phones.
 | 
			
		||||
def get_phone_ids(xml):
 | 
			
		||||
    # should this be ns2?
 | 
			
		||||
    namespace = {'ns': 'http://www.cisco.com/AXL/API/12.5'}
 | 
			
		||||
    items = xml.findall(".//phone/name", namespace)
 | 
			
		||||
 | 
			
		||||
    names = []
 | 
			
		||||
    for item in items:
 | 
			
		||||
        names.append(item.text)
 | 
			
		||||
 | 
			
		||||
    return names
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Given CUCM XML, use XPath to extract a bunch of details for each phone.
 | 
			
		||||
def get_phone_details(xml):
 | 
			
		||||
    namespace = {'ns1': 'http://schemas.cisco.com/ast/soap'}
 | 
			
		||||
    items = xml.findall(".//ns1:DeviceClass[.='Phone']/..", namespace)
 | 
			
		||||
 | 
			
		||||
    names_seen = {}
 | 
			
		||||
    phone_details = []
 | 
			
		||||
    for item in items:
 | 
			
		||||
        ip          = item.find('.//ns1:IP',       namespace).text
 | 
			
		||||
        name        = item.find('ns1:Name',        namespace).text
 | 
			
		||||
        dir_num     = item.find('ns1:DirNumber',   namespace).text
 | 
			
		||||
        description = item.find('ns1:Description', namespace).text
 | 
			
		||||
        user        = item.find('ns1:LoginUserId', namespace).text
 | 
			
		||||
 | 
			
		||||
        # These come with a -Registered on the end of the numbers.
 | 
			
		||||
        # Since all numbers we get from CUCM are registered, there's no
 | 
			
		||||
        # need for the -Registered, and we cut it off here.
 | 
			
		||||
        if dir_num:
 | 
			
		||||
            dir_num = dir_num.split('-')[0]
 | 
			
		||||
 | 
			
		||||
        if not names_seen.get(name):
 | 
			
		||||
            phone_details.append((name, ip, dir_num, user, description))
 | 
			
		||||
            names_seen[name] = True
 | 
			
		||||
 | 
			
		||||
    return phone_details
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# If a phone (possibly) returns XML, attempt to extract the MAC, serial and
 | 
			
		||||
# model. We're using regex here, instead of full-blown XML parsing, to minimize
 | 
			
		||||
# the time and GC garbage generated.
 | 
			
		||||
def get_phone_details_from_xml(data):
 | 
			
		||||
    # if the HTTP server didn't return a 200...
 | 
			
		||||
    if data.find('200 OK') == -1:
 | 
			
		||||
        return None, None, None
 | 
			
		||||
 | 
			
		||||
    # attempt to extract info from XML
 | 
			
		||||
    mac    = re.search('<MACAddress>(.+)</MACAddress>',     data)
 | 
			
		||||
    serial = re.search('<serialNumber>(.+)</serialNumber>', data)
 | 
			
		||||
    model  = re.search('<modelNumber>(.+)</modelNumber>',   data)
 | 
			
		||||
 | 
			
		||||
    mac_str = mac    and normalize_mac(html.unescape(mac[1]))
 | 
			
		||||
    ser_str = serial and html.unescape(serial[1])
 | 
			
		||||
    mod_str = model  and html.unescape(model[1])
 | 
			
		||||
 | 
			
		||||
    return mac_str, ser_str, mod_str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# If a phone (possibly) returns HTML, attempt to extract the MAC, serial and
 | 
			
		||||
# model. We use regex here for the same reason we use it in
 | 
			
		||||
# get_phone_details_from_xml().
 | 
			
		||||
def get_phone_details_from_html(data):
 | 
			
		||||
    if data.find('200 OK') == -1:
 | 
			
		||||
        return None, None, None
 | 
			
		||||
 | 
			
		||||
    mac = None
 | 
			
		||||
    serial = None
 | 
			
		||||
    model = None
 | 
			
		||||
 | 
			
		||||
    # attempt to extract info from HTML
 | 
			
		||||
    matches = re.findall('<b>\s*(.*?)\s*</b>', data, re.M | re.I)
 | 
			
		||||
    for i, txt in enumerate(matches):
 | 
			
		||||
        txt = txt.lower()
 | 
			
		||||
        if   not mac    and txt == 'mac address':
 | 
			
		||||
            mac    = normalize_mac(html.unescape(matches[i + 1]))
 | 
			
		||||
        elif not serial and txt == 'serial number':
 | 
			
		||||
            serial = html.unescape(matches[i + 1])
 | 
			
		||||
        elif not model  and txt == 'model number':
 | 
			
		||||
            model  = html.unescape(matches[i + 1])
 | 
			
		||||
        elif mac and serial and model:
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    return mac, serial, model
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Different phones return MACs in different formats. We convert them to a single
 | 
			
		||||
# canonical format here.
 | 
			
		||||
def normalize_mac(mac):
 | 
			
		||||
    if mac.find(":") != -1:
 | 
			
		||||
        return mac.lower()
 | 
			
		||||
    else:
 | 
			
		||||
        return ":".join(wrap(mac, 2)).lower()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Create a new HTTP/HTTPS connection, send a request, and extract any results.
 | 
			
		||||
async def get_async_url(ip, url, insecure=False):
 | 
			
		||||
    ctx = None
 | 
			
		||||
    port = 80
 | 
			
		||||
    if not insecure:
 | 
			
		||||
        ctx = create_ssl_ctx()
 | 
			
		||||
        port = 443
 | 
			
		||||
 | 
			
		||||
#   XXX switch from 127.0.0.1:8081 to ip:port
 | 
			
		||||
#    future = asyncio.open_connection('127.0.0.1', 8081, ssl=ctx)
 | 
			
		||||
    future = asyncio.open_connection(ip, port, ssl=ctx)
 | 
			
		||||
    reader, writer = await asyncio.wait_for(future, timeout=phone_query_timeout)
 | 
			
		||||
    query = f'GET {url} HTTP/1.1\r\nHost: {ip}\r\nConnection: close\r\n\r\n'
 | 
			
		||||
    writer.write(query.encode())
 | 
			
		||||
    await writer.drain()
 | 
			
		||||
 | 
			
		||||
    data = ''
 | 
			
		||||
    while not reader.at_eof():
 | 
			
		||||
        raw = await reader.read(-1)
 | 
			
		||||
        data += raw.decode()
 | 
			
		||||
 | 
			
		||||
    writer.close()
 | 
			
		||||
    await writer.wait_closed()
 | 
			
		||||
 | 
			
		||||
    return data
 | 
			
		||||
 | 
			
		||||
# Asynchronously contact the HTTP server in a phone. There are several
 | 
			
		||||
# different URLs that might return information, depending on the model of phone.
 | 
			
		||||
# To fetch the MAC and serial details we want requires us to potentially call
 | 
			
		||||
# all endpoints until we get some results. Be aware that some phone HTTP servers
 | 
			
		||||
# return 200 (and empty results) if we call the wrong URL for that model.
 | 
			
		||||
#
 | 
			
		||||
# We attempt to contact the phone using HTTPS first, falling back to HTTP if
 | 
			
		||||
# attempts with HTTPS failed.
 | 
			
		||||
#
 | 
			
		||||
# Originally we tried to take advantage of HTTP connection reuse, but this was
 | 
			
		||||
# causing some problems, and it wasn't worth the effort to handle the edge
 | 
			
		||||
# cases. Now we always make a new connection per request, even when it's
 | 
			
		||||
# multiple URLs on the same IP. If additional performance is ever needed, HTTP
 | 
			
		||||
# connection reuse is worth adding; depending on the latency it can easily
 | 
			
		||||
# 2x+ request rate.
 | 
			
		||||
async def query_phone_info_now(details):
 | 
			
		||||
    name, ip, dir_num, user, description = details
 | 
			
		||||
    mac = None
 | 
			
		||||
    serial = None
 | 
			
		||||
    model = None
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        data = await get_async_url(ip, '/DeviceInformationX')
 | 
			
		||||
        mac, serial, model = get_phone_details_from_xml(data)
 | 
			
		||||
 | 
			
		||||
        if not mac:
 | 
			
		||||
            data = await get_async_url(ip, '/Device_Information.html')
 | 
			
		||||
            mac, serial, model = get_phone_details_from_html(data)
 | 
			
		||||
 | 
			
		||||
        if not mac:
 | 
			
		||||
            data = await get_async_url(ip, '/CGI/Java/Serviceability?adapter=device.statistics.device')
 | 
			
		||||
            mac, serial, model = get_phone_details_from_html(data)
 | 
			
		||||
 | 
			
		||||
        if not mac:
 | 
			
		||||
            data = await get_async_url(ip, '/')
 | 
			
		||||
            mac, serial, model = get_phone_details_from_html(data)
 | 
			
		||||
    except (ConnectionRefusedError, asyncio.TimeoutError, ssl.SSLError):
 | 
			
		||||
        try:
 | 
			
		||||
            if not mac:
 | 
			
		||||
                data = await get_async_url(ip, '/DeviceInformationX', True)
 | 
			
		||||
                mac, serial, model = get_phone_details_from_xml(data)
 | 
			
		||||
 | 
			
		||||
            if not mac:
 | 
			
		||||
                data = await get_async_url(ip, '/Device_Information.html', True)
 | 
			
		||||
                mac, serial, model = get_phone_details_from_html(data)
 | 
			
		||||
 | 
			
		||||
            if not mac:
 | 
			
		||||
                data = await get_async_url(ip, '/CGI/Java/Serviceability?adapter=device.statistics.device', True)
 | 
			
		||||
                mac, serial, model = get_phone_details_from_html(data)
 | 
			
		||||
 | 
			
		||||
            if not mac:
 | 
			
		||||
                data = await get_async_url(ip, '/', True)
 | 
			
		||||
                mac, serial, model = get_phone_details_from_html(data)
 | 
			
		||||
        except (ConnectionRefusedError, asyncio.TimeoutError):
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "name": name,
 | 
			
		||||
        "ip": ip,
 | 
			
		||||
        "mac": mac,
 | 
			
		||||
        "serial": serial,
 | 
			
		||||
        "dir_num": dir_num,
 | 
			
		||||
        "model": model,
 | 
			
		||||
        "user": user,
 | 
			
		||||
        "description": description
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# This functions job is solely to keep a limit on the concurrent number of
 | 
			
		||||
# connections made to the phones. Without this limit we'd quickly run out of
 | 
			
		||||
# spare sockets when dealing with large numbers of phones.
 | 
			
		||||
async def query_phone_info(details, semaphore):
 | 
			
		||||
    async with semaphore:
 | 
			
		||||
        return await query_phone_info_now(details)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Given information about a list of phones (specifically, their IP addresses),
 | 
			
		||||
# we call the HTTP server on each phone to extract the MAC and serial. We
 | 
			
		||||
# return with a list of dicts containing information about all phones.
 | 
			
		||||
# Contacting thousands of phones serially would take too long, so we keep 200
 | 
			
		||||
# concurrent calls in-flight to the phones to shorten all querying to a few
 | 
			
		||||
# seconds.
 | 
			
		||||
async def query_phones(details):
 | 
			
		||||
    sem = asyncio.Semaphore(200)
 | 
			
		||||
    tasks = map(lambda d: asyncio.create_task(query_phone_info(d, sem)), details)
 | 
			
		||||
    done, pending = await asyncio.wait(tasks, timeout=phone_queries_timeout)
 | 
			
		||||
    # we silently ignore pending for now
 | 
			
		||||
    return map(lambda f: f.result(), done)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Given an array of phone names, do paginated queries to the CUCM for phone
 | 
			
		||||
# information, then asynchronously query all the phones. While the CUCM has
 | 
			
		||||
# most of the information we want about a phone, it critically lacks the serial
 | 
			
		||||
# and MAC of the phone, which is why we need to fetch the details from the
 | 
			
		||||
# phone itself over an HTTP server each phone has.
 | 
			
		||||
def get_phones(addr, port, user, password, insecure):
 | 
			
		||||
    axl_xml = query_cucm_axl(addr, port, user, password, insecure)
 | 
			
		||||
    phone_ids = get_phone_ids(axl_xml)
 | 
			
		||||
 | 
			
		||||
    phone_details = []
 | 
			
		||||
    page_size = cucm_page_size
 | 
			
		||||
    for i in range(0, len(phone_ids), page_size):
 | 
			
		||||
        ids = phone_ids[i:i + page_size]
 | 
			
		||||
        cucm_xml = query_cucm_risport(addr, port, user, password, insecure, ids)
 | 
			
		||||
        details = get_phone_details(cucm_xml)
 | 
			
		||||
        phone_details.extend(details)
 | 
			
		||||
 | 
			
		||||
    return asyncio.run(query_phones(phone_details))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Print out all our results in a format that CheckMK understands. Most of our
 | 
			
		||||
# output are in JSON rows.
 | 
			
		||||
def print_out(device_info, agent_name):
 | 
			
		||||
    sys.stdout.write(f"<<<{agent_name}:sep(0)>>>\n")
 | 
			
		||||
    device_info = list(device_info)
 | 
			
		||||
    device_info.sort(key=lambda d: d["ip"])
 | 
			
		||||
    for entry in device_info:
 | 
			
		||||
        sys.stdout.write("%s\n" % json.dumps(entry))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Parse the command-line arguments. We have several options, but hostname is
 | 
			
		||||
# always required. Print out help to console if we get no args.
 | 
			
		||||
def parse_arguments(argv):
 | 
			
		||||
    parser = argparse.ArgumentParser()
 | 
			
		||||
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-u", "--user", default=None, help="Username for CUCM login"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-s", "--password", default=None, help="Password for CUCM login"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-p", "--port", default=443, type=int, help="Use alternative port (default: 443)"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "hostname", metavar="HOSTNAME", help="Hostname of the CUCM to query."
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-k", "--insecure", default=False, help="Skip certificate verification",
 | 
			
		||||
        action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return parser.parse_args(argv)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Parse args, contact CUCM, query phones, and then print results
 | 
			
		||||
def main(argv=None):
 | 
			
		||||
    if argv is None:
 | 
			
		||||
        argv = sys.argv[1:]
 | 
			
		||||
 | 
			
		||||
    args = parse_arguments(argv)
 | 
			
		||||
    phones = get_phones(args.hostname, args.port, args.user, args.password,
 | 
			
		||||
                        args.insecure)
 | 
			
		||||
    print_out(phones, 'cucm_inv')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    sys.exit(main())
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								check_mk-cucm/local/share/check_mk/checks/agent_cucm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								check_mk-cucm/local/share/check_mk/checks/agent_cucm
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def agent_cucm_arguments(params, hostname, ipaddress):
 | 
			
		||||
    args = []
 | 
			
		||||
 | 
			
		||||
    if "user" in params:
 | 
			
		||||
        args += ["-u", params["user"]]
 | 
			
		||||
 | 
			
		||||
    if "password" in params:
 | 
			
		||||
        args += ["-s", params["password"]]
 | 
			
		||||
 | 
			
		||||
    if "port" in params:
 | 
			
		||||
        args += ["-p", params["port"]]
 | 
			
		||||
 | 
			
		||||
    if params.get("insecure"):
 | 
			
		||||
        args.append("-k")
 | 
			
		||||
 | 
			
		||||
    args.append(params["instance"])
 | 
			
		||||
 | 
			
		||||
    return args
 | 
			
		||||
 | 
			
		||||
special_agent_info["cucm_chk"] = agent_cucm_arguments
 | 
			
		||||
special_agent_info["cucm_inv"] = agent_cucm_arguments
 | 
			
		||||
							
								
								
									
										19
									
								
								check_mk-cucm/local/share/check_mk/web/plugins/views/cucm.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								check_mk-cucm/local/share/check_mk/web/plugins/views/cucm.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
from cmk.gui.plugins.views import inventory_displayhints
 | 
			
		||||
from cmk.gui.i18n import _l
 | 
			
		||||
 | 
			
		||||
# inventory list for phones found in CUCM
 | 
			
		||||
 | 
			
		||||
inventory_displayhints.update({
 | 
			
		||||
    ".phones:": {
 | 
			
		||||
        "title": _l("Phones"),
 | 
			
		||||
        "keyorder": ["name", "user", "dir_num", "ip", "model", "serial", "mac", "description"],
 | 
			
		||||
    },
 | 
			
		||||
    ".phones:*.name": {"title": _l("Name")},
 | 
			
		||||
    ".phones:*.user": {"title": _l("User")},
 | 
			
		||||
    ".phones:*.dir_num": {"title": _l("Dir Num")},
 | 
			
		||||
    ".phones:*.ip": {"title": _l("IP Addr")},
 | 
			
		||||
    ".phones:*.model": {"title": _l("Model")},
 | 
			
		||||
    ".phones:*.serial": {"title": _l("Serial")},
 | 
			
		||||
    ".phones:*.mac": {"title": _l("MAC Addr")},
 | 
			
		||||
    ".phones:*.description": {"title": _l("Description")},
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										120
									
								
								check_mk-cucm/local/share/check_mk/web/plugins/wato/cucm.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								check_mk-cucm/local/share/check_mk/web/plugins/wato/cucm.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,120 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# GUI configuration pages to set up inventorying and checks done by the CUCM
 | 
			
		||||
# agent to CUCM. These two pages are for giving the agent the necessary details
 | 
			
		||||
# to connect to CUCM (e.g. IP address, user, login, etc).
 | 
			
		||||
#
 | 
			
		||||
# Ideally, we'd have a single page to configure both the inventorying and
 | 
			
		||||
# checks, since both contact the same CUCM instance. Unfortunately, I didn't
 | 
			
		||||
# find a clean way to do it, so we're left with two identical GUI pages that
 | 
			
		||||
# take identical information. At least we manage to share most of the code
 | 
			
		||||
# here by taking a deep copy and modifying the title.
 | 
			
		||||
 | 
			
		||||
import copy
 | 
			
		||||
from cmk.gui.i18n import _
 | 
			
		||||
from cmk.gui.plugins.wato.utils import (
 | 
			
		||||
    rulespec_registry,
 | 
			
		||||
    HostRulespec,
 | 
			
		||||
    RulespecGroupCheckParametersHardware
 | 
			
		||||
)
 | 
			
		||||
from cmk.gui.plugins.wato.inventory import RulespecGroupInventory
 | 
			
		||||
from cmk.gui.watolib.rulespecs import Rulespec
 | 
			
		||||
from cmk.gui.valuespec import (
 | 
			
		||||
    Dictionary,
 | 
			
		||||
    TextInput,
 | 
			
		||||
    Hostname,
 | 
			
		||||
    NetworkPort,
 | 
			
		||||
    Password,
 | 
			
		||||
    TextAscii,
 | 
			
		||||
    FixedValue
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# GUI config page for inventory.
 | 
			
		||||
def _valuespec_special_agents_cucm_inv():
 | 
			
		||||
    return Dictionary(
 | 
			
		||||
        title=_("CUCM inventory"),
 | 
			
		||||
        help=_(""),
 | 
			
		||||
        optional_keys=["port", "user", "password", "insecure"],
 | 
			
		||||
        elements=[
 | 
			
		||||
            (
 | 
			
		||||
                "instance",
 | 
			
		||||
                Hostname(
 | 
			
		||||
                    title=_("Hostname"),
 | 
			
		||||
                    help=_(
 | 
			
		||||
                        "Host of CUCM host for query"
 | 
			
		||||
                    ),
 | 
			
		||||
                    allow_empty=False,
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "port",
 | 
			
		||||
                NetworkPort(
 | 
			
		||||
                    title=_("Port"),
 | 
			
		||||
                    help=_(
 | 
			
		||||
                        "Port of CUCM host for query"
 | 
			
		||||
                    ),
 | 
			
		||||
                    minvalue=1,
 | 
			
		||||
                    default_value=443,
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "user",
 | 
			
		||||
                TextInput(
 | 
			
		||||
                    title=_("Username"),
 | 
			
		||||
                    help=_(
 | 
			
		||||
                        "Username used when querying CUCM"
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "password",
 | 
			
		||||
                Password(
 | 
			
		||||
                    title=_("Password"),
 | 
			
		||||
                    help=_(
 | 
			
		||||
                        "Password used when querying CUCM"
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "insecure",
 | 
			
		||||
                FixedValue(
 | 
			
		||||
                    True,
 | 
			
		||||
                    title=_("Insecure"),
 | 
			
		||||
                    totext=_("Disable SSL certificate verification"),
 | 
			
		||||
                    help=_(
 | 
			
		||||
                        "Ignore unverified HTTPS request warnings when contacting CUCM"
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# GUI config page for checks. We do a deep copy of the above function and just
 | 
			
		||||
# change the title. A bit hackish since we're changing a private attribute.
 | 
			
		||||
def _valuespec_special_agents_cucm_chk():
 | 
			
		||||
   inv_spec = _valuespec_special_agents_cucm_inv()
 | 
			
		||||
   chk_spec = copy.deepcopy(inv_spec)
 | 
			
		||||
   chk_spec._title=_("CUCM checks")
 | 
			
		||||
   return chk_spec
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
rulespec_registry.register(
 | 
			
		||||
    HostRulespec(
 | 
			
		||||
        factory_default=Rulespec.FACTORY_DEFAULT_UNUSED,
 | 
			
		||||
        name="special_agents:cucm_inv",
 | 
			
		||||
        group=RulespecGroupInventory,
 | 
			
		||||
        valuespec=_valuespec_special_agents_cucm_inv,
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
rulespec_registry.register(
 | 
			
		||||
    HostRulespec(
 | 
			
		||||
        factory_default=Rulespec.FACTORY_DEFAULT_UNUSED,
 | 
			
		||||
        name="special_agents:cucm_chk",
 | 
			
		||||
        group=RulespecGroupCheckParametersHardware,
 | 
			
		||||
        valuespec=_valuespec_special_agents_cucm_chk,
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user