- Repository
- Munin (contrib)
- Last change
- 2020-10-28
- Graph Categories
- Family
- contrib manual
- Keywords
- Language
- Python (3.x)
- License
- GPL-2.0-only
- Authors
snmp__airport
Name
Monitor various items of data from an Apple Airport Express/Extreme or a Time Capsule.
Installation
To use this plugin, use specially named symlinks:
cd /etc/munin/plugins
ln -s /path/to/snmp__airport snmp_myairport_airport_clients
ln -s /path/to/snmp__airport snmp_myairport_airport_dhcpclients
ln -s /path/to/snmp__airport snmp_myairport_airport_rate
ln -s /path/to/snmp__airport snmp_myairport_airport_signal
ln -s /path/to/snmp__airport snmp_myairport_airport_noise
NOTE: the name ‘myairport’ should be a valid hostname or IP address for your Airport. It can be any value, but it must not include the character ‘_’.
Now add a virtual host entry to your munin server’s munin.conf:
[myairport]
address 123.123.123.123
user_node_name no
(with the correct IP address, obviously)
this will create a virtual host in munin for the airport named ‘myairport’ and produce graphs for:
- number of connected wireless clients
- number of active DHCP leases
- rate at which clients are connected (in Mb/s)
- signal quality of connected clients (in dB)
- noise level of connected clients (in dB)
Authors
Copyright (C) 2011 Chris Jones cmsj@tenshu.net
License
This script is released under the GNU GPL v2 license.
SPDX-License-Identifier: GPL-2.0-only
Magic Markers
#%# capabilities=
#%# family=contrib manual
#!/usr/bin/env python3
"""
=head1 NAME
Monitor various items of data from an Apple Airport Express/Extreme or a Time Capsule.
=head1 INSTALLATION
To use this plugin, use specially named symlinks:
cd /etc/munin/plugins
ln -s /path/to/snmp__airport snmp_myairport_airport_clients
ln -s /path/to/snmp__airport snmp_myairport_airport_dhcpclients
ln -s /path/to/snmp__airport snmp_myairport_airport_rate
ln -s /path/to/snmp__airport snmp_myairport_airport_signal
ln -s /path/to/snmp__airport snmp_myairport_airport_noise
NOTE: the name 'myairport' should be a valid hostname or IP address for your
Airport. It can be any value, but it must not include the character '_'.
Now add a virtual host entry to your munin server's munin.conf:
[myairport]
address 123.123.123.123
user_node_name no
(with the correct IP address, obviously)
this will create a virtual host in munin for the airport named 'myairport' and
produce graphs for:
=over 4
=item number of connected wireless clients
=item number of active DHCP leases
=item rate at which clients are connected (in Mb/s)
=item signal quality of connected clients (in dB)
=item noise level of connected clients (in dB)
=back
=head1 AUTHORS
Copyright (C) 2011 Chris Jones <cmsj@tenshu.net>
=head1 LICENSE
This script is released under the GNU GPL v2 license.
SPDX-License-Identifier: GPL-2.0-only
=head1 MAGIC MARKERS
#%# capabilities=
#%# family=contrib manual
=cut
"""
import sys
import os
try:
import netsnmp
except ImportError:
print("""ERROR: Unable to import netsnmp.
Please install the Python bindings for libsnmp.
On Debian/Ubuntu machines this package is named 'libsnmp-python'""", file=sys.stderr)
sys.exit(-3)
DEBUG = None
CMDS = ['type', 'rates', 'time', 'lastrefresh', 'signal', 'noise', 'rate', 'rx', 'tx', 'rxerr',
'txerr']
CMD = None
DESTHOST = None
NUMCLIENTS = None
NUMDHCPCLIENTS = None
WANIFINDEX = None
def dbg(text):
"""Print some debugging text if DEBUG=1 is in our environment"""
if DEBUG is not None:
print("DEBUG: %s" % text)
def usage():
"""Print some usage information about ourselves"""
print(__doc__)
def parseName(name):
"""Examining argv[0] (i.e. the name of this script) for the hostname we should
be talking to and the type of check we want to run. The hostname should be
a valid, resolvable hostname, or an IP address. The command can be any of:
* clients - number of connected wireless clients
* signal - dB reported by the wireless clients for signal strength
* noise - dB reported by the wireless clients for noise level
* rate - Mb/s rate the wireless clients are connected at
The name should take the form snmp_HOSTORIP_airport_COMMAND
"""
bits = name.split('_')
if len(bits) >= 4:
destHost = bits[1]
cmd = bits[3]
dbg("parseName split '%s' into '%s'/'%s'" % (name, destHost, cmd))
return (destHost, cmd)
else:
dbg("parseName found an inconsistent name: '%s'" % name)
return None
def tableToDict(table, num):
"""The netsnmp library returns a tuple with all of the data, it is not in any
way formatted into rows. This function converts the data into a structured
dictionary, with each key being the MAC address of a wireless client. The
associated value will be a dictionary containing the information available
about the client:
* type - 1 = sta, 2 = wds
* rates - the wireless rates available to the client
* time - length of time the client has been connected
* lastrefresh - time since the client last refreshed
* signal - dB signal strength reported by the client (or -1)
* noise - dB noise level reported by the client (or -1)
* rate - Mb/s rate the client is connected at
* rx - number of packets received by the client
* tx - number of packets transmitted by the client
* rxerr - number of error packets received by the client
* txerr - number of error packets transmitted by the client
"""
table = list(table)
clients = []
clientTable = {}
# First get the MACs
i = num
while i > 0:
data = table.pop(0)
clients.append(data)
clientTable[data] = {}
dbg("tableToDict: found client '%s'" % data)
i = i - 1
for cmd in CMDS:
i = 0
while i < num:
data = table.pop(0)
clientTable[clients[i]][cmd] = data
dbg("tableToDict: %s['%s'] = %s" % (clients[i], cmd, data))
i = i + 1
return clientTable
def getNumClients():
"""Returns the number of wireless clients connected to the Airport we are
examining. This will only ever be polled via SNMP once per invocation. If
called a second time, it will just return the first value it found. This is
intended to be an optimisation to reduce SNMP roundtrips because this script
should not be long-running"""
global NUMCLIENTS
wirelessNumberOID = '.1.3.6.1.4.1.63.501.3.2.1.0'
# Dumbly cache this so we only look it up once.
if NUMCLIENTS is None:
NUMCLIENTS = int(netsnmp.snmpget(netsnmp.Varbind(wirelessNumberOID),
Version=2, DestHost=DESTHOST,
Community='public')[0])
dbg("getNumClients: polled SNMP for client number")
dbg("getNumClients: found %d clients" % NUMCLIENTS)
return NUMCLIENTS
def getNumDHCPClients():
"""Returns the number of DHCP clients with currently active leases. This
will only ever be polled via SNMP once per invocation. If called a second
time, it will just return the first value it found. This is intended to be
an optimisation to reduce SNMP roundtrips because this script should not be
long-running"""
global NUMDHCPCLIENTS
dhcpNumberOID = '.1.3.6.1.4.1.63.501.3.3.1.0'
# Dumbly cache this so we only look it up once.
if NUMDHCPCLIENTS is None:
NUMDHCPCLIENTS = int(netsnmp.snmpget(netsnmp.Varbind(dhcpNumberOID),
Version=2, DestHost=DESTHOST,
Community='public')[0])
dbg("getNumDHCPClients: polled SNMP for dhcp client number")
dbg("getNumDHCPClients: found %d clients" % NUMDHCPCLIENTS)
return NUMDHCPCLIENTS
def getExternalInterface():
"""Returns the index of the WAN interface of the Airport. This will only
ever be polled via SNMP once per invocation, per getNum*Clients(). See
above."""
global WANIFINDEX
iFaceNames = '.1.3.6.1.2.1.2.2.1.2'
if WANIFINDEX is None:
interfaces = list(netsnmp.snmpwalk(netsnmp.Varbind(iFaceNames),
Version=2, DestHost=DESTHOST,
Community='public'))
dbg("getExternalInterface: found interfaces: %s" % interfaces)
try:
WANIFINDEX = interfaces.index('mgi1') + 1
except ValueError:
print("ERROR: Unable to find WAN interface mgi1")
print(interfaces)
sys.exit(-3)
dbg("getExternalInterface: found mgi1 at index: %d" % WANIFINDEX)
return WANIFINDEX
def getExternalInOctets():
"""Returns the number of octets of inbound traffic on the WAN interface"""
return getOctets('In')
def getExternalOutOctets():
"""Returns the number of octets of outbound traffic on the WAN interface"""
return getOctets('Out')
def getOctets(direction):
"""Returns the number of octets of traffic on the WAN interface in the
requested direction"""
index = getExternalInterface()
if direction == 'In':
iFaceOctets = '.1.3.6.1.2.1.2.2.1.10.%s' % index
else:
iFaceOctets = '.1.3.6.1.2.1.2.2.1.16.%s' % index
return int(netsnmp.snmpget(netsnmp.Varbind(iFaceOctets),
Version=2, DestHost=DESTHOST,
Community='public')[0])
def getWanSpeed():
"""Returns the speed of the WAN interface"""
ifSpeed = "1.3.6.1.2.1.2.2.1.5.%s" % getExternalInterface()
dbg("getWanSpeed: OID for WAN interface speed: %s" % ifSpeed)
try:
wanSpeed = int(netsnmp.snmpget(netsnmp.Varbind(ifSpeed),
Version=2, DestHost=DESTHOST,
Community='public')[0])
except: # noqa: E722 (TODO: specify the expected exceptions)
dbg("getWanSpeed: Unable to probe for data, defaultint to 10000000")
wanSpeed = 10000000
return wanSpeed
def getData():
"""Returns a dictionary populated with all of the wireless clients and their
metadata"""
wirelessClientTableOID = '.1.3.6.1.4.1.63.501.3.2.2.1'
numClients = getNumClients()
if numClients == 0:
# FIXME: what's actually the correct munin plugin behaviour if there is no
# data to be presented?
dbg("getData: 0 clients found, exiting")
sys.exit(0)
dbg("getData: polling SNMP for client table")
clientTable = netsnmp.snmpwalk(netsnmp.Varbind(wirelessClientTableOID),
Version=2, DestHost=DESTHOST,
Community='public')
clients = tableToDict(clientTable, numClients)
return clients
def main(clients=None):
"""This function fetches metadata about wireless clients if needed, then
displays whatever values have been requested"""
if clients is None and CMD not in ['clients', 'dhcpclients', 'wanTraffic']:
clients = getData()
if CMD == 'clients':
print("clients.value %s" % getNumClients())
elif CMD == 'dhcpclients':
print("dhcpclients.value %s" % getNumDHCPClients())
elif CMD == 'wanTraffic':
print("recv.value %s" % getExternalInOctets())
print("send.value %s" % getExternalOutOctets())
else:
for client in clients:
print("MAC_%s.value %s" % (client, clients[client][CMD]))
if __name__ == '__main__':
clients = None
if os.getenv('DEBUG') == '1':
DEBUG = True
netsnmp.verbose = 1
else:
netsnmp.verbose = 0
BITS = parseName(sys.argv[0])
if BITS is None:
usage()
sys.exit(0)
else:
DESTHOST = BITS[0]
CMD = BITS[1]
if len(sys.argv) > 1:
if sys.argv[1] == 'config':
print("""
graph_category network
host_name %s""" % DESTHOST)
if CMD == 'signal':
print("""graph_args -l 0 --lower-limit -100 --upper-limit 0
graph_title Wireless client signal
graph_scale no
graph_vlabel dBm Signal""")
elif CMD == 'noise':
print("""graph_args -l 0 --lower-limit -100 --upper-limit 0
graph_title Wireless client noise
graph_scale no
graph_vlabel dBm Noise""")
elif CMD == 'rate':
print("""graph_args -l 0 --lower-limit 0 --upper-limit 500
graph_title Wireless client WiFi rate
graph_scale no
graph_vlabel WiFi Rate""")
elif CMD == 'clients':
print("""graph_title Number of connected clients
graph_args --base 1000 -l 0
graph_vlabel number of wireless clients
graph_info This graph shows the number of wireless clients connected
clients.label clients
clients.draw LINE2
clients.info The number of clients.""")
elif CMD == 'dhcpclients':
print("""graph_title Number of active DHCP leases
graph_args --base 1000 -l 0
graph_vlabel number of DHCP clients
graph_info This graph shows the number of active DHCP leases
dhcpclients.label leases
dhcpclients.draw LINE2
dhcpclients.info The number of leases.""")
elif CMD == 'wanTraffic':
speed = getWanSpeed()
print("""graph_title WAN interface traffic
graph_order recv send
graph_args --base 1000
graph_vlabel bits in (-) / out (+) per ${graph_period}
graph_category network
graph_info This graph shows traffic for the mgi1 network interface
send.info Bits sent/received by this interface.
recv.label recv
recv.type DERIVE
recv.graph no
recv.cdef recv,8,*
recv.max %s
recv.min 0
send.label bps
send.type DERIVE
send.negative recv
send.cdef send,8,*
send.max %s
send.min 0""" % (speed, speed))
else:
print("Unknown command: %s" % CMD, file=sys.stderr)
sys.exit(-2)
if CMD in ['clients', 'dhcpclients', 'wanTraffic']:
# This is static, so we sent the .label data above
pass
else:
clients = getData()
for client in clients:
print("MAC_%s.label %s" % (client, client))
sys.exit(0)
else:
main(clients)