Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

root / plugins / network / ath9k_ @ 14ff36a3

Historique | Voir | Annoter | Télécharger (19 ko)

1
#!/bin/sh
2
# weird shebang? See below: "interpreter selection"
3
#
4
# Collect information related to ath9k wireless events and states.
5
#   * rate control statistics ("rc_stats")
6
#   * events (dropped, transmitted, beacon loss, ...)
7
#   * traffic (packets, bytes)
8
#
9
# All data is collected for each separate station (in case of multiple
10
# connected peers). Combined graphs are provided as a summary.
11
#
12
#
13
# This plugin works with the following python interpreters:
14
#   * Python 3
15
#   * micropython
16
#
17
#
18
# Copyright (C) 2015 Lars Kruse <devel@sumpfralle.de>
19
#
20
#    This program is free software: you can redistribute it and/or modify
21
#    it under the terms of the GNU General Public License as published by
22
#    the Free Software Foundation, either version 3 of the License, or
23
#    (at your option) any later version.
24
#
25
#    This program is distributed in the hope that it will be useful,
26
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
27
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
28
#    GNU General Public License for more details.
29
#
30
#    You should have received a copy of the GNU General Public License
31
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
32
#
33
# Magic markers
34
#%# capabilities=autoconf suggest
35
#%# family=auto
36

    
37
"""true"
38
# ****************** Interpreter Selection ***************
39
# This unbelievable dirty hack allows to find a suitable python interpreter.
40
# This is specifically useful for OpenWRT where typically only micropython is available.
41
#
42
# This "execution hack" works as follows:
43
#   * the script is executed by busybox ash or another shell
44
#   * the above line (three quotes before and one quote after 'true') evaluates differently for shell and python:
45
#    * shell: run "true" (i.e. nothing happens)
46
#    * python: ignore everything up to the next three consecutive quotes
47
# Thus we may place shell code here that will take care for selecting an interpreter.
48

    
49
# prefer micropython if it is available - otherwise fall back to any python (2 or 3)
50
if which micropython >/dev/null; then
51
    /usr/bin/micropython "$0" "$@"
52
else
53
    python3 "$0" "$@"
54
fi
55
exit $?
56

    
57
# For shell: ignore everything starting from here until the last line of this file.
58
# This is necessary for syntax checkers that try to complain about invalid shell syntax below.
59
true <<EOF
60
"""
61

    
62

    
63
"""
64
The following graphs are generated for each physical ath9k interface:
65
    phy0_wifi0_traffic
66
      phy0_wifi0_traffic.station0
67
      ...
68
    pyh0_wifi0_events
69
      phy0_wifi0_events.station0
70
      ...
71
    pyh0_wifi0_rc_stats
72
      phy0_wifi0_rc_stats.station0
73
      ...
74
"""
75

    
76

    
77
plugin_version = "0.2"
78

    
79
STATION_TRAFFIC_COUNTERS = ("rx_bytes", "tx_bytes", "rx_packets", "tx_packets")
80
STATION_EVENT_COUNTERS = ("tx_retry_count", "tx_retry_failed", "tx_filtered", "tx_fragments",
81
                          "rx_dropped", "rx_fragments", "rx_duplicates", "beacon_loss_count")
82
# 16 colors (see http://munin-monitoring.org/wiki/fieldname.colour) for visualizing
83
# rate control selection (see rc_stats)
84
QUALITY_GRAPH_COLORS_16 = ("FF1F00", "FF4500", "FF7000", "FF9700",
85
                           "FFBC00", "FAE600", "D1FF00", "7BFF00",
86
                           "1CFF00", "06E41B", "00C43B", "009D60",
87
                           "007986", "0058A8", "0033CC", "0018DE")
88
SYS_BASE_DIR = "/sys/kernel/debug/ieee80211"
89
GRAPH_BASE_NAME = "ath9k_stats"
90
PLUGIN_SCOPES = ("traffic", "events", "rcstats")
91

    
92

    
93
import os
94
import os.path
95
import subprocess
96
import sys
97

    
98

    
99
class Station:
100

    
101
    config_map = {"events": lambda station, **kwargs: station._get_events_config(**kwargs),
102
                  "traffic": lambda station, **kwargs: station._get_traffic_config(**kwargs),
103
                  "rcstats": lambda station, **kwargs: station._get_rc_stats_config(**kwargs)}
