root / plugins / network / olsrd @ 92483a04
Historique | Voir | Annoter | Télécharger (13,2 ko)
| 1 | 1c281c61 | Lars Kruse | #!/bin/sh |
|---|---|---|---|
| 2 | # weird shebang? See below: "interpreter selection" |
||
| 3 | |||
| 4 | """true" |
||
| 5 | 04b9fc04 | Lars Kruse | : <<=cut |
| 6 | |||
| 7 | =head1 NAME |
||
| 8 | |||
| 9 | olsrd - Monitor the state of an OLSR-based routing network |
||
| 10 | |||
| 11 | |||
| 12 | =head1 APPLICABLE SYSTEMS |
||
| 13 | |||
| 14 | Information is parsed from the output of "txtinfo" plugin for olsrd. |
||
| 15 | |||
| 16 | |||
| 17 | =head1 CONFIGURATION |
||
| 18 | |||
| 19 | Environment variables: |
||
| 20 | |||
| 21 | * OLSRD_HOST: name or IP of the host running the txtinfo plugin (default: localhost) |
||
| 22 | * OLSRD_TXTINFO_PORT: the port that the txtinfo plugin is listening to (default: 2006) |
||
| 23 | * OLSRD_BIN_PATH: name of the olsrd binary (only used for 'autoconf', default: /usr/sbin/olsrd) |
||
| 24 | * MICROPYTHON_HEAP: adjust this parameter for micropython if your olsr network contains |
||
| 25 | more than a few thousand nodes (default: 512k) |
||
| 26 | |||
| 27 | =head1 USAGE |
||
| 28 | |||
| 29 | Collect basic information about the neighbours of an OLSR node: |
||
| 30 | |||
| 31 | * link quality |
||
| 32 | * neighbour link quality |
||
| 33 | * number of nodes reachable behind each neighbour |
||
| 34 | * ping times of direct neighbours |
||
| 35 | |||
| 36 | This plugin works with the following python interpreters: |
||
| 37 | |||
| 38 | * Python 2 |
||
| 39 | * Python 3 |
||
| 40 | * micropython (e.g. OpenWrt) |
||
| 41 | |||
| 42 | |||
| 43 | =head1 VERSION |
||
| 44 | |||
| 45 | 0.4 |
||
| 46 | |||
| 47 | |||
| 48 | =head1 AUTHOR |
||
| 49 | |||
| 50 | Lars Kruse <devel@sumpfralle.de> |
||
| 51 | |||
| 52 | |||
| 53 | =head1 LICENSE |
||
| 54 | |||
| 55 | GPLv3 or above |
||
| 56 | |||
| 57 | |||
| 58 | =head1 MAGIC MARKERS |
||
| 59 | |||
| 60 | #%# family=auto |
||
| 61 | #%# capabilities=autoconf |
||
| 62 | |||
| 63 | =cut |
||
| 64 | |||
| 65 | |||
| 66 | 1c281c61 | Lars Kruse | # ****************** Interpreter Selection *************** |
| 67 | # This unbelievable dirty hack allows to find a suitable python interpreter. |
||
| 68 | # This is specifically useful for OpenWRT where typically only micropython is available. |
||
| 69 | # |
||
| 70 | # Additionally we need to run micropython with additional startup options. |
||
| 71 | 04b9fc04 | Lars Kruse | # This is necessary due to our demand for more than 128k heap (this default is sufficient for only |
| 72 | # 400 olsr nodes). |
||
| 73 | 1c281c61 | Lars Kruse | # |
| 74 | # This "execution hack" works as follows: |
||
| 75 | # * the script is executed by busybox ash or another shell |
||
| 76 | 04b9fc04 | Lars Kruse | # * the above line (three quotes before and one quote after 'true') evaluates differently for |
| 77 | # shell and python: |
||
| 78 | # * shell: run "true" (i.e. nothing happens) |
||
| 79 | # * python: ignore everything up to the next three consecutive quotes |
||
| 80 | 1c281c61 | Lars Kruse | # Thus we may place shell code here that will take care for selecting an interpreter. |
| 81 | |||
| 82 | # prefer micropython if it is available - otherwise fall back to any python (2 or 3) |
||
| 83 | 04b9fc04 | Lars Kruse | MICROPYTHON_BIN=$(which micropython || true) |
| 84 | if [ -n "$MICROPYTHON_BIN" ]; then |
||
| 85 | "$MICROPYTHON_BIN" -X "heapsize=${MICROPYTHON_HEAP:-512k}" "$0" "$@"
|
||
| 86 | 1c281c61 | Lars Kruse | else |
| 87 | python "$0" "$@" |
||
| 88 | fi |
||
| 89 | exit $? |
||
| 90 | 612ebf5a | Lars Kruse | |
| 91 | # For shell: ignore everything starting from here until the last line of this file. |
||
| 92 | # This is necessary for syntax checkers that try to complain about invalid shell syntax below. |
||
| 93 | true <<EOF |
||
| 94 | 1c281c61 | Lars Kruse | """ |
| 95 | |||
| 96 | |||
| 97 | import os |
||
| 98 | 04b9fc04 | Lars Kruse | import os.path |
| 99 | 1c281c61 | Lars Kruse | import socket |
| 100 | import sys |
||
| 101 | |||
| 102 | |||
| 103 | 04b9fc04 | Lars Kruse | plugin_version = "0.4" |
| 104 | |||
| 105 | 1c281c61 | Lars Kruse | LQ_GRAPH_CONFIG = """ |
| 106 | graph_title {title}
|
||
| 107 | graph_vlabel Link Quality (-) / Neighbour Link Quality (+) |
||
| 108 | graph_category network |
||
| 109 | 04b9fc04 | Lars Kruse | graph_info OLSR estimates the quality of a connection by the ratio of successfully received \ |
| 110 | (link quality) and transmitted (neighbour link quality) hello packets. |
||
| 111 | 1c281c61 | Lars Kruse | """ |
| 112 | |||
| 113 | LQ_VALUES_CONFIG = """ |
||
| 114 | nlq{suffix}.label none
|
||
| 115 | nlq{suffix}.type GAUGE
|
||
| 116 | nlq{suffix}.graph no
|
||
| 117 | nlq{suffix}.draw {draw_type}
|
||
| 118 | nlq{suffix}.min 0
|
||
| 119 | lq{suffix}.label {label}
|
||
| 120 | lq{suffix}.type GAUGE
|
||
| 121 | lq{suffix}.draw {draw_type}
|
||
| 122 | lq{suffix}.negative nlq{suffix}
|
||
| 123 | lq{suffix}.min 0
|
||
| 124 | """ |
||
| 125 | |||
| 126 | NEIGHBOUR_COUNT_CONFIG = """ |
||
| 127 | graph_title Reachable nodes via neighbours |
||
| 128 | graph_vlabel Number of Nodes |
||
| 129 | graph_category network |
||
| 130 | 04b9fc04 | Lars Kruse | graph_info Count the number of locally known routes passing through each direct neighbour. \ |
| 131 | This number is a good approximation for the number of mesh nodes reachable via this specific \ |
||
| 132 | neighbour. MIDs (alternative addresses of an OLSR node) and HNAs (host network announcements) are \ |
||
| 133 | ignored. |
||
| 134 | 1c281c61 | Lars Kruse | """ |
| 135 | |||
| 136 | NEIGHBOUR_COUNT_VALUE = """ |
||
| 137 | neighbour_{host_fieldname}.label {host}
|
||
| 138 | neighbour_{host_fieldname}.type GAUGE
|
||
| 139 | neighbour_{host_fieldname}.draw {draw_type}
|
||
| 140 | neighbour_{host_fieldname}.min 0
|
||
| 141 | """ |
||
| 142 | |||
| 143 | NEIGHBOUR_PING_CONFIG = """ |
||
| 144 | graph_title {title}
|
||
| 145 | graph_vlabel roundtrip time (ms) |
||
| 146 | graph_category network |
||
| 147 | graph_info This graph shows ping RTT statistics. |
||
| 148 | graph_args --base 1000 --lower-limit 0 |
||
| 149 | graph_scale no |
||
| 150 | """ |
||
| 151 | |||
| 152 | NEIGHBOUR_PING_VALUE = """neighbour_{host_fieldname}.label {host}"""
|
||
| 153 | |||
| 154 | # micropython (as of 2015) does not contain "os.linesep" |
||
| 155 | LINESEP = getattr(os, "linesep", "\n") |
||
| 156 | |||
| 157 | |||
| 158 | 04b9fc04 | Lars Kruse | def get_clean_fieldname(name): |
| 159 | chars = [] |
||
| 160 | for index, char in enumerate(name): |
||
| 161 | if ("a" <= char.lower() <= "z") or ((index == 0) or ("0" <= char <= "9")):
|
||
| 162 | chars.append(char) |
||
| 163 | else: |
||
| 164 | chars.append("_")
|
||
| 165 | return "".join(chars) |
||
| 166 | 1c281c61 | Lars Kruse | |
| 167 | |||
| 168 | def query_olsrd_txtservice(section=""): |
||
| 169 | host = os.getenv("OLSRD_HOST", "localhost")
|
||
| 170 | port = os.getenv("OLSRD_TXTINFO_PORT", "2006")
|
||
| 171 | conn = socket.create_connection((host, port), 1.0) |
||
| 172 | try: |
||
| 173 | # Python3 |
||
| 174 | request = bytes("/%s" % section, "ascii")
|
||
| 175 | except TypeError: |
||
| 176 | # Python2 |
||
| 177 | request = bytes("/%s" % section)
|
||
| 178 | conn.sendall(request) |
||
| 179 | fconn = conn.makefile() |
||
| 180 | a8d117a3 | Lars Kruse | in_header = True |
| 181 | in_body_count = 0 |
||
| 182 | 1c281c61 | Lars Kruse | for line in fconn.readlines(): |
| 183 | a8d117a3 | Lars Kruse | if in_header: |
| 184 | if not line.strip(): |
||
| 185 | # the empty line marks the end of the header |
||
| 186 | in_header = False |
||
| 187 | # ignore header lines (nothing to be done) |
||
| 188 | else: |
||
| 189 | # skip the first two body lines - they are table headers |
||
| 190 | if in_body_count >= 2: |
||
| 191 | line = line.strip() |
||
| 192 | if line: |
||
| 193 | yield line |
||
| 194 | in_body_count += 1 |
||
| 195 | 1c281c61 | Lars Kruse | fconn.close() |
| 196 | conn.close() |
||
| 197 | |||
| 198 | |||
| 199 | def get_address_device_mapping(): |
||
| 200 | mapping = {}
|
||
| 201 | for line in query_olsrd_txtservice("mid"):
|
||
| 202 | # example line content: |
||
| 203 | # 192.168.2.171 192.168.22.171;192.168.12.171 |
||
| 204 | a8d117a3 | Lars Kruse | # since olsr v0.9.5: |
| 205 | # 192.168.2.171 192.168.22.171 192.168.12.171 |
||
| 206 | device_id, mids = line.split(None, 1) |
||
| 207 | for mid in mids.replace(";", " ").split():
|
||
| 208 | 1c281c61 | Lars Kruse | mapping[mid] = device_id |
| 209 | return mapping |
||
| 210 | |||
| 211 | |||
| 212 | def count_routes_by_neighbour(address_mapping, ignore_list): |
||
| 213 | node_count = {}
|
||
| 214 | a8d117a3 | Lars Kruse | for line in query_olsrd_txtservice("rou"):
|
| 215 | 1c281c61 | Lars Kruse | # example line content: |
| 216 | # 192.168.1.79/32 192.168.12.38 4 4.008 wlan0 |
||
| 217 | tokens = line.split() |
||
| 218 | target = tokens[0] |
||
| 219 | via = tokens[1] |
||
| 220 | # we care only about single-host routes |
||
| 221 | if target.endswith("/32"):
|
||
| 222 | if target[:-3] in address_mapping: |
||
| 223 | # we ignore MIDs - we want only real nodes |
||
| 224 | continue |
||
| 225 | if target in ignore_list: |
||
| 226 | continue |
||
| 227 | # replace the neighbour's IP with its main IP (if it is an MID) |
||
| 228 | via = address_mapping.get(via, via) |
||
| 229 | # increase the counter |
||
| 230 | node_count[via] = node_count.get(via, 0) + 1 |
||
| 231 | return node_count |
||
| 232 | |||
| 233 | |||
| 234 | def get_olsr_links(): |
||
| 235 | mid_mapping = get_address_device_mapping() |
||
| 236 | hna_list = [line.split()[0] for line in query_olsrd_txtservice("hna")]
|
||
| 237 | route_count = count_routes_by_neighbour(mid_mapping, hna_list) |
||
| 238 | result = [] |
||
| 239 | a8d117a3 | Lars Kruse | for line in query_olsrd_txtservice("lin"):
|
| 240 | 1c281c61 | Lars Kruse | tokens = line.split() |
| 241 | a8d117a3 | Lars Kruse | # the "cost" may be infinite |
| 242 | if tokens[-1] == "INFINITE": |
||
| 243 | # "inf" is the python keyword for "maximum float number" |
||
| 244 | tokens[-1] = "inf" |
||
| 245 | 1c281c61 | Lars Kruse | link = {}
|
| 246 | link["local"] = tokens.pop(0) |
||
| 247 | remote = tokens.pop(0) |
||
| 248 | # replace the neighbour's IP with its main IP (if it is an MID) |
||
| 249 | link["remote"] = mid_mapping.get(remote, remote) |
||
| 250 | for key in ("hysterese", "lq", "nlq", "cost"):
|
||
| 251 | link[key] = float(tokens.pop(0)) |
||
| 252 | # add the route count |
||
| 253 | link["route_count"] = route_count.get(link["remote"], 0) |
||
| 254 | result.append(link) |
||
| 255 | result.sort(key=lambda link: link["remote"]) |
||
| 256 | return result |
||
| 257 | |||
| 258 | |||
| 259 | def _read_file(filename): |
||
| 260 | try: |
||
| 261 | return open(filename, "r").read().split(LINESEP) |
||
| 262 | except OSError: |
||
| 263 | return [] |
||
| 264 | |||
| 265 | |||
| 266 | def get_ping_times(hosts): |
||
| 267 | tempfile = "/tmp/munin-olsrd-{pid}.tmp".format(pid=os.getpid())
|
||
| 268 | 04b9fc04 | Lars Kruse | command = ('for host in {hosts}; do echo -n "$host "; '
|
| 269 | 'ping -c 1 -w 1 "$host" | grep /avg/ || echo; done >{tempfile}'
|
||
| 270 | .format(hosts=" ".join(hosts), tempfile=tempfile)) |
||
| 271 | # micropython supports only "os.system" (as of 2015) - thus we need to stick with it for |
||
| 272 | # OpenWrt. |
||
| 273 | 1c281c61 | Lars Kruse | returncode = os.system(command) |
| 274 | if returncode != 0: |
||
| 275 | return {}
|
||
| 276 | lines = _read_file(tempfile) |
||
| 277 | os.unlink(tempfile) |
||
| 278 | # example output for one host: |
||
| 279 | # 192.168.2.41 round-trip min/avg/max = 4.226/4.226/4.226 ms |
||
| 280 | result = {}
|
||
| 281 | for line in lines: |
||
| 282 | tokens = line.split(None) |
||
| 283 | if len(tokens) > 1: |
||
| 284 | host = tokens[0] |
||
| 285 | avg_ping = tokens[-2].split("/")[1]
|
||
| 286 | result[host] = float(avg_ping) |
||
| 287 | return result |
||
| 288 | |||
| 289 | |||
| 290 | 04b9fc04 | Lars Kruse | def do_config(): |
| 291 | links = list(get_olsr_links()) |
||
| 292 | |||
| 293 | # link quality with regard to neighbours |
||
| 294 | print("multigraph olsr_link_quality")
|
||
| 295 | print(LQ_GRAPH_CONFIG.format(title="OLSR Link Quality")) |
||
| 296 | for link in links: |
||
| 297 | print(LQ_VALUES_CONFIG.format( |
||
| 298 | label=link["remote"], |
||
| 299 | suffix="_{host}".format(host=get_clean_fieldname(link["remote"])),
|
||
| 300 | draw_type="AREASTACK")) |
||
| 301 | for link in links: |
||
| 302 | print("multigraph olsr_link_quality.host_{remote}"
|
||
| 303 | .format(remote=get_clean_fieldname(link["remote"]))) |
||
| 304 | title = "Link Quality towards {host}".format(host=link["remote"])
|
||
| 305 | print(LQ_GRAPH_CONFIG.format(title=title)) |
||
| 306 | print(LQ_VALUES_CONFIG.format(label="Link Quality", suffix="", draw_type="AREA")) |
||
| 307 | |||
| 308 | # link count ("number of nodes behind each neighbour")
|
||
| 309 | print("multigraph olsr_neighbour_link_count")
|
||
| 310 | print(NEIGHBOUR_COUNT_CONFIG) |
||
| 311 | for link in links: |
||
| 312 | print(NEIGHBOUR_COUNT_VALUE |
||
| 313 | .format(host=link["remote"], host_fieldname=get_clean_fieldname(link["remote"]), |
||
| 314 | draw_type="AREASTACK")) |
||
| 315 | |||
| 316 | # neighbour ping |
||
| 317 | print("multigraph olsr_neighbour_ping")
|
||
| 318 | print(NEIGHBOUR_PING_CONFIG.format(title="Ping time of neighbours")) |
||
| 319 | for link in links: |
||
| 320 | print(NEIGHBOUR_PING_VALUE |
||
| 321 | .format(host=link["remote"], host_fieldname=get_clean_fieldname(link["remote"]))) |
||
| 322 | # neighbour pings - single subgraphs |
||
| 323 | for link in links: |
||
| 324 | remote = get_clean_fieldname(link["remote"]) |
||
| 325 | print("multigraph olsr_neighbour_ping.host_{remote}".format(remote=remote))
|
||
| 326 | title = "Ping time of {remote}".format(remote=remote)
|
||
| 327 | print(NEIGHBOUR_PING_CONFIG.format(title=title)) |
||
| 328 | print(NEIGHBOUR_PING_VALUE.format(host=link["remote"], host_fieldname=remote)) |
||
| 329 | 1c281c61 | Lars Kruse | |
| 330 | |||
| 331 | 04b9fc04 | Lars Kruse | def do_fetch(): |
| 332 | 1c281c61 | Lars Kruse | # output values |
| 333 | links = list(get_olsr_links()) |
||
| 334 | |||
| 335 | # overview graph for the link quality (ETX) of all neighbours |
||
| 336 | print("multigraph olsr_link_quality")
|
||
| 337 | for link in links: |
||
| 338 | print("lq_{remote}.value {lq:f}".format(lq=link["lq"],
|
||
| 339 | remote=get_clean_fieldname(link["remote"]))) |
||
| 340 | print("nlq_{remote}.value {nlq:f}".format(nlq=link["nlq"],
|
||
| 341 | remote=get_clean_fieldname(link["remote"]))) |
||
| 342 | # detailed ETX graph for each single neighbour link |
||
| 343 | for link in links: |
||
| 344 | print("multigraph olsr_link_quality.host_{remote}"
|
||
| 345 | .format(remote=get_clean_fieldname(link["remote"]))) |
||
| 346 | print("lq.value {lq:f}".format(lq=link["lq"]))
|
||
| 347 | print("nlq.value {nlq:f}".format(nlq=link["nlq"]))
|
||
| 348 | |||
| 349 | # count the links/nodes behind each neighbour node |
||
| 350 | print("multigraph olsr_neighbour_link_count")
|
||
| 351 | for link in links: |
||
| 352 | print("neighbour_{host_fieldname}.value {value}"
|
||
| 353 | .format(value=link["route_count"], |
||
| 354 | host_fieldname=get_clean_fieldname(link["remote"]))) |
||
| 355 | |||
| 356 | # overview of ping roundtrip times |
||
| 357 | print("multigraph olsr_neighbour_ping")
|
||
| 358 | ping_times = get_ping_times([link["remote"] for link in links]) |
||
| 359 | for link in links: |
||
| 360 | ping_time = ping_times.get(link["remote"], None) |
||
| 361 | a8d117a3 | Lars Kruse | value = "{:.4f}".format(ping_time) if ping_time is not None else "U"
|
| 362 | print("neighbour_{remote}.value {value}"
|
||
| 363 | .format(value=value, remote=get_clean_fieldname(link["remote"]))) |
||
| 364 | 1c281c61 | Lars Kruse | # single detailed graphs for the ping time of each link |
| 365 | for link in links: |
||
| 366 | ping_time = ping_times.get(link["remote"], None) |
||
| 367 | a8d117a3 | Lars Kruse | value = "{:.4f}".format(ping_time) if ping_time is not None else "U"
|
| 368 | remote = get_clean_fieldname(link["remote"]) |
||
| 369 | print("multigraph olsr_neighbour_ping.host_{remote}".format(remote=remote))
|
||
| 370 | print("neighbour_{remote}.value {value}".format(remote=remote, value=value))
|
||
| 371 | 612ebf5a | Lars Kruse | |
| 372 | 04b9fc04 | Lars Kruse | |
| 373 | if __name__ == "__main__": |
||
| 374 | # parse arguments |
||
| 375 | if len(sys.argv) > 1: |
||
| 376 | if sys.argv[1] == "config": |
||
| 377 | do_config() |
||
| 378 | if os.getenv("MUNIN_CAP_DIRTYCONFIG") == "1":
|
||
| 379 | do_fetch() |
||
| 380 | sys.exit(0) |
||
| 381 | elif sys.argv[1] == "autoconf": |
||
| 382 | if os.path.exists(os.getenv('OLSRD_BIN_PATH', '/usr/sbin/olsrd')):
|
||
| 383 | print('yes')
|
||
| 384 | else: |
||
| 385 | print('no')
|
||
| 386 | sys.exit(0) |
||
| 387 | elif sys.argv[1] == "version": |
||
| 388 | print('olsrd Munin plugin, version %s' % plugin_version)
|
||
| 389 | sys.exit(0) |
||
| 390 | elif sys.argv[1] == "": |
||
| 391 | # ignore |
||
| 392 | pass |
||
| 393 | else: |
||
| 394 | # unknown argument |
||
| 395 | sys.stderr.write("Unknown argument{eol}".format(eol=LINESEP))
|
||
| 396 | sys.exit(1) |
||
| 397 | |||
| 398 | do_fetch() |
||
| 399 | |||
| 400 | 612ebf5a | Lars Kruse | # final marker for shell / python hybrid script (see "Interpreter Selection") |
| 401 | EOF = True |
||
| 402 | EOF |
