From 7ad0ad33c86b14ae1b35967b7bf9ca73c2aaceec Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Wed, 1 Oct 2025 15:03:18 +0200 Subject: [PATCH] Port CUCM plugin forward to CheckMK 2.4. --- .gitignore | 2 + cucm/2.2/CUCM-0.3.3.mkp | Bin 0 -> 11686 bytes .../base/plugins/agent_based/cucm_chk.py | 0 .../base/plugins/agent_based/cucm_inv.py | 0 .../check_mk/agents/special/agent_cucm_chk | 0 .../check_mk/agents/special/agent_cucm_inv | 0 .../local/share/check_mk/checks/agent_cucm | 0 .../share/check_mk/web/plugins/views/cucm.py | 0 .../share/check_mk/web/plugins/wato/cucm.py | 0 .../plugins/cucm/agent_based/cucm_chk.py | 71 +++ .../plugins/cucm/agent_based/cucm_inv.py | 45 ++ .../plugins/cucm/libexec/agent_cucm_chk | 341 ++++++++++++++ .../plugins/cucm/libexec/agent_cucm_inv | 435 ++++++++++++++++++ .../cmk_addons/plugins/cucm/rulesets/cucm.py | 79 ++++ .../cucm/server_side_calls/special_agent.py | 38 ++ 15 files changed, 1011 insertions(+) create mode 100644 .gitignore create mode 100755 cucm/2.2/CUCM-0.3.3.mkp rename cucm/{ => 2.2}/local/lib/check_mk/base/plugins/agent_based/cucm_chk.py (100%) rename cucm/{ => 2.2}/local/lib/check_mk/base/plugins/agent_based/cucm_inv.py (100%) rename cucm/{ => 2.2}/local/share/check_mk/agents/special/agent_cucm_chk (100%) rename cucm/{ => 2.2}/local/share/check_mk/agents/special/agent_cucm_inv (100%) rename cucm/{ => 2.2}/local/share/check_mk/checks/agent_cucm (100%) rename cucm/{ => 2.2}/local/share/check_mk/web/plugins/views/cucm.py (100%) rename cucm/{ => 2.2}/local/share/check_mk/web/plugins/wato/cucm.py (100%) create mode 100644 cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/agent_based/cucm_chk.py create mode 100644 cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/agent_based/cucm_inv.py create mode 100755 cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/libexec/agent_cucm_chk create mode 100755 cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/libexec/agent_cucm_inv create mode 100644 cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/rulesets/cucm.py create mode 100644 cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/server_side_calls/special_agent.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d35cb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.pyc diff --git a/cucm/2.2/CUCM-0.3.3.mkp b/cucm/2.2/CUCM-0.3.3.mkp new file mode 100755 index 0000000000000000000000000000000000000000..3350dd96a7a828f454475479d3b5a8cc3cd54c57 GIT binary patch literal 11686 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 + + + + + + 2000 + {device} + 255 + + + Name + + Any + Any + + + + + """) + 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()) diff --git a/cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/libexec/agent_cucm_inv b/cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/libexec/agent_cucm_inv new file mode 100755 index 0000000..4835b6e --- /dev/null +++ b/cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/libexec/agent_cucm_inv @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +# Copyright (C) 2025 Spearhead Systems SRL +# +# 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, """ + + + + + + % + + + + + + + + """) + 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'{id}' for id in phone_ids]) + + try: + return get_url(url, user, password, insecure, headers, f""" + + + + + + + 2000 + Any + 255 + Registered + + Name + + {id_query} + + Any + Any + + + + + """) + 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('(.+)', data) + serial = re.search('(.+)', data) + model = re.search('(.+)', 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(r'\s*(.*?)\s*', 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()) + diff --git a/cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/rulesets/cucm.py b/cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/rulesets/cucm.py new file mode 100644 index 0000000..b8c4127 --- /dev/null +++ b/cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/rulesets/cucm.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# Copyright (C) 2025 Spearhead Systems SRL +# +# 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 use the same code for both. + +from cmk.rulesets.v1.form_specs import Dictionary, DictElement, String, Integer, Password, BooleanChoice, DefaultValue +from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic, Title, Help +from cmk.rulesets.v1.form_specs.validators import LengthInRange, NumberInRange + + +# GUI config page for both checks and inventory. +def _formspec_cucm(): + return Dictionary( + title = Title("CUCM"), + elements = { + "instance": DictElement( + required = True, + parameter_form = String( + title = Title("Hostname"), + help_text = Help("Host of CUCM host for query"), + custom_validate = (LengthInRange(min_value=1),), + ), + ), + "port": DictElement( + required = True, + parameter_form = Integer( + title = Title("Port"), + help_text = Help("Port of CUCM host for query"), + prefill = DefaultValue(443), + custom_validate = (NumberInRange(min_value=1, max_value=65535),), + ), + ), + "user": DictElement( + required = True, + parameter_form = String( + title = Title("Username"), + help_text = Help("Username used when querying CUCM"), + custom_validate = (LengthInRange(min_value=1),), + ), + ), + "password": DictElement( + required = True, + parameter_form = Password( + title = Title("Password"), + help_text = Help("Password used when querying CUCM"), + ), + ), + "insecure": DictElement( + required = True, + parameter_form = BooleanChoice( + title = Title("Insecure"), + help_text = Help("Ignore unverified HTTPS request warnings when contacting CUCM"), + prefill = DefaultValue(False), + ), + ), + }, + ) + + +rule_spec_agent_config_cucm_chk = SpecialAgent( + topic=Topic.NETWORKING, + name="cucm_chk", + title=Title("CUCM Checks"), + parameter_form=_formspec_cucm, +) + +rule_spec_agent_config_cucm_inv = SpecialAgent( + topic=Topic.PERIPHERALS, + name="cucm_inv", + title=Title("CUCM Inventory"), + parameter_form=_formspec_cucm, +) diff --git a/cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/server_side_calls/special_agent.py b/cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/server_side_calls/special_agent.py new file mode 100644 index 0000000..89f1efc --- /dev/null +++ b/cucm/2.4/local/lib/python3/cmk_addons/plugins/cucm/server_side_calls/special_agent.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# Copyright (C) 2025 Spearhead Systems SRL + + +from cmk.server_side_calls.v1 import noop_parser, SpecialAgentConfig, SpecialAgentCommand + + +def agent_cucm_arguments(params, host_config): + args = [] + + if "user" in params: + args += ["-u", params["user"]] + + if "password" in params: + args += ["-s", params["password"].unsafe()] + + if "port" in params: + args += ["-p", str(params["port"])] + + if params.get("insecure"): + args.append("-k") + + args.append(params["instance"]) + + yield SpecialAgentCommand(command_arguments=args) + + +special_agent_cucm_chk = SpecialAgentConfig( + name = "cucm_chk", + parameter_parser = noop_parser, + commands_function = agent_cucm_arguments, +) + +special_agent_cucm_inv = SpecialAgentConfig( + name = "cucm_inv", + parameter_parser = noop_parser, + commands_function = agent_cucm_arguments, +)