Permalink
Browse files

Merge pull request #271 from cmsj/master

Add a plugin to monitor Apple Airport devices
  • Loading branch information...
2 parents 078e38f + 23a5a6f commit f7e9d542f8176ed04ccfe15f88319de391fb79b8 @kenyon kenyon committed Mar 15, 2013
Showing with 355 additions and 0 deletions.
  1. +355 −0 plugins/snmp/snmp__airport
View
@@ -0,0 +1,355 @@
+#!/usr/bin/python
+"""
+Munin plugin to monitor various items of data from an Apple Airport
+Express/Extreme or a Time Capsule.
+
+v1.0 by Chris Jones <cmsj@tenshu.net>
+Copyright (C) 2011 Chris Jones
+This script is released under the GNU GPL v2 license.
+
+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)
+
+# Magic markers
+#%# capabilities=
+#%# family=contrib manual
+"""
+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'"""
+ 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):
+ """Examing 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 becaues 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:
+ 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
+ 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)
+

0 comments on commit f7e9d54

Please sign in to comment.