104
    values_map = {"events": lambda station: station._events_stats,
105
                  "traffic": lambda station: station._traffic_stats,
106
                  "rcstats": lambda station: station._get_rc_stats_success()}
107

    
108
    def __init__(self, label, key, path):
109
        self._path = path
110
        self.label = label
111
        self.key = key
112
        self._events_stats = self._parse_file_based_stats(STATION_EVENT_COUNTERS)
113
        self._traffic_stats = self._parse_file_based_stats(STATION_TRAFFIC_COUNTERS)
114
        self._rc_stats = self._parse_rc_stats()
115

    
116
    def _parse_rc_stats(self):
117
        """ example content
118

    
119
         type           rate      tpt eprob *prob ret  *ok(*cum)        ok(      cum)
120
         HT20/LGI       MCS0      5.6 100.0 100.0   3    0(   0)         3(        3)
121
         HT20/LGI       MCS1     10.5 100.0 100.0   0    0(   0)         1(        1)
122
         HT20/LGI       MCS2     14.9 100.0 100.0   0    0(   0)         1(        1)
123
         HT20/LGI       MCS3     18.7  96.5 100.0   5    0(   0)       261(      328)
124
         HT20/LGI       MCS4     25.3  95.6 100.0   5    0(   0)      4267(     5460)
125
         HT20/LGI       MCS5     30.6  95.8 100.0   5    0(   0)     11735(    17482)
126
         HT20/LGI       MCS6     32.9  95.7 100.0   5    0(   0)     24295(    32592)
127
         HT20/LGI    DP MCS7     35.0  90.4  95.2   5    0(   0)     63356(    88600)
128
         HT20/LGI       MCS8     10.5 100.0 100.0   0    0(   0)         1(        1)
129

    
130
        beware: sometimes the last two pairs of columns are joined without withespace: "90959383(100188029)"
131
        """
132
        stats = {}
133
        with open(os.path.join(self._path, "rc_stats"), "r") as statsfile:
134
            rate_column = None
135
            skip_retry_column = False
136
            for index, line in enumerate(statsfile.readlines()):
137
                # remove trailing linebreak, replace braces (annoyingly present in the lasf four columns)
138
                line = line.rstrip().replace("(", " ").replace(")", " ")
139
                # ignore the trailing summary lines
140
                if not line:
141
                    break
142
                if index == 0:
143
                    # we need to remember the start of the "rate" column (in order to skip the flags)
144
                    rate_column = line.index("rate")
145
                    if rate_column == 0:
146
                        # the following weird format was found on a Barrier Breaker host (2014, Linux 3.10.49):
147
                        #   rate      throughput  ewma prob  this prob  this succ/attempt   success    attempts
148
                        #   ABCDP  6         5.4       89.9      100.0             0(  0)       171         183
149
                        # Thus we just assume that there are five flag letters and two blanks.
150
                        # Let's hope for the best!
151
                        rate_column = 6
152
                        # this format does not contain the "retry" column
153
                        skip_retry_column = True
154
                    # skip the header line
155
                    continue
156
                cutoff_line = line[rate_column:]
157
                tokens = cutoff_line.split()
158
                entry = {}
159
                entry["rate"] = tokens.pop(0)
160
                entry["throughput"] = float(tokens.pop(0))
161
                entry["ewma_probability"] = float(tokens.pop(0))
162
                entry["this_probability"] = float(tokens.pop(0))
163
                if skip_retry_column:
164
                    entry["retry"] = 0
165
                else:
166
                    entry["retry"] = int(tokens.pop(0))
167
                entry["this_success"] = int(tokens.pop(0))
168
                entry["this_attempts"] = int(tokens.pop(0))
169
                entry["success"] = int(tokens.pop(0))
170
                entry["attempts"] = int(tokens.pop(0))
171
                # some "rate" values are given in MBit/s - some are MCS0..15
172
                try:
173
                    entry["rate_label"] = "{rate:d} MBit/s".format(rate=int(entry["rate"]))
174
                except ValueError:
