From ffafe3c4f6c37bf502422e2a5e08898047a6d3c0 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Tue, 20 May 2025 19:30:12 +0200 Subject: [PATCH] Reduced connect() timeout to speed up scan, and percolate NAPI connection errors up to CheckMK's GUI. --- .../triton_wedge/agent_based/triton_wedge.py | 6 ++- .../triton_wedge/libexec/agent_triton_wedge | 39 +++++++++++------- wedge/triton_wedge-0.3.0.mkp | Bin 5337 -> 0 bytes wedge/triton_wedge-0.3.1.mkp | Bin 0 -> 5475 bytes 4 files changed, 30 insertions(+), 15 deletions(-) delete mode 100755 wedge/triton_wedge-0.3.0.mkp create mode 100755 wedge/triton_wedge-0.3.1.mkp diff --git a/wedge/local/lib/python3/cmk_addons/plugins/triton_wedge/agent_based/triton_wedge.py b/wedge/local/lib/python3/cmk_addons/plugins/triton_wedge/agent_based/triton_wedge.py index d838a35..0a2c593 100644 --- a/wedge/local/lib/python3/cmk_addons/plugins/triton_wedge/agent_based/triton_wedge.py +++ b/wedge/local/lib/python3/cmk_addons/plugins/triton_wedge/agent_based/triton_wedge.py @@ -28,8 +28,12 @@ def check_triton_wedge(item, params, section): vms = section wedged_vms = [] + if len(vms) == 1 and vms[0].get("error"): + yield Result(state=State.UNKNOWN, summary=vms[0]["error"]) + return + for vm in vms: - if vm["wedged"]: + if vm["wedged"] == "probably": wedged_vms.append(vm) if len(wedged_vms) == 0: diff --git a/wedge/local/lib/python3/cmk_addons/plugins/triton_wedge/libexec/agent_triton_wedge b/wedge/local/lib/python3/cmk_addons/plugins/triton_wedge/libexec/agent_triton_wedge index 371b799..0c1a7b9 100755 --- a/wedge/local/lib/python3/cmk_addons/plugins/triton_wedge/libexec/agent_triton_wedge +++ b/wedge/local/lib/python3/cmk_addons/plugins/triton_wedge/libexec/agent_triton_wedge @@ -11,13 +11,14 @@ PORT_RANGE_START = 50000 PORT_RANGE_END = 65504 # How often and for how long to attempt to connect to a VM CONNECT_RETRIES = 3 -CONNECT_TIMEOUT = 1 # seconds +CONNECT_TIMEOUT = 0.5 # seconds # Remote ports, in order, to attempt to connect to. More ports means higher # chance of being able to test for a wedge, but also takes more time. CHECK_REMOTE_PORTS = [443, 80] # How many VMs we'll be portmapping concurrently. CONCURRENT_SCANS = 200 NAPI_TIMEOUT = 10 # seconds +AGENT_NAME = "triton_wedge" # This is a hackish lookup to quickly convert CN UUIDs to host names. @@ -81,8 +82,8 @@ def query_napi(addr): json_data = get_url(url, NAPI_TIMEOUT) nics = json.loads(json_data) return nics - except urllib.error.HTTPError as e: - sys.stderr.write("NAPI error: %s\n" % e) + except (urllib.error.HTTPError, urllib.error.URLError): + return None # asyncio provides some nice connection methods, but none of them allow us to @@ -148,7 +149,6 @@ def calculate_local_port_range(): # 1. Find an open remote port (to speed things up we check ports 443 and 80) # 2. Repeatedly connect() to the remote port while incrementing our local port # 3. If we find a local port that fails to connect, this may be a wedge -# async def check_for_wedge(nic, semaphore): local_ip = "0.0.0.0" remote_ip = nic["ip"] @@ -165,7 +165,7 @@ async def check_for_wedge(nic, semaphore): "cn": cn, "vm": nic["belongs_to_uuid"], "ip": nic["ip"], - "wedged": False + "wedged": "no" } async with semaphore: @@ -185,13 +185,13 @@ async def check_for_wedge(nic, semaphore): connected = await async_connect(src, dest) except OSError as e: if e.errno == errno.EADDRINUSE: - result["wedged"] = None + result["wedged"] = "temporary local port collision" return result else: raise if can_connect and not connected: - result["wedged"] = True + result["wedged"] = "probably" return result elif connected: can_connect = True @@ -208,9 +208,20 @@ async def scan(nics): return map(lambda f: f.result(), done) +# This is only used when we cannot contact NAPI. Ops wants to know when this +# happens, but if we cannot contact NAPI then we also don't know about any CNs. +# We use information in HOSTNAME_LOOKUP to still be able to report about +# a lack of contact to NAPI, even when we cannot contact NAPI. +def print_out_napi_err(): + for host in HOSTNAME_LOOKUP.values(): + sys.stdout.write(f"<<<<{host}>>>>\n") + sys.stdout.write(f"<<<{AGENT_NAME}:sep(0)>>>\n") + sys.stdout.write('{"error": "Cannot contact NAPI"}\n') + + # Print out all our results in a format that CheckMK understands. Most of our # output are in JSON rows. -def print_out(scan_results, agent_name): +def print_out(scan_results): scan_results = list(scan_results) scan_results.sort(key=lambda d: d["cn"] + d["vm"] + d["ip"]) @@ -222,7 +233,7 @@ def print_out(scan_results, agent_name): if curr_cn != last_cn: last_cn = curr_cn sys.stdout.write(f"<<<<{curr_cn}>>>>\n") - sys.stdout.write(f"<<<{agent_name}:sep(0)>>>\n") + sys.stdout.write(f"<<<{AGENT_NAME}:sep(0)>>>\n") sys.stdout.write("%s\n" % json.dumps({ "vm": entry["vm"], @@ -249,13 +260,13 @@ def main(argv=None): args = parse_arguments(argv) nics = query_napi(args.hostname) - # Sort the IPs so that (tend) to scan them in relative order. This is to - # increase the time between scans to the same IP due to consecutive agent - # executions, otherwise there's a higher chance we bump into TIME_WAIT. - #nics.sort(key=lambda d: d["ip"]) + if nics == None: + print_out_napi_err() + # We must return 0, or CheckMK won't check the output we're returning + sys.exit(0) scan_results = asyncio.run(scan(nics)) - print_out(scan_results, "triton_wedge") + print_out(scan_results) if __name__ == "__main__": diff --git a/wedge/triton_wedge-0.3.0.mkp b/wedge/triton_wedge-0.3.0.mkp deleted file mode 100755 index d6b10e26be1441a1efb4715e09be21f128ea5072..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5337 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