mirror of
https://github.com/munin-monitoring/contrib.git
synced 2018-11-08 00:59:34 +01:00
14ff36a31c
This works around a syntax warning of the continuous integration system. The cause of this warning could have never triggered anyway. This commit encloses the whole python code safely in a "here" document.
461 lines
19 KiB
Bash
Executable File
461 lines
19 KiB
Bash
Executable File
#!/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)
|
|
#
|
|
# 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
|
|
#
|
|
#
|
|
# Copyright (C) 2015 Lars Kruse <devel@sumpfralle.de>
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
# 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 any python (2 or 3)
|
|
if which micropython >/dev/null; then
|
|
/usr/bin/micropython "$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 <<EOF
|
|
"""
|
|
|
|
|
|
"""
|
|
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
|
|
...
|
|
"""
|
|
|
|
|
|
plugin_version = "0.2"
|
|
|
|
STATION_TRAFFIC_COUNTERS = ("rx_bytes", "tx_bytes", "rx_packets", "tx_packets")
|
|
STATION_EVENT_COUNTERS = ("tx_retry_count", "tx_retry_failed", "tx_filtered", "tx_fragments",
|
|
"rx_dropped", "rx_fragments", "rx_duplicates", "beacon_loss_count")
|
|
# 16 colors (see http://munin-monitoring.org/wiki/fieldname.colour) for visualizing
|
|
# rate control selection (see rc_stats)
|
|
QUALITY_GRAPH_COLORS_16 = ("FF1F00", "FF4500", "FF7000", "FF9700",
|
|
"FFBC00", "FAE600", "D1FF00", "7BFF00",
|
|
"1CFF00", "06E41B", "00C43B", "009D60",
|
|
"007986", "0058A8", "0033CC", "0018DE")
|
|
SYS_BASE_DIR = "/sys/kernel/debug/ieee80211"
|
|
GRAPH_BASE_NAME = "ath9k_stats"
|
|
PLUGIN_SCOPES = ("traffic", "events", "rcstats")
|
|
|
|
|
|
import os
|
|
import os.path
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
class Station:
|
|
|
|
config_map = {"events": lambda station, **kwargs: station._get_events_config(**kwargs),
|
|
"traffic": lambda station, **kwargs: station._get_traffic_config(**kwargs),
|
|
"rcstats": lambda station, **kwargs: station._get_rc_stats_config(**kwargs)}
|
|
values_map = {"events": lambda station: station._events_stats,
|
|
"traffic": lambda station: station._traffic_stats,
|
|
"rcstats": lambda station: station._get_rc_stats_success()}
|
|
|
|
def __init__(self, label, key, path):
|
|
self._path = path
|
|
self.label = label
|
|
self.key = key
|
|
self._events_stats = self._parse_file_based_stats(STATION_EVENT_COUNTERS)
|
|
self._traffic_stats = self._parse_file_based_stats(STATION_TRAFFIC_COUNTERS)
|
|
self._rc_stats = self._parse_rc_stats()
|
|
|
|
def _parse_rc_stats(self):
|
|
""" example content
|
|
|
|
type rate tpt eprob *prob ret *ok(*cum) ok( cum)
|
|
HT20/LGI MCS0 5.6 100.0 100.0 3 0( 0) 3( 3)
|
|
HT20/LGI MCS1 10.5 100.0 100.0 0 0( 0) 1( 1)
|
|
HT20/LGI MCS2 14.9 100.0 100.0 0 0( 0) 1( 1)
|
|
HT20/LGI MCS3 18.7 96.5 100.0 5 0( 0) 261( 328)
|
|
HT20/LGI MCS4 25.3 95.6 100.0 5 0( 0) 4267( 5460)
|
|
HT20/LGI MCS5 30.6 95.8 100.0 5 0( 0) 11735( 17482)
|
|
HT20/LGI MCS6 32.9 95.7 100.0 5 0( 0) 24295( 32592)
|
|
HT20/LGI DP MCS7 35.0 90.4 95.2 5 0( 0) 63356( 88600)
|
|
HT20/LGI MCS8 10.5 100.0 100.0 0 0( 0) 1( 1)
|
|
|
|
beware: sometimes the last two pairs of columns are joined without withespace: "90959383(100188029)"
|
|
"""
|
|
stats = {}
|
|
with open(os.path.join(self._path, "rc_stats"), "r") as statsfile:
|
|
rate_column = None
|
|
skip_retry_column = False
|
|
for index, line in enumerate(statsfile.readlines()):
|
|
# remove trailing linebreak, replace braces (annoyingly present in the lasf four columns)
|
|
line = line.rstrip().replace("(", " ").replace(")", " ")
|
|
# ignore the trailing summary lines
|
|
if not line:
|
|
break
|
|
if index == 0:
|
|
# we need to remember the start of the "rate" column (in order to skip the flags)
|
|
rate_column = line.index("rate")
|
|
if rate_column == 0:
|
|
# the following weird format was found on a Barrier Breaker host (2014, Linux 3.10.49):
|
|
# rate throughput ewma prob this prob this succ/attempt success attempts
|
|
# ABCDP 6 5.4 89.9 100.0 0( 0) 171 183
|
|
# Thus we just assume that there are five flag letters and two blanks.
|
|
# Let's hope for the best!
|
|
rate_column = 6
|
|
# this format does not contain the "retry" column
|
|
skip_retry_column = True
|
|
# skip the header line
|
|
continue
|
|
cutoff_line = line[rate_column:]
|
|
tokens = cutoff_line.split()
|
|
entry = {}
|
|
entry["rate"] = tokens.pop(0)
|
|
entry["throughput"] = float(tokens.pop(0))
|
|
entry["ewma_probability"] = float(tokens.pop(0))
|
|
entry["this_probability"] = float(tokens.pop(0))
|
|
if skip_retry_column:
|
|
entry["retry"] = 0
|
|
else:
|
|
entry["retry"] = int(tokens.pop(0))
|
|
entry["this_success"] = int(tokens.pop(0))
|
|
entry["this_attempts"] = int(tokens.pop(0))
|
|
entry["success"] = int(tokens.pop(0))
|
|
entry["attempts"] = int(tokens.pop(0))
|
|
# some "rate" values are given in MBit/s - some are MCS0..15
|
|
try:
|
|
entry["rate_label"] = "{rate:d} MBit/s".format(rate=int(entry["rate"]))
|
|
except ValueError:
|
|
# keep the MCS string
|
|
entry["rate_label"] = entry["rate"]
|
|
stats[entry["rate"]] = entry
|
|
return stats
|
|
|
|
def _get_rc_stats_success(self):
|
|
rc_values = {self._get_rate_fieldname(rate["rate"]): rate["success"] for rate in self._rc_stats.values()}
|
|
rc_values["sum"] = sum(rc_values.values())
|
|
return rc_values
|
|
|
|
def _parse_file_based_stats(self, counters):
|
|
stats = {}
|
|
for counter in counters:
|
|
# some events are not handled with older versions (e.g. "beacon_loss_count")
|
|
filename = os.path.join(self._path, counter)
|
|
if os.path.exists(filename):
|
|
content = open(filename, "r").read().strip()
|
|
stats[counter] = int(content)
|
|
return stats
|
|
|
|
def get_values(self, scope, graph_base):
|
|
func = self.values_map[scope]
|
|
yield "multigraph {base}_{suffix}.{station}".format(base=graph_base, suffix=scope, station=self.key)
|
|
for key, value in func(self).items():
|
|
yield "{key}.value {value}".format(key=key, value=value)
|
|
yield ""
|
|
|
|
@classmethod
|
|
def get_summary_values(cls, scope, siblings, graph_base):
|
|
func = cls.values_map[scope]
|
|
yield "multigraph {base}_{suffix}".format(base=graph_base, suffix=scope)
|
|
stats = {}
|
|
for station in siblings:
|
|
for key, value in func(station).items():
|
|
stats[key] = stats.get(key, 0) + value
|
|
for key, value in stats.items():
|
|
yield "{key}.value {value}".format(key=key, value=value)
|
|
yield ""
|
|
|
|
def get_config(self, scope, graph_base):
|
|
func = self.config_map[scope]
|
|
yield "multigraph {base}_{suffix}.{station}".format(base=graph_base, suffix=scope, station=self.key)
|
|
yield from func(self, label=self.label, siblings=[self])
|
|
|
|
@classmethod
|
|
def get_summary_config(cls, scope, siblings, graph_base):
|
|
func = cls.config_map[scope]
|
|
yield "multigraph {base}_{suffix}".format(base=graph_base, suffix=scope)
|
|
for station in siblings:
|
|
yield from func(station, siblings=[station])
|
|
|
|
@classmethod
|
|
def _get_traffic_config(cls, label=None, siblings=None):
|
|
if label:
|
|
yield "graph_title ath9k Station Traffic {label}".format(label=label)
|
|
else:
|
|
yield "graph_title ath9k Station Traffic"
|
|
yield "graph_args --base 1024"
|
|
yield "graph_vlabel received (-) / transmitted (+)"
|
|
yield "graph_category wireless"
|
|
# convert bytes/s into kbit/s (x * 8 / 1000 = x / 125)
|
|
yield from _get_up_down_pair("kBit/s", "tx_bytes", "rx_bytes", divider=125, use_negative=False)
|
|
yield from _get_up_down_pair("Packets/s", "tx_packets", "rx_packets", use_negative=False)
|
|
yield ""
|
|
|
|
@classmethod
|
|
def _get_events_config(cls, label=None, siblings=None):
|
|
if label:
|
|
yield "graph_title ath9k Station Events {label}".format(label=label)
|
|
else:
|
|
yield "graph_title ath9k Station Events"
|
|
yield "graph_vlabel events per ${graph_period}"
|
|
yield "graph_category wireless"
|
|
events = set()
|
|
for station in siblings:
|
|
for event in STATION_EVENT_COUNTERS:
|
|
events.add(event)
|
|
for event in events:
|
|
yield "{event}.label {event}".format(event=event)
|
|
yield "{event}.type COUNTER".format(event=event)
|
|
yield ""
|
|
|
|
@classmethod
|
|
def _get_rate_fieldname(cls, rate):
|
|
return "rate_{0}".format(rate.lower()).replace(".", "_")
|
|
|
|
@classmethod
|
|
def _get_rc_stats_config(cls, label=None, siblings=None):
|
|
if label:
|
|
yield "graph_title ath9k Station Transmit Rates {label} Success".format(label=label)
|
|
else:
|
|
yield "graph_title ath9k Station Transmit Rates Success"
|
|
yield "graph_vlabel transmit rates %"
|
|
yield "graph_category wireless"
|
|
yield "graph_args --base 1000 -r --lower-limit 0 --upper-limit 100"
|
|
all_rates = {}
|
|
# collect alle unique rates
|
|
for station in siblings:
|
|
for rate, details in station._rc_stats.items():
|
|
all_rates[rate] = details
|
|
# return all rates
|
|
is_first = True
|
|
num_extract = lambda text: int("".join([char for char in text if "0" <= char <= "9"]))
|
|
get_key = lambda rate_name: cls._get_rate_fieldname(all_rates[rate_name]["rate"])
|
|
# add all rates for percent visualization ("MCS7,MCS6,MCS5,MCS4,MCS3,MCS2,MCS1,MCS0,+,+,+,+,+,+,+")
|
|
cdef = None
|
|
for sum_rate in all_rates:
|
|
if cdef is None:
|
|
cdef = get_key(sum_rate)
|
|
else:
|
|
cdef = "{key},{cdef},+".format(key=get_key(sum_rate), cdef=cdef)
|
|
yield "sum.label Sum of all counters"
|
|
yield "sum.type DERIVE"
|
|
yield "sum.graph no"
|
|
for index, rate in enumerate(sorted(all_rates, key=num_extract)):
|
|
details = all_rates[rate]
|
|
key = get_key(rate)
|
|
yield "{key}.label {rate_label}".format(key=key, rate_label=details["rate_label"])
|
|
yield "{key}.type DERIVE".format(key=key)
|
|
yield "{key}.min 0".format(key=key)
|
|
if index < len(QUALITY_GRAPH_COLORS_16):
|
|
yield "{key}.colour {colour}".format(key=key, colour=QUALITY_GRAPH_COLORS_16[index])
|
|
yield "{key}.draw {draw_type}".format(key=key, draw_type=("AREA" if is_first else "STACK"))
|
|
# divide the current value by the above sum of all counters and calculate percent
|
|
yield "{key}.cdef 100,{key},sum,/,*".format(key=key, cdef=cdef)
|
|
is_first = False
|
|
yield ""
|
|
|
|
|
|
class WifiInterface:
|
|
|
|
def __init__(self, name, path, graph_base):
|
|
self._path = path
|
|
self._graph_base = graph_base
|
|
self.name = name
|
|
self.stations = tuple(self._parse_stations())
|
|
|
|
def _parse_arp_cache(self):
|
|
""" read IPs and MACs from /proc/net/arp and return a dictionary for MAC -> 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 not line: continue
|
|
tokens = line.split()
|
|
ip, mac = tokens[0], tokens[3]
|
|
# the header line can be ignored - all other should have well-formed MACs
|
|
if not ":" in mac: continue
|
|
# ignore remote peers outside of the broadcast domain
|
|
if mac == "00:00:00:00:00:00": continue
|
|
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 Ath9kDriver:
|
|
|
|
def __init__(self, path, graph_base):
|
|
self._path = path
|
|
self._graph_base = graph_base
|
|
self.interfaces = tuple(self._parse_interfaces())
|
|
|
|
def _parse_interfaces(self):
|
|
for phy in os.listdir(self._path):
|
|
phy_path = os.path.join(self._path, phy)
|
|
for item in os.listdir(phy_path):
|
|
if item.startswith("netdev:"):
|
|
wifi = item.split(":", 1)[1]
|
|
label = "{phy}/{interface}".format(phy=phy, interface=wifi)
|
|
wifi_path = os.path.join(phy_path, item)
|
|
graph_base = "{base}_{phy}_{interface}".format(base=self._graph_base, phy=phy, interface=wifi)
|
|
yield WifiInterface(label, wifi_path, graph_base)
|
|
|
|
def get_config(self, scope):
|
|
for interface in self.interfaces:
|
|
yield from interface.get_config(scope)
|
|
|
|
def get_values(self, scope):
|
|
for interface in self.interfaces:
|
|
yield from interface.get_values(scope)
|
|
|
|
|
|
|
|
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 not scope 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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ath9k = Ath9kDriver(SYS_BASE_DIR, GRAPH_BASE_NAME)
|
|
# parse arguments
|
|
if len(sys.argv) > 1:
|
|
if sys.argv[1]=="config":
|
|
for item in ath9k.get_config(get_scope()):
|
|
print(item)
|
|
sys.exit(0)
|
|
elif sys.argv[1] == "autoconf":
|
|
if os.path.exists(SYS_BASE_PATH):
|
|
print('yes')
|
|
else:
|
|
print('no')
|
|
sys.exit(0)
|
|
elif sys.argv[1] == "suggest":
|
|
for scope in PLUGIN_SCOPES:
|
|
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)
|
|
|
|
# output values
|
|
for item in ath9k.get_values(get_scope()):
|
|
print(item)
|
|
|
|
# final marker for shell / python hybrid script (see "Interpreter Selection")
|
|
EOF = True
|
|
EOF
|