root / plugins / snmp / snmp__airport @ 8589c6df
Historique | Voir | Annoter | Télécharger (12,3 ko)
| 1 | 23a5a6f4 | Chris Jones | #!/usr/bin/python |
|---|---|---|---|
| 2 | """ |
||
| 3 | Munin plugin to monitor various items of data from an Apple Airport |
||
| 4 | Express/Extreme or a Time Capsule. |
||
| 5 | |||
| 6 | v1.0 by Chris Jones <cmsj@tenshu.net> |
||
| 7 | Copyright (C) 2011 Chris Jones |
||
| 8 | This script is released under the GNU GPL v2 license. |
||
| 9 | |||
| 10 | To use this plugin, use specially named symlinks: |
||
| 11 | |||
| 12 | cd /etc/munin/plugins |
||
| 13 | ln -s /path/to/snmp__airport snmp_myairport_airport_clients |
||
| 14 | ln -s /path/to/snmp__airport snmp_myairport_airport_dhcpclients |
||
| 15 | ln -s /path/to/snmp__airport snmp_myairport_airport_rate |
||
| 16 | ln -s /path/to/snmp__airport snmp_myairport_airport_signal |
||
| 17 | ln -s /path/to/snmp__airport snmp_myairport_airport_noise |
||
| 18 | |||
| 19 | NOTE: the name 'myairport' should be a valid hostname or IP address for your |
||
| 20 | Airport. It can be any value, but it must not include the character '_'. |
||
| 21 | |||
| 22 | Now add a virtual host entry to your munin server's munin.conf: |
||
| 23 | |||
| 24 | [myairport] |
||
| 25 | address 123.123.123.123 |
||
| 26 | user_node_name no |
||
| 27 | |||
| 28 | (with the correct IP address, obviously) |
||
| 29 | |||
| 30 | this will create a virtual host in munin for the airport named 'myairport' and |
||
| 31 | produce graphs for: |
||
| 32 | * number of connected wireless clients |
||
| 33 | * number of active DHCP leases |
||
| 34 | * rate at which clients are connected (in Mb/s) |
||
| 35 | * signal quality of connected clients (in dB) |
||
| 36 | * noise level of connected clients (in dB) |
||
| 37 | |||
| 38 | # Magic markers |
||
| 39 | #%# capabilities= |
||
| 40 | #%# family=contrib manual |
||
| 41 | """ |
||
| 42 | import sys |
||
| 43 | import os |
||
| 44 | try: |
||
| 45 | import netsnmp |
||
| 46 | except ImportError: |
||
| 47 | print """ERROR: Unable to import netsnmp. |
||
| 48 | Please install the Python bindings for libsnmp. |
||
| 49 | On Debian/Ubuntu machines this package is named 'libsnmp-python'""" |
||
| 50 | sys.exit(-3) |
||
| 51 | |||
| 52 | DEBUG=None |
||
| 53 | CMDS=['type', 'rates', 'time', 'lastrefresh', 'signal', 'noise', 'rate', 'rx', |
||
| 54 | 'tx', 'rxerr', 'txerr'] |
||
| 55 | CMD=None |
||
| 56 | DESTHOST=None |
||
| 57 | NUMCLIENTS=None |
||
| 58 | NUMDHCPCLIENTS=None |
||
| 59 | WANIFINDEX=None |
||
| 60 | |||
| 61 | def dbg(text): |
||
| 62 | """Print some debugging text if DEBUG=1 is in our environment""" |
||
| 63 | if DEBUG is not None: |
||
| 64 | print "DEBUG: %s" % text |
||
| 65 | |||
| 66 | def usage(): |
||
| 67 | """Print some usage information about ourselves""" |
||
| 68 | print __doc__ |
||
| 69 | |||
| 70 | def parseName(name): |
||
| 71 | """Examing argv[0] (i.e. the name of this script) for the hostname we should |
||
| 72 | be talking to and the type of check we want to run. The hostname should be |
||
| 73 | a valid, resolvable hostname, or an IP address. The command can be any of: |
||
| 74 | * clients - number of connected wireless clients |
||
| 75 | * signal - dB reported by the wireless clients for signal strength |
||
| 76 | * noise - dB reported by the wireless clients for noise level |
||
| 77 | * rate - Mb/s rate the wireless clients are connected at |
||
| 78 | |||
| 79 | The name should take the form snmp_HOSTORIP_airport_COMMAND |
||
| 80 | """ |
||
| 81 | bits = name.split('_')
|
||
| 82 | if len(bits) >= 4: |
||
| 83 | destHost = bits[1] |
||
| 84 | cmd = bits[3] |
||
| 85 | dbg("parseName split '%s' into '%s'/'%s'" % (name, destHost, cmd))
|
||
| 86 | return (destHost, cmd) |
||
| 87 | else: |
||
| 88 | dbg("parseName found an inconsistent name: '%s'" % name)
|
||
| 89 | return None |
||
| 90 | |||
| 91 | def tableToDict(table, num): |
||
| 92 | """The netsnmp library returns a tuple with all of the data, it is not in any |
||
| 93 | way formatted into rows. This function converts the data into a structured |
||
| 94 | dictionary, with each key being the MAC address of a wireless client. The |
||
| 95 | associated value will be a dictionary containing the information available |
||
| 96 | about the client: |
||
| 97 | * type - 1 = sta, 2 = wds |
||
| 98 | * rates - the wireless rates available to the client |
||
| 99 | * time - length of time the client has been connected |
||
| 100 | * lastrefresh - time since the client last refreshed |
||
| 101 | * signal - dB signal strength reported by the client (or -1) |
||
| 102 | * noise - dB noise level reported by the client (or -1) |
||
| 103 | * rate - Mb/s rate the client is connected at |
||
| 104 | * rx - number of packets received by the client |
||
| 105 | * tx - number of packets transmitted by the client |
||
| 106 | * rxerr - number of error packets received by the client |
||
| 107 | * txerr - number of error packets transmitted by the client |
||
| 108 | """ |
||
| 109 | table = list(table) |
||
| 110 | clients = [] |
||
| 111 | clientTable = {}
|
||
| 112 | |||
| 113 | # First get the MACs |
||
| 114 | i = num |
||
| 115 | while i > 0: |
||
| 116 | data = table.pop(0) |
||
| 117 | clients.append(data) |
||
| 118 | clientTable[data] = {}
|
||
| 119 | dbg("tableToDict: found client '%s'" % data)
|
||
| 120 | i = i - 1 |
||
| 121 | |||
| 122 | for cmd in CMDS: |
||
| 123 | i = 0 |
||
| 124 | while i < num: |
||
| 125 | data = table.pop(0) |
||
| 126 | clientTable[clients[i]][cmd] = data |
||
| 127 | dbg("tableToDict: %s['%s'] = %s" % (clients[i], cmd, data))
|
||
| 128 | i = i + 1 |
||
| 129 | |||
| 130 | return clientTable |
||
| 131 | |||
| 132 | def getNumClients(): |
||
| 133 | """Returns the number of wireless clients connected to the Airport we are |
||
| 134 | examining. This will only ever be polled via SNMP once per invocation. If |
||
| 135 | called a second time, it will just return the first value it found. This is |
||
| 136 | intended to be an optimisation to reduce SNMP roundtrips because this script |
||
| 137 | should not be long-running""" |
||
| 138 | global NUMCLIENTS |
||
| 139 | wirelessNumberOID = '.1.3.6.1.4.1.63.501.3.2.1.0' |
||
| 140 | |||
| 141 | # Dumbly cache this so we only look it up once. |
||
| 142 | if NUMCLIENTS is None: |
||
| 143 | NUMCLIENTS = int(netsnmp.snmpget(netsnmp.Varbind(wirelessNumberOID), |
||
| 144 | Version=2, DestHost=DESTHOST, |
||
| 145 | Community='public')[0]) |
||
| 146 | dbg("getNumClients: polled SNMP for client number")
|
||
| 147 | |||
| 148 | dbg("getNumClients: found %d clients" % NUMCLIENTS)
|
||
| 149 | return NUMCLIENTS |
||
| 150 | |||
| 151 | def getNumDHCPClients(): |
||
| 152 | """Returns the number of DHCP clients with currently active leases. This |
||
| 153 | will only ever be polled via SNMP once per invocation. If called a second |
||
| 154 | time, it will just return the first value it found. This is intended to be |
||
| 155 | fba800ae | Veres Lajos | an optimisation to reduce SNMP roundtrips because this script should not be |
| 156 | 23a5a6f4 | Chris Jones | long-running""" |
| 157 | global NUMDHCPCLIENTS |
||
| 158 | dhcpNumberOID = '.1.3.6.1.4.1.63.501.3.3.1.0' |
||
| 159 | |||
| 160 | # Dumbly cache this so we only look it up once. |
||
| 161 | if NUMDHCPCLIENTS is None: |
||
| 162 | NUMDHCPCLIENTS = int(netsnmp.snmpget(netsnmp.Varbind(dhcpNumberOID), |
||
| 163 | Version=2, DestHost=DESTHOST, |
||
| 164 | Community='public')[0]) |
||
| 165 | dbg("getNumDHCPClients: polled SNMP for dhcp client number")
|
||
| 166 | |||
| 167 | dbg("getNumDHCPClients: found %d clients" % NUMDHCPCLIENTS)
|
||
| 168 | return NUMDHCPCLIENTS |
||
| 169 | |||
| 170 | def getExternalInterface(): |
||
| 171 | """Returns the index of the WAN interface of the Airport. This will only |
||
| 172 | ever be polled via SNMP once per invocation, per getNum*Clients(). See |
||
| 173 | above.""" |
||
| 174 | global WANIFINDEX |
||
| 175 | iFaceNames = '.1.3.6.1.2.1.2.2.1.2' |
||
| 176 | |||
| 177 | if WANIFINDEX is None: |
||
| 178 | interfaces = list(netsnmp.snmpwalk(netsnmp.Varbind(iFaceNames), |
||
| 179 | Version=2, DestHost=DESTHOST, |
||
| 180 | Community='public')) |
||
| 181 | dbg("getExternalInterface: found interfaces: %s" % interfaces)
|
||
| 182 | try: |
||
| 183 | WANIFINDEX = interfaces.index('mgi1') + 1
|
||
| 184 | except ValueError: |
||
| 185 | print "ERROR: Unable to find WAN interface mgi1" |
||
| 186 | print interfaces |
||
| 187 | sys.exit(-3) |
||
| 188 | |||
| 189 | dbg("getExternalInterface: found mgi1 at index: %d" % WANIFINDEX)
|
||
| 190 | return WANIFINDEX |
||
| 191 | |||
| 192 | def getExternalInOctets(): |
||
| 193 | """Returns the number of octets of inbound traffic on the WAN interface""" |
||
| 194 | return getOctets('In')
|
||
| 195 | |||
| 196 | def getExternalOutOctets(): |
||
| 197 | """Returns the number of octets of outbound traffic on the WAN interface""" |
||
| 198 | return getOctets('Out')
|
||
| 199 | |||
| 200 | def getOctets(direction): |
||
| 201 | """Returns the number of octets of traffic on the WAN interface in the |
||
| 202 | requested direction""" |
||
| 203 | index = getExternalInterface() |
||
| 204 | |||
| 205 | if direction == 'In': |
||
| 206 | iFaceOctets = '.1.3.6.1.2.1.2.2.1.10.%s' % index |
||
| 207 | else: |
||
| 208 | iFaceOctets = '.1.3.6.1.2.1.2.2.1.16.%s' % index |
||
| 209 | |||
| 210 | return int(netsnmp.snmpget(netsnmp.Varbind(iFaceOctets), |
||
| 211 | Version=2, DestHost=DESTHOST, |
||
| 212 | Community='public')[0]) |
||
| 213 | |||
| 214 | def getWanSpeed(): |
||
| 215 | """Returns the speed of the WAN interface""" |
||
| 216 | ifSpeed = "1.3.6.1.2.1.2.2.1.5.%s" % getExternalInterface() |
||
| 217 | dbg("getWanSpeed: OID for WAN interface speed: %s" % ifSpeed)
|
||
| 218 | try: |
||
| 219 | wanSpeed = int(netsnmp.snmpget(netsnmp.Varbind(ifSpeed), |
||
| 220 | Version=2, DestHost=DESTHOST, |
||
| 221 | Community='public')[0]) |
||
| 222 | except: |
||
| 223 | dbg("getWanSpeed: Unable to probe for data, defaultint to 10000000")
|
||
| 224 | wanSpeed = 10000000 |
||
| 225 | |||
| 226 | return wanSpeed |
||
| 227 | |||
| 228 | def getData(): |
||
| 229 | """Returns a dictionary populated with all of the wireless clients and their |
||
| 230 | metadata""" |
||
| 231 | wirelessClientTableOID = '.1.3.6.1.4.1.63.501.3.2.2.1' |
||
| 232 | |||
| 233 | numClients = getNumClients() |
||
| 234 | |||
| 235 | if numClients == 0: |
||
| 236 | # FIXME: what's actually the correct munin plugin behaviour if there is no |
||
| 237 | # data to be presented? |
||
| 238 | dbg("getData: 0 clients found, exiting")
|
||
| 239 | sys.exit(0) |
||
| 240 | |||
| 241 | dbg("getData: polling SNMP for client table")
|
||
| 242 | clientTable = netsnmp.snmpwalk(netsnmp.Varbind(wirelessClientTableOID), |
||
| 243 | Version=2, DestHost=DESTHOST, |
||
| 244 | Community='public') |
||
| 245 | clients = tableToDict(clientTable, numClients) |
||
| 246 | |||
| 247 | return clients |
||
| 248 | |||
| 249 | def main(clients=None): |
||
| 250 | """This function fetches metadata about wireless clients if needed, then |
||
| 251 | displays whatever values have been requested""" |
||
| 252 | if clients is None and CMD not in ['clients', 'dhcpclients', 'wanTraffic']: |
||
| 253 | clients = getData() |
||
| 254 | |||
| 255 | if CMD == 'clients': |
||
| 256 | print "clients.value %s" % getNumClients() |
||
| 257 | elif CMD == 'dhcpclients': |
||
| 258 | print "dhcpclients.value %s" % getNumDHCPClients() |
||
| 259 | elif CMD == 'wanTraffic': |
||
| 260 | print "recv.value %s" % getExternalInOctets() |
||
| 261 | print "send.value %s" % getExternalOutOctets() |
||
| 262 | else: |
||
| 263 | for client in clients: |
||
| 264 | print "MAC_%s.value %s" % (client, clients[client][CMD]) |
||
| 265 | |||
| 266 | if __name__ == '__main__': |
||
| 267 | clients = None |
||
| 268 | if os.getenv('DEBUG') == '1':
|
||
| 269 | DEBUG = True |
||
| 270 | netsnmp.verbose = 1 |
||
| 271 | else: |
||
| 272 | netsnmp.verbose = 0 |
||
| 273 | |||
| 274 | BITS = parseName(sys.argv[0]) |
||
| 275 | if BITS is None: |
||
| 276 | usage() |
||
| 277 | sys.exit(0) |
||
| 278 | else: |
||
| 279 | DESTHOST = BITS[0] |
||
| 280 | CMD = BITS[1] |
||
| 281 | |||
| 282 | if len(sys.argv) > 1: |
||
| 283 | if sys.argv[1] == 'config': |
||
| 284 | print """ |
||
| 285 | graph_category network |
||
| 286 | host_name %s""" % DESTHOST |
||
| 287 | |||
| 288 | if CMD == 'signal': |
||
| 289 | print """graph_args -l 0 --lower-limit -100 --upper-limit 0 |
||
| 290 | graph_title Wireless client signal |
||
| 291 | graph_scale no |
||
| 292 | graph_vlabel dBm Signal""" |
||
| 293 | elif CMD == 'noise': |
||
| 294 | print """graph_args -l 0 --lower-limit -100 --upper-limit 0 |
||
| 295 | graph_title Wireless client noise |
||
| 296 | graph_scale no |
||
| 297 | graph_vlabel dBm Noise""" |
||
| 298 | elif CMD == 'rate': |
||
| 299 | print """graph_args -l 0 --lower-limit 0 --upper-limit 500 |
||
| 300 | graph_title Wireless client WiFi rate |
||
| 301 | graph_scale no |
||
| 302 | graph_vlabel WiFi Rate""" |
||
| 303 | elif CMD == 'clients': |
||
| 304 | print """graph_title Number of connected clients |
||
| 305 | graph_args --base 1000 -l 0 |
||
| 306 | graph_vlabel number of wireless clients |
||
| 307 | graph_info This graph shows the number of wireless clients connected |
||
| 308 | clients.label clients |
||
| 309 | clients.draw LINE2 |
||
| 310 | clients.info The number of clients.""" |
||
| 311 | elif CMD == 'dhcpclients': |
||
| 312 | print """graph_title Number of active DHCP leases |
||
| 313 | graph_args --base 1000 -l 0 |
||
| 314 | graph_vlabel number of DHCP clients |
||
| 315 | graph_info This graph shows the number of active DHCP leases |
||
| 316 | dhcpclients.label leases |
||
| 317 | dhcpclients.draw LINE2 |
||
| 318 | dhcpclients.info The number of leases.""" |
||
| 319 | elif CMD == 'wanTraffic': |
||
| 320 | speed = getWanSpeed() |
||
| 321 | print """graph_title WAN interface traffic |
||
| 322 | graph_order recv send |
||
| 323 | graph_args --base 1000 |
||
| 324 | graph_vlabel bits in (-) / out (+) per ${graph_period}
|
||
| 325 | graph_category network |
||
| 326 | graph_info This graph shows traffic for the mgi1 network interface |
||
| 327 | send.info Bits sent/received by this interface. |
||
| 328 | recv.label recv |
||
| 329 | recv.type DERIVE |
||
| 330 | recv.graph no |
||
| 331 | recv.cdef recv,8,* |
||
| 332 | recv.max %s |
||
| 333 | recv.min 0 |
||
| 334 | send.label bps |
||
| 335 | send.type DERIVE |
||
| 336 | send.negative recv |
||
| 337 | send.cdef send,8,* |
||
| 338 | send.max %s |
||
| 339 | send.min 0""" % (speed, speed) |
||
| 340 | else: |
||
| 341 | print "Unknown command: %s" % CMD |
||
| 342 | sys.exit(-2) |
||
| 343 | |||
| 344 | if CMD in ['clients', 'dhcpclients', 'wanTraffic']: |
||
| 345 | # This is static, so we sent the .label data above |
||
| 346 | pass |
||
| 347 | else: |
||
| 348 | clients = getData() |
||
| 349 | for client in clients: |
||
| 350 | print "MAC_%s.label %s" % (client, client) |
||
| 351 | |||
| 352 | sys.exit(0) |
||
| 353 | else: |
||
| 354 | main(clients) |
