#!/bin/sh # weird shebang? See below: "interpreter selection" # # Collect information related to ath9k wireless events and states. # * rate control statistics ("rc_stats") # * events (dropped, transmitted, beacon loss, ...) # * traffic (packets, bytes) # * DFS events (processed patterns, approved signals) # # All data is collected for each separate station (in case of multiple # connected peers). Combined graphs are provided as a summary. # # # This plugin works with the following python interpreters: # * Python 3 # * micropython # # # The following graphs are generated for each physical ath9k interface: # phy0_wifi0_traffic # phy0_wifi0_traffic.station0 # ... # pyh0_wifi0_events # phy0_wifi0_events.station0 # ... # pyh0_wifi0_rc_stats # phy0_wifi0_rc_stats.station0 # ... # # # Copyright (C) 2015-2018 Lars Kruse # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Magic markers # #%# capabilities=autoconf suggest # #%# family=auto """true" # ****************** 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. # # 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 python 3 MICROPYTHON_BIN=$(which micropython || true) if [ -n "$MICROPYTHON_BIN" ]; then "$MICROPYTHON_BIN" "$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 < IP """ arp_cache = {} # example content: # IP address HW type Flags HW address Mask Device # 192.168.2.70 0x1 0x0 00:00:00:00:00:00 * eth0.10 # 192.168.12.76 0x1 0x2 24:a4:3c:fd:76:98 * eth1.10 for line in open("/proc/net/arp", "r").read().split("\n"): # skip empty lines if line: tokens = line.split() ip, mac = tokens[0], tokens[3] # the header line can be ignored - all other should have well-formed MACs if ":" in mac: # ignore remote peers outside of the broadcast domain if mac != "00:00:00:00:00:00": arp_cache[mac] = ip return arp_cache def _parse_stations(self): stations_base = os.path.join(self._path, "stations") arp_cache = self._parse_arp_cache() for item in os.listdir(stations_base): peer_mac = item # use the IP or fall back to the MAC without separators (":") if peer_mac in arp_cache: label = arp_cache[peer_mac] key = peer_mac.replace(":", "") else: label = peer_mac key = "host_" + peer_mac.replace(":", "").replace(".", "") yield Station(label, key, os.path.join(stations_base, item)) def get_config(self, scope): yield from Station.get_summary_config(scope, self.stations, self._graph_base) for station in self.stations: yield from station.get_config(scope, self._graph_base) yield "" def get_values(self, scope): yield from Station.get_summary_values(scope, self.stations, self._graph_base) for station in self.stations: yield from station.get_values(scope, self._graph_base) yield "" class WifiPhy: def __init__(self, name, path, graph_base): self._path = path self._graph_base = graph_base self.name = name self.dfs_events = self._parse_dfs_events() self.interfaces = tuple(self._parse_interfaces()) def _parse_dfs_events(self): result = {} fname = os.path.join(self._path, "ath9k", "dfs_stats") if not os.path.exists(fname): # older ath9k modules (e.g. Linux 3.3) did not provide this data return {} for line in open(fname, "r").read().split("\n"): tokens = line.split(":") if len(tokens) == 2: label, value = tokens[0].strip(), tokens[1].strip() if label in DFS_EVENT_COUNTERS: fieldname = DFS_EVENT_COUNTERS[label] result[fieldname] = value return result def _parse_interfaces(self): for item in os.listdir(self._path): if item.startswith("netdev:"): wifi = item.split(":", 1)[1] label = "{phy}/{interface}".format(phy=self.name, interface=wifi) wifi_path = os.path.join(self._path, item) graph_base = "{base}_{phy}_{interface}".format(base=self._graph_base, phy=self.name, interface=wifi) yield WifiInterface(label, wifi_path, graph_base) def get_config(self, scope): if scope == "dfs_events": yield "multigraph {graph_base}_dfs_events".format(graph_base=self._graph_base) yield "graph_title DFS Events" yield "graph_vlabel events per second" yield "graph_args --base 1000 --logarithmic" yield "graph_category wireless" for label, fieldname in DFS_EVENT_COUNTERS.items(): yield "{fieldname}.label {label}".format(fieldname=fieldname, label=label) yield "{fieldname}.type COUNTER".format(fieldname=fieldname) yield "" else: for interface in self.interfaces: yield from interface.get_config(scope) def get_values(self, scope): if scope == "dfs_events": yield "multigraph {graph_base}_dfs_events".format(graph_base=self._graph_base) for fieldname, value in self.dfs_events.items(): yield "{fieldname}.value {value}".format(fieldname=fieldname, value=value) yield "" else: for interface in self.interfaces: yield from interface.get_values(scope) class Ath9kDriver: def __init__(self, path, graph_base): self._path = path self._graph_base = graph_base self.phys = list(self._parse_phys()) def _parse_phys(self): if not os.path.exists(self._path): return for phy in os.listdir(self._path): phy_path = os.path.join(self._path, phy) graph_base = "{base}_{phy}".format(base=self._graph_base, phy=phy) yield WifiPhy(phy, phy_path, graph_base) def get_config(self, scope): for phy in self.phys: yield from phy.get_config(scope) def get_values(self, scope): for phy in self.phys: yield from phy.get_values(scope) def has_dfs_support(self): for phy in self.phys: if phy.dfs_events: return True return False def has_devices(self): return len(self.phys) > 0 def _get_up_down_pair(unit, key_up, key_down, factor=None, divider=None, use_negative=True): """ return all required statements for a munin-specific up/down value pair "factor" or "divider" can be given for unit conversions """ for key in (key_up, key_down): if use_negative: yield "{key}.label {unit}".format(key=key, unit=unit) else: yield "{key}.label {key} {unit}".format(key=key, unit=unit) yield "{key}.type COUNTER".format(key=key) if factor: yield "{key}.cdef {key},{factor},*".format(key=key, factor=factor) if divider: yield "{key}.cdef {key},{divider},/".format(key=key, divider=divider) if use_negative: yield "{key_down}.graph no".format(key_down=key_down) yield "{key_up}.negative {key_down}".format(key_up=key_up, key_down=key_down) def get_scope(): called_name = os.path.basename(sys.argv[0]) name_prefix = "ath9k_" if called_name.startswith(name_prefix): scope = called_name[len(name_prefix):] if scope not in PLUGIN_SCOPES: print_error("Invalid scope requested: {0} (expected: {1})" .format(scope, PLUGIN_SCOPES)) sys.exit(2) else: print_error("Invalid filename - failed to discover plugin scope") sys.exit(2) return scope def print_error(message): # necessary fallback for micropython linesep = getattr(os, "linesep", "\n") sys.stderr.write(message + linesep) def do_fetch(ath9k): for item in ath9k.get_values(get_scope()): print(item) def do_config(ath9k): for item in ath9k.get_config(get_scope()): print(item) if __name__ == "__main__": ath9k = Ath9kDriver(SYS_BASE_DIR, GRAPH_BASE_NAME) # parse arguments if len(sys.argv) > 1: if sys.argv[1] == "config": do_config(ath9k) if os.getenv("MUNIN_CAP_DIRTYCONFIG") == "1": do_fetch(ath9k) sys.exit(0) elif sys.argv[1] == "autoconf": if os.path.exists(SYS_BASE_DIR): print('yes') else: print('no (missing ath9k driver sysfs directory: {})'.format(SYS_BASE_DIR)) sys.exit(0) elif sys.argv[1] == "suggest": if ath9k.has_devices(): for scope in PLUGIN_SCOPES: # skip the "dfs_events" scope if there is not DFS support if (scope != "dfs_events") or ath9k.has_dfs_support(): print(scope) sys.exit(0) elif sys.argv[1] == "version": print_error('olsrd Munin plugin, version %s' % plugin_version) sys.exit(0) elif sys.argv[1] == "": # ignore pass else: # unknown argument print_error("Unknown argument") sys.exit(1) do_fetch(ath9k) # final marker for shell / python hybrid script (see "Interpreter Selection") EOF = True EOF