From 9e0e13a636041c36b5196e070a368ca5ddafac8a Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Tue, 20 May 2025 16:56:19 +0200 Subject: [PATCH] Improvements as requested by George: * Switch wedge agent from reporting CN UUIDs to CN hostnames, and use CheckMK's piggyback mechanism to send wedge status to the correct Host in CheckMK. * Improve how local ports are selected during connect() attempts, so there's (much) less likely to be conflicts on subsequent runs, due to length of ephemeral port expiry. * Increase connect() time from 100ms to 1000ms, to deal better with potentially slow/overloaded VMs. --- .../triton_wedge/agent_based/triton_wedge.py | 28 ++--- .../triton_wedge/libexec/agent_triton_wedge | 116 ++++++++++++++++-- wedge/triton_wedge-0.2.0.mkp | Bin 4091 -> 0 bytes wedge/triton_wedge-0.3.0.mkp | Bin 0 -> 5337 bytes 4 files changed, 115 insertions(+), 29 deletions(-) delete mode 100755 wedge/triton_wedge-0.2.0.mkp create mode 100755 wedge/triton_wedge-0.3.0.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 74c9c49..d838a35 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 @@ -7,15 +7,10 @@ from cmk.agent_based.v2 import Result, Service, State, CheckPlugin, AgentSection def parse_triton_wedge(string_table): - lookup = {} - + vms = [] for row in string_table: - nic = json.loads(row[0]) - cn_name = nic["cn"] - vms_in_cn = lookup.setdefault(cn_name, []) - vms_in_cn.append(nic) - - return lookup + vms.append(json.loads(row[0])) + return vms agent_section_triton_wedge = AgentSection( @@ -25,18 +20,12 @@ agent_section_triton_wedge = AgentSection( def discover_triton_wedge(section): - for cn_name, vms in sorted(section.items()): - yield Service(item=cn_name, parameters={"name": cn_name}) + if section: + yield Service(item="1") def check_triton_wedge(item, params, section): - cn_name = params["name"] - vms = section.get(cn_name) - - if vms is None: - yield Result(state=State.WARN, summary="Not appearing in NAPI") - return - + vms = section wedged_vms = [] for vm in vms: @@ -44,7 +33,8 @@ def check_triton_wedge(item, params, section): wedged_vms.append(vm) if len(wedged_vms) == 0: - yield Result(state=State.OK, summary="No wedge detected") + summary = f"No wedge detected ({len(vms)} VM external NIC(s) checked)" + yield Result(state=State.OK, summary=summary) elif len(wedged_vms) == 1: vm = wedged_vms[0] summary = "Potential wedge detected for VM %s (%s)" % (vm["vm"], vm["ip"]) @@ -56,7 +46,7 @@ def check_triton_wedge(item, params, section): check_plugin_triton_wedge = CheckPlugin( name="triton_wedge", - service_name="Triton Wedge CN %s", + service_name="Triton Wedge Detector (%s)", discovery_function=discover_triton_wedge, check_function=check_triton_wedge, check_default_parameters={}, 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 5da84b6..371b799 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 @@ -2,16 +2,69 @@ # The range of ephemeral local ports we use when attempting to probe remote # IPs. In the past, wedged ports appeared with a stride of 8; to be safe, we use -# a stride of 128. -PORT_RANGE_START = 57000 -PORT_RANGE_END = 57128 +# a stride of 16. So we select a subrange of 16 ports somewhere within the +# PORT_RANGE_START/END to scan; this reduces the chance of exhausted ports when +# a human uses this tool (we have a tendency to run commands faster than +# the ephemeral port timeout). +PORT_SUBRANGE_SIZE = 16 +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 +# 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 +# This is a hackish lookup to quickly convert CN UUIDs to host names. +# Ops wants host names, and wants them quick. Adding the lookup here is +# the fastest way to do it, although ideally it'd be put in a rule instead. +HOSTNAME_LOOKUP = { + "00000000-0000-0000-0000-ac1f6b41905a": "ac-1f-6b-27-81-40", + "44454c4c-3000-104a-804a-b3c04f465632": "e4-43-4b-b7-ad-a4", + "44454c4c-3000-104b-8039-b3c04f465632": "e4-43-4b-b7-ad-e0", + "44454c4c-3000-1056-8044-b4c04f465632": "e4-43-4b-b7-b0-38", + "44454c4c-3300-1051-8030-b4c04f525032": "e4-43-4b-bd-94-4c", + "44454c4c-3600-1038-804b-b2c04f445a32": "e4-43-4b-86-30-30", + "44454c4c-4200-1031-8033-c4c04f594d32": "24-6e-96-5e-a9-c8", + "44454c4c-4400-1053-8054-b4c04f474c32": "80-18-44-e5-20-38", + "44454c4c-4400-1053-8058-b4c04f474c32": "80-18-44-e5-1f-b4", + "44454c4c-4400-1054-8030-b4c04f484c32": "80-18-44-e5-35-80", + "44454c4c-4400-1054-8052-b4c04f474c32": "80-18-44-e5-24-bc", + "44454c4c-4400-1058-8042-c3c04f513033": "24-6e-96-5e-b3-9c", + "44454c4c-4400-1059-8042-c3c04f513033": "24-6e-96-63-f7-9c", + "44454c4c-4400-105a-8037-c3c04f513033": "24-6e-96-2e-fa-54", + "44454c4c-4600-1030-8057-c2c04f485032": "24-6e-96-0d-9c-98", + "44454c4c-4800-1038-8048-b2c04f435a32": "e4-43-4b-86-72-c8", + "44454c4c-4800-1038-8048-b4c04f435a32": "e4-43-4b-86-72-d0", + "44454c4c-4800-1038-8048-b5c04f435a32": "e4-43-4b-86-6c-8c", + "44454c4c-4800-1038-8048-b6c04f435a32": "e4-43-4b-86-6c-08", + "44454c4c-4800-1038-8048-b7c04f435a32": "e4-43-4b-86-73-18", + "44454c4c-4800-1038-8048-b8c04f435a32": "e4-43-4b-86-72-ec", + "44454c4c-4800-1038-8048-b9c04f435a32": "e4-43-4b-86-73-00", + "44454c4c-4800-1038-8048-c2c04f435a32": "e4-43-4b-86-73-04", + "44454c4c-4a00-1048-8033-b7c04f513033": "24-6e-96-2f-22-28", + "44454c4c-4b00-1056-8057-83965dec755b": "80-18-44-e5-d2-58-backup", + "44454c4c-4b00-1056-8057-b7c04f314c32": "headnode", + "44454c4c-4c00-104a-805a-b7c04f314c32": "80-18-44-e5-cf-84", + "44454c4c-4c00-104b-8058-b7c04f314c32": "80-18-44-e5-d2-50", + "44454c4c-4c00-104c-8051-b7c04f314c32": "80-18-44-e5-cf-4c", + "44454c4c-4c00-104c-8057-b7c04f314c32": "80-18-44-e5-ce-24", + "44454c4c-4c00-104d-8051-b7c04f314c32": "80-18-44-e5-ce-6c", + "44454c4c-4c00-104d-8058-b7c04f314c32": "80-18-44-e5-d2-6c", + "44454c4c-4c00-104e-8052-b7c04f314c32": "80-18-44-e5-d0-8c", + "44454c4c-4c00-104e-8056-b7c04f314c32": "80-18-44-e5-d0-1c", + "44454c4c-4c00-104e-8058-b7c04f314c32": "80-18-44-e5-cd-1c", + "44454c4c-5000-1053-8036-c3c04f445032": "24-6e-96-39-6c-5c", +} + + import urllib.request, sys, argparse, asyncio, json, socket, errno +import random def get_url(url, timeout=None): @@ -50,7 +103,7 @@ async def async_connect(src, dest): for attempt in range(CONNECT_RETRIES): try: future = loop.sock_connect(sd, dest) - await asyncio.wait_for(future, timeout=0.1) + await asyncio.wait_for(future, timeout=CONNECT_TIMEOUT) connected = True break except ConnectionRefusedError: @@ -74,6 +127,20 @@ async def async_connect(src, dest): return connected +# Return a pair of numbers to use as the start and end ephemeral ports which +# are used to connect to a remote server. +def calculate_local_port_range(): + # Pick a subrange of local ports to use, with a granularity of + # the size of that range to prevent subrange overlaps. + num_ports = PORT_RANGE_END - PORT_RANGE_START + num_ranges = int(num_ports / PORT_SUBRANGE_SIZE) + + range_start = random.randint(0, num_ranges) * PORT_SUBRANGE_SIZE + PORT_RANGE_START + range_end = range_start + PORT_SUBRANGE_SIZE + + return range_start, range_end + + # Check for a wedge on a NIC. We detect a wedge by doing the following: # # Us (local IP, local port) -----> Them (remote IP, remote port) @@ -87,24 +154,41 @@ async def check_for_wedge(nic, semaphore): remote_ip = nic["ip"] can_connect = False + + cn = nic["cn_uuid"] + # convert server UUID to hostname if we know the hostname + cn_hostname = HOSTNAME_LOOKUP.get(cn) + if cn_hostname: + cn = cn_hostname + result = { - "cn": nic["cn_uuid"], + "cn": cn, "vm": nic["belongs_to_uuid"], "ip": nic["ip"], "wedged": False } async with semaphore: + local_start_port, local_end_port = calculate_local_port_range() + # To speed things up, we only check ports 443 and 80, which are the # most common ports on the Internet. for remote_port in CHECK_REMOTE_PORTS: if can_connect: break - for local_port in range(PORT_RANGE_START, PORT_RANGE_END): + for local_port in range(local_start_port, local_end_port): src = (local_ip, local_port) dest = (remote_ip, remote_port) - connected = await async_connect(src, dest) + + try: + connected = await async_connect(src, dest) + except OSError as e: + if e.errno == errno.EADDRINUSE: + result["wedged"] = None + return result + else: + raise if can_connect and not connected: result["wedged"] = True @@ -127,11 +211,24 @@ async def scan(nics): # 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): - sys.stdout.write(f"<<<{agent_name}:sep(0)>>>\n") scan_results = list(scan_results) scan_results.sort(key=lambda d: d["cn"] + d["vm"] + d["ip"]) + + last_cn = None + for entry in scan_results: - sys.stdout.write("%s\n" % json.dumps(entry)) + curr_cn = entry["cn"] + + 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("%s\n" % json.dumps({ + "vm": entry["vm"], + "ip": entry["ip"], + "wedged": entry["wedged"], + })) # Parse the command-line arguments, specifically for hostname. Print out help @@ -163,4 +260,3 @@ def main(argv=None): if __name__ == "__main__": sys.exit(main()) - diff --git a/wedge/triton_wedge-0.2.0.mkp b/wedge/triton_wedge-0.2.0.mkp deleted file mode 100755 index 9ecd718ba79322723d940047c6d7696cd9e72638..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4091 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 diff --git a/wedge/triton_wedge-0.3.0.mkp b/wedge/triton_wedge-0.3.0.mkp new file mode 100755 index 0000000000000000000000000000000000000000..d6b10e26be1441a1efb4715e09be21f128ea5072 GIT binary patch literal 5337 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