175
                    # keep the MCS string
176
                    entry["rate_label"] = entry["rate"]
177
                stats[entry["rate"]] = entry
178
        return stats
179

    
180
    def _get_rc_stats_success(self):
181
        rc_values = {self._get_rate_fieldname(rate["rate"]): rate["success"] for rate in self._rc_stats.values()}
182
        rc_values["sum"] = sum(rc_values.values())
183
        return rc_values
184

    
185
    def _parse_file_based_stats(self, counters):
186
        stats = {}
187
        for counter in counters:
188
            # some events are not handled with older versions (e.g. "beacon_loss_count")
189
            filename = os.path.join(self._path, counter)
190
            if os.path.exists(filename):
191
                content = open(filename, "r").read().strip()
192
                stats[counter] = int(content)
193
        return stats
194

    
195
    def get_values(self, scope, graph_base):
196
        func = self.values_map[scope]
197
        yield "multigraph {base}_{suffix}.{station}".format(base=graph_base, suffix=scope, station=self.key)
198
        for key, value in func(self).items():
199
            yield "{key}.value {value}".format(key=key, value=value)
200
        yield ""
201

    
202
    @classmethod
203
    def get_summary_values(cls, scope, siblings, graph_base):
204
        func = cls.values_map[scope]
205
        yield "multigraph {base}_{suffix}".format(base=graph_base, suffix=scope)
206
        stats = {}
207
        for station in siblings:
208
            for key, value in func(station).items():
209
                stats[key] = stats.get(key, 0) + value
210
        for key, value in stats.items():
211
            yield "{key}.value {value}".format(key=key, value=value)
212
        yield ""
213

    
214
    def get_config(self, scope, graph_base):
215
        func = self.config_map[scope]
216
        yield "multigraph {base}_{suffix}.{station}".format(base=graph_base, suffix=scope, station=self.key)
217
        yield from func(self, label=self.label, siblings=[self])
218

    
219
    @classmethod
220
    def get_summary_config(cls, scope, siblings, graph_base):
221
        func = cls.config_map[scope]
222
        yield "multigraph {base}_{suffix}".format(base=graph_base, suffix=scope)
223
        for station in siblings:
224
            yield from func(station, siblings=[station])
225

    
226
    @classmethod
227
    def _get_traffic_config(cls, label=None, siblings=None):
228
        if label:
229
            yield "graph_title ath9k Station Traffic {label}".format(label=label)
230
        else:
231
            yield "graph_title ath9k Station Traffic"
232
        yield "graph_args --base 1024"
233
        yield "graph_vlabel received (-) / transmitted (+)"
234
        yield "graph_category wireless"
235
        # convert bytes/s into kbit/s (x * 8 / 1000 = x / 125)
236
        yield from _get_up_down_pair("kBit/s", "tx_bytes", "rx_bytes", divider=125, use_negative=False)
237
        yield from _get_up_down_pair("Packets/s", "tx_packets", "rx_packets", use_negative=False)
238
        yield ""
239

    
240
    @classmethod
241
    def _get_events_config(cls, label=None, siblings=None):
242
        if label:
243
            yield "graph_title ath9k Station Events {label}".format(label=label)
244
        else:
245
            yield "graph_title ath9k Station Events"
246
        yield "graph_vlabel events per ${graph_period}"
247
        yield "graph_category wireless"
248
        events = set()
249
        for station in siblings:
250
            for event in STATION_EVENT_COUNTERS:
251
                events.add(event)
252
        for event in events:
253
            yield "{event}.label {event}".format(event=event)
254
            yield "{event}.type COUNTER".format(event=event)
255
        yield ""
256

    
257
    @classmethod
258
    def _get_rate_fieldname(cls, rate):
259
        return "rate_{0}".format(rate.lower()).replace(".", "_")
260

    
261
    @classmethod
262
    def _get_rc_stats_config(cls, label=None, siblings=None):
263
        if label:
264
            yield "graph_title ath9k Station Transmit Rates {label} Success".format(label=label)
265
        else:
266
            yield "graph_title ath9k Station Transmit Rates Success"
267
        yield "graph_vlabel transmit rates %"
268
        yield "graph_category wireless"
269
        yield "graph_args --base 1000 -r --lower-limit 0 --upper-limit 100"
270
        all_rates = {}
271
        # collect alle unique rates
272
        for station in siblings:
273
            for rate, details in station._rc_stats.items():
274
                all_rates[rate] = details
275
        # return all rates
276
        is_first = True
277
        num_extract = lambda text: int("".join([char for char in text if "0" <= char <= "9"]))
278
        get_key = lambda rate_name: cls._get_rate_fieldname(all_rates[rate_name]["rate"])
279
        # add all rates for percent visualization ("MCS7,MCS6,MCS5,MCS4,MCS3,MCS2,MCS1,MCS0,+,+,+,+,+,+,+")
280
        cdef = None
281
        for sum_rate in all_rates:
282
            if cdef is None:
283
                cdef = get_key(sum_rate)
284
            else:
285
                cdef = "{key},{cdef},+".format(key=get_key(sum_rate), cdef=cdef)
286
        yield "sum.label Sum of all counters"
287
        yield "sum.type DERIVE"
288
        yield "sum.graph no"
289
        for index, rate in enumerate(sorted(all_rates, key=num_extract)):
290
            details = all_rates[rate]
291
            key = get_key(rate)
292
            yield "{key}.label {rate_label}".format(key=key, rate_label=details["rate_label"])
293
            yield "{key}.type DERIVE".format(key=key)
294
            yield "{key}.min 0".format(key=key)
295
            if index < len(QUALITY_GRAPH_COLORS_16):
296
                yield "{key}.colour {colour}".format(key=key, colour=QUALITY_GRAPH_COLORS_16[index])
297
            yield "{key}.draw {draw_type}".format(key=key, draw_type=("AREA" if is_first else "STACK"))
298
            # divide the current value by the above sum of all counters and calculate percent
299
            yield "{key}.cdef 100,{key},sum,/,*".format(key=key, cdef=cdef)
300
            is_first = False
301
        yield ""
302

    
303

    
304
class WifiInterface:
305

    
306
    def __init__(self, name, path, graph_base):
307
        self._path = path
308
        self._graph_base = graph_base
309
        self.name = name
310
        self.stations = tuple(self._parse_stations())
311

    
312
    def _parse_arp_cache(self):
313
        """ read IPs and MACs from /proc/net/arp and return a dictionary for MAC -> IP """
314
        arp_cache = {}
315
        # example content:
316
        #   IP address       HW type     Flags       HW address            Mask     Device
317
        #   192.168.2.70     0x1         0x0         00:00:00:00:00:00     *        eth0.10
318
        #   192.168.12.76    0x1         0x2         24:a4:3c:fd:76:98     *        eth1.10
319
        for line in open("/proc/net/arp", "r").read().split("\n"):
320
            # skip empty lines
321
            if not line: continue
322
            tokens = line.split()
323
            ip, mac = tokens[0], tokens[3]
324
            # the header line can be ignored - all other should have well-formed MACs
325
            if not ":" in mac: continue
326
            # ignore remote peers outside of the broadcast domain
327
            if mac == "00:00:00:00:00:00": continue
328
            arp_cache[mac] = ip
329
        return arp_cache
330

    
331
    def _parse_stations(self):
332
        stations_base = os.path.join(self._path, "stations")
333
        arp_cache = self._parse_arp_cache()
334
        for item in os.listdir(stations_base):
335
            peer_mac = item
336
            # use the IP or fall back to the MAC without separators (":")
337
            if peer_mac in arp_cache:
338
                label = arp_cache[peer_mac]
339
                key = peer_mac.replace(":", "")
340
            else:
341
                label = peer_mac
342
                key = "host_" + peer_mac.replace(":", "").replace(".", "")
343
            yield Station(label, key, os.path.join(stations_base, item))
344

    
345
    def get_config(self, scope):
346
        yield from Station.get_summary_config(scope, self.stations, self._graph_base)
347
        for station in self.stations:
348
            yield from station.get_config(scope, self._graph_base)
349
        yield ""
350

    
351
    def get_values(self, scope):
352
        yield from Station.get_summary_values(scope, self.stations, self._graph_base)
353
        for station in self.stations:
354
            yield from station.get_values(scope, self._graph_base)
355
        yield ""
356

    
357

    
358
class Ath9kDriver:
359

    
360
    def __init__(self, path, graph_base):
361
        self._path = path
362
        self._graph_base = graph_base
363
        self.interfaces = tuple(self._parse_interfaces())
364

    
365
    def _parse_interfaces(self):
366
        for phy in os.listdir(self._path):
367
            phy_path = os.path.join(self._path, phy)
368
            for item in os.listdir(phy_path):
369
                if item.startswith("netdev:"):
370
                    wifi = item.split(":", 1)[1]
371
                    label = "{phy}/{interface}".format(phy=phy, interface=wifi)
372
                    wifi_path = os.path.join(phy_path, item)
373
                    graph_base = "{base}_{phy}_{interface}".format(base=self._graph_base, phy=phy, interface=wifi)
374
                    yield WifiInterface(label, wifi_path, graph_base)
375

    
376
    def get_config(self, scope):
377
        for interface in self.interfaces:
378
            yield from interface.get_config(scope)
379

    
380
    def get_values(self, scope):
381
        for interface in self.interfaces:
382
            yield from interface.get_values(scope)
383

    
384

    
385

    
386
def _get_up_down_pair(unit, key_up, key_down, factor=None, divider=None, use_negative=True):
387
    """ return all required statements for a munin-specific up/down value pair
388
        "factor" or "divider" can be given for unit conversions
389
    """
390
    for key in (key_up, key_down):
391
        if use_negative:
392
            yield "{key}.label {unit}".format(key=key, unit=unit)
393
        else:
394
            yield "{key}.label {key} {unit}".format(key=key, unit=unit)
395
        yield "{key}.type COUNTER".format(key=key)
396
        if factor:
397
            yield "{key}.cdef {key},{factor},*".format(key=key, factor=factor)
398
        if divider:
399
            yield "{key}.cdef {key},{divider},/".format(key=key, divider=divider)
400
    if use_negative:
401
        yield "{key_down}.graph no".format(key_down=key_down)
402
        yield "{key_up}.negative {key_down}".format(key_up=key_up, key_down=key_down)
403

    
404

    
405
def get_scope():
406
    called_name = os.path.basename(sys.argv[0])
407
    name_prefix = "ath9k_"
408
    if called_name.startswith(name_prefix):
409
        scope = called_name[len(name_prefix):]
410
        if not scope in PLUGIN_SCOPES:
411
            print_error("Invalid scope requested: {0} (expected: {1})".format(scope, PLUGIN_SCOPES))
412
            sys.exit(2)
413
    else:
414
        print_error("Invalid filename - failed to discover plugin scope")
415
        sys.exit(2)
416
    return scope
417

    
418

    
419
def print_error(message):
420
    # necessary fallback for micropython
421
    linesep = getattr(os, "linesep", "\n")
422
    sys.stderr.write(message + linesep)
423

    
424

    
425
if __name__ == "__main__":
426
    ath9k = Ath9kDriver(SYS_BASE_DIR, GRAPH_BASE_NAME)
427
    # parse arguments
428
    if len(sys.argv) > 1:
429
        if sys.argv[1]=="config":
430
            for item in ath9k.get_config(get_scope()):
431
                print(item)
432
            sys.exit(0)
433
        elif sys.argv[1] == "autoconf":
434
            if os.path.exists(SYS_BASE_PATH):
435
                print('yes')
436
            else:
437
                print('no')
438
            sys.exit(0)
439
        elif sys.argv[1] == "suggest":
440
            for scope in PLUGIN_SCOPES:
441
                print(scope)
442
            sys.exit(0)
443
        elif sys.argv[1] == "version":
444
            print_error('olsrd Munin plugin, version %s' % plugin_version)
445
            sys.exit(0)
446
        elif sys.argv[1] == "":
447
            # ignore
448
            pass
449
        else:
450
            # unknown argument
451
            print_error("Unknown argument")
452
            sys.exit(1)
453

    
454
    # output values
455
    for item in ath9k.get_values(get_scope()):
456
        print(item)
457

    
458
# final marker for shell / python hybrid script (see "Interpreter Selection")
459
EOF = True
460
EOF