#!/bin/sh # weird shebang? See below: "interpreter selection" """true" : <<=cut =head1 NAME olsrd - Monitor the state of an OLSR-based routing network =head1 APPLICABLE SYSTEMS Information is parsed from the output of "txtinfo" plugin for olsrd. =head1 CONFIGURATION Environment variables: * OLSRD_HOST: name or IP of the host running the txtinfo plugin (default: localhost) * OLSRD_TXTINFO_PORT: the port that the txtinfo plugin is listening to (default: 2006) * OLSRD_BIN_PATH: name of the olsrd binary (only used for 'autoconf', default: /usr/sbin/olsrd) * MICROPYTHON_HEAP: adjust this parameter for micropython if your olsr network contains more than a few thousand nodes (default: 512k) =head1 USAGE Collect basic information about the neighbours of an OLSR node: * link quality * neighbour link quality * number of nodes reachable behind each neighbour * ping times of direct neighbours This plugin works with the following python interpreters: * Python 3 * micropython (e.g. OpenWrt) =head1 VERSION 0.5 =head1 AUTHOR Lars Kruse =head1 LICENSE GPLv3 or above =head1 MAGIC MARKERS #%# family=auto #%# capabilities=autoconf =cut # ****************** Interpreter Selection *************** # This unbelievable dirty hack allows to find a suitable python interpreter. # This is specifically useful for OpenWRT where typically only micropython is available. # # Additionally we need to run micropython with additional startup options. # This is necessary due to our demand for more than 128k heap (this default is sufficient for only # 400 olsr nodes). # # This "execution hack" works as follows: # * the script is executed by busybox ash or another shell # * the above line (three quotes before and one quote after 'true') evaluates differently for # shell and python: # * shell: run "true" (i.e. nothing happens) # * python: ignore everything up to the next three consecutive quotes # Thus we may place shell code here that will take care for selecting an interpreter. # prefer micropython if it is available - otherwise fall back to any python (2 or 3) MICROPYTHON_BIN=$(which micropython || true) if [ -n "$MICROPYTHON_BIN" ]; then "$MICROPYTHON_BIN" -X "heapsize=${MICROPYTHON_HEAP:-512k}" "$0" "$@" else python3 "$0" "$@" fi exit $? # For shell: ignore everything starting from here until the last line of this file. # This is necessary for syntax checkers that try to complain about invalid shell syntax below. true <= 2: line = line.strip() if line: yield line in_body_count += 1 fconn.close() conn.close() def get_address_device_mapping(): mapping = {} for line in query_olsrd_txtservice("mid"): # example line content: # 192.168.2.171 192.168.22.171;192.168.12.171 # since olsr v0.9.5: # 192.168.2.171 192.168.22.171 192.168.12.171 device_id, mids = line.split(None, 1) for mid in mids.replace(";", " ").split(): mapping[mid] = device_id return mapping def count_routes_by_neighbour(address_mapping, ignore_list): node_count = {} for line in query_olsrd_txtservice("rou"): # example line content: # 192.168.1.79/32 192.168.12.38 4 4.008 wlan0 tokens = line.split() target = tokens[0] via = tokens[1] # we care only about single-host routes if target.endswith("/32"): if target[:-3] in address_mapping: # we ignore MIDs - we want only real nodes continue if target in ignore_list: continue # replace the neighbour's IP with its main IP (if it is an MID) via = address_mapping.get(via, via) # increase the counter node_count[via] = node_count.get(via, 0) + 1 return node_count def get_olsr_links(): mid_mapping = get_address_device_mapping() hna_list = [line.split()[0] for line in query_olsrd_txtservice("hna")] route_count = count_routes_by_neighbour(mid_mapping, hna_list) result = [] for line in query_olsrd_txtservice("lin"): tokens = line.split() # the "cost" may be infinite if tokens[-1] == "INFINITE": # "inf" is the python keyword for "maximum float number" tokens[-1] = "inf" link = {} link["local"] = tokens.pop(0) remote = tokens.pop(0) # replace the neighbour's IP with its main IP (if it is an MID) link["remote"] = mid_mapping.get(remote, remote) for key in ("hysterese", "lq", "nlq", "cost"): link[key] = float(tokens.pop(0)) # add the route count link["route_count"] = route_count.get(link["remote"], 0) result.append(link) result.sort(key=lambda link: link["remote"]) return result def _read_file(filename): try: return open(filename, "r").read().split(linesep) except OSError: return [] def get_ping_times(hosts): tempfile = "/tmp/munin-olsrd-{pid}.tmp".format(pid=getpid()) command = ('for host in {hosts}; do echo -n "$host "; ' 'ping -c 1 -w 1 "$host" | grep /avg/ || echo; done >{tempfile}' .format(hosts=" ".join(hosts), tempfile=tempfile)) # micropython supports only "os.system" (as of 2015) - thus we need to stick with it for # OpenWrt. returncode = system(command) if returncode != 0: return {} lines = _read_file(tempfile) unlink(tempfile) # example output for one host: # 192.168.2.41 round-trip min/avg/max = 4.226/4.226/4.226 ms result = {} for line in lines: tokens = line.split(None) if len(tokens) > 1: host = tokens[0] avg_ping = tokens[-2].split("/")[1] result[host] = float(avg_ping) return result def do_config(): links = list(get_olsr_links()) # link quality with regard to neighbours print("multigraph olsr_link_quality") print(LQ_GRAPH_CONFIG.format(title="OLSR Link Quality")) for index, link in enumerate(links): print(LQ_VALUES_CONFIG.format( label=link["remote"], suffix="_{host}".format(host=get_clean_fieldname(link["remote"])), draw_type=("AREA" if index == 0 else "AREASTACK"))) for link in links: print("multigraph olsr_link_quality.host_{remote}" .format(remote=get_clean_fieldname(link["remote"]))) title = "Link Quality towards {host}".format(host=link["remote"]) print(LQ_GRAPH_CONFIG.format(title=title)) print(LQ_VALUES_CONFIG.format(label="Link Quality", suffix="", draw_type="AREA")) # link count ("number of nodes behind each neighbour") print("multigraph olsr_neighbour_link_count") print(NEIGHBOUR_COUNT_CONFIG) for link in links: print(NEIGHBOUR_COUNT_VALUE .format(host=link["remote"], host_fieldname=get_clean_fieldname(link["remote"]), draw_type="AREASTACK")) # neighbour ping print("multigraph olsr_neighbour_ping") print(NEIGHBOUR_PING_CONFIG.format(title="Ping time of OLSR neighbours")) for link in links: print(NEIGHBOUR_PING_VALUE .format(host=link["remote"], host_fieldname=get_clean_fieldname(link["remote"]))) # neighbour pings - single subgraphs for link in links: remote = get_clean_fieldname(link["remote"]) print("multigraph olsr_neighbour_ping.host_{remote}".format(remote=remote)) title = "Ping time of {remote}".format(remote=remote) print(NEIGHBOUR_PING_CONFIG.format(title=title)) print(NEIGHBOUR_PING_VALUE.format(host=link["remote"], host_fieldname=remote)) def do_fetch(): # output values links = list(get_olsr_links()) # overview graph for the link quality (ETX) of all neighbours print("multigraph olsr_link_quality") for link in links: print("lq_{remote}.value {lq:f}".format(lq=link["lq"], remote=get_clean_fieldname(link["remote"]))) print("nlq_{remote}.value {nlq:f}".format(nlq=link["nlq"], remote=get_clean_fieldname(link["remote"]))) # detailed ETX graph for each single neighbour link for link in links: print("multigraph olsr_link_quality.host_{remote}" .format(remote=get_clean_fieldname(link["remote"]))) print("lq.value {lq:f}".format(lq=link["lq"])) print("nlq.value {nlq:f}".format(nlq=link["nlq"])) # count the links/nodes behind each neighbour node print("multigraph olsr_neighbour_link_count") for link in links: print("neighbour_{host_fieldname}.value {value}" .format(value=link["route_count"], host_fieldname=get_clean_fieldname(link["remote"]))) # overview of ping roundtrip times print("multigraph olsr_neighbour_ping") ping_times = get_ping_times([link["remote"] for link in links]) for link in links: ping_time = ping_times.get(link["remote"], None) value = "{:.4f}".format(ping_time) if ping_time is not None else "U" print("neighbour_{remote}.value {value}" .format(value=value, remote=get_clean_fieldname(link["remote"]))) # single detailed graphs for the ping time of each link for link in links: ping_time = ping_times.get(link["remote"], None) value = "{:.4f}".format(ping_time) if ping_time is not None else "U" remote = get_clean_fieldname(link["remote"]) print("multigraph olsr_neighbour_ping.host_{remote}".format(remote=remote)) print("neighbour_{remote}.value {value}".format(remote=remote, value=value)) if __name__ == "__main__": # parse arguments if len(sys.argv) > 1: if sys.argv[1] == "config": do_config() if getenv("MUNIN_CAP_DIRTYCONFIG") == "1": do_fetch() sys.exit(0) elif sys.argv[1] == "autoconf": if access(getenv('OLSRD_BIN_PATH') or '/usr/sbin/olsrd', 0): print('yes') else: print('no') sys.exit(0) elif sys.argv[1] == "version": print('olsrd Munin plugin, version %s' % plugin_version) sys.exit(0) elif sys.argv[1] == "": # ignore pass else: # unknown argument sys.stderr.write("Unknown argument{eol}".format(eol=linesep)) sys.exit(1) do_fetch() # final marker for shell / python hybrid script (see "Interpreter Selection") EOF = True EOF