#!/usr/bin/env python """ =head1 NAME linux_if - munin plugin monitoring Linux network interfaces =head1 DESCRIPTION This is not a wildcard plugin. Monitored interfaces are controlled by 'include', 'exclude' in config. By default, only statically configured interfaces (and their sub-interfaces) are monitored. Features: =over =item bonding - group bonding slave interfaces with master =item vlans - group vlan sub-interfaces with main (dot1q trunk) interface =back =head1 CONFIGURATION [linux_if] # run plugin as root (required if you have VLAN sub-interfaces) user = root # comma separated list of interface patterns to exclude from monitoring # default: lo # example: env.exclude = lo,vnet* # comma separated list of interface patterns to include in monitoring # default: (empty) # example: env.include = br_* # should statically configured interfaces be included (they have ifcfg-* file) # default: true env.include_configured_if = true Include/exclude logic in detail. Interface name is matched according to the following rules: =over 4 =item 1. if matched by any exclude pattern, then exclude. Otherwise next step. =item 2. if matched by any include pattern, then include, Otherwise next step. =item 3. if 'include_configured_if' is true and 'ifcfg-*' file exists then include =item 4. default is not to include interface in monitoring =item 5. automatically include sub-interface, if the parent interface is monitored =back Tested on: RHEL 6.x and clones (with Python 2.6) =head1 TODO =over 4 =item implement 'data loaning' between graphs, removes duplicit measures =item add support for bridging =item configurable graph max based on interface speed =back =head1 MAGIC MARKERS #%# family=manual =cut """ __author__ = 'Brano Zarnovican' __email__ = 'zarnovican@gmail.com' __license__ = 'BSD' __version__ = '0.9' import fnmatch, os, sys #from pprint import pprint # # handle 'autoconf' option # if len(sys.argv) > 1 and sys.argv[1] == 'autoconf': if os.path.exists('/proc/net/dev'): print('yes') sys.exit(0) else: print('no') sys.exit(1) # # plugin configuration # exclude_patterns = os.environ.get('exclude', 'lo').split(',') include_patterns = os.environ.get('include', '').split(',') include_configured_if = os.environ.get('include_configured_if', 'true').lower() def interface_is_enabled(ifname): """logic to include or exclude this interface in plugin based on configuration""" if any(fnmatch.fnmatch(ifname, pattern) for pattern in exclude_patterns): return False if any(fnmatch.fnmatch(ifname, pattern) for pattern in include_patterns): return True if include_configured_if == 'true' and \ os.path.exists('/etc/sysconfig/network-scripts/ifcfg-'+ifname): return True return False # # read counts for all interfaces (for both 'update' or 'config') # interface = {} # interface[name][measure] = value try: fieldnames = ('rxbytes', 'rxpackets', 'rxerrs', 'rxdrop', 'rxfifo', 'rxframe', 'rxcompressed', 'rxmulticast') +\ ('txbytes', 'txpackets', 'txerrs', 'txdrop', 'txfifo', 'txcolls', 'txcarrier', 'txcompressed') with open('/proc/net/dev') as f: f.readline() # skip 2-line header f.readline() for line in f: l = line.replace('|', ' ').replace(':', ' ').split() ifname = l[0].strip(':') assert len(l) == 17, 'Unexpected number of fields (%d)' % len(l) interface[ifname] = dict(zip(fieldnames, l[1:])) interface[ifname]['name'] = ifname interface[ifname]['sname'] = ifname.replace('.', '_') # sanitized interface name except IOError as e: print(e) sys.exit(-1) # # associate slave interfaces to their bond masters # bond = {} # bond[bondname][slavename][measure] = value try: with open('/sys/class/net/bonding_masters') as f: bond_list = f.read().split() for bondname in bond_list: if bondname not in interface: continue if not interface_is_enabled(bondname): continue bond[bondname] = { 'subifs': [], } bond[bondname]['parent'] = interface[bondname] with open('/sys/class/net/'+bondname+'/bonding/slaves') as f: slave_list = f.read().split() for slave in slave_list: if slave not in interface: continue bond[bondname]['subifs'].append(slave) bond[bondname][slave] = interface[slave] del interface[slave] except IOError: pass # bonding not configured # # associate VLAN sub-interfaces to their trunks # trunk = {} # trunk[trunkname][subifname][measure] = value try: with open('/proc/net/vlan/config') as f: f.readline() f.readline() for line in f: (subif, vlanid, trunkif) = line.replace('|', ' ').split() if trunkif not in interface: continue if subif not in interface: continue if not interface_is_enabled(trunkif): continue if trunkif not in trunk: trunk[trunkif] = { 'subifs': [], } trunk[trunkif]['parent'] = interface[trunkif] trunk[trunkif]['subifs'].append(subif) trunk[trunkif][subif] = interface[subif] del interface[subif] except IOError: pass # vlans not configured (or not running as root) # # all remaining interfaces are considered 'plain' # plain = {} # plain[ifname][measure] = value for (ifname, counts) in interface.items(): if ifname in bond or ifname in trunk: continue if not interface_is_enabled(ifname): continue plain[ifname] = counts # # now, do the actual stdout output.. # in_config = (len(sys.argv) > 1 and sys.argv[1] == 'config') def graph_interface_traffic(data): if in_config: print("""graph_title {name} traffic graph_order down up graph_args --base 1000 --lower-limit 0 graph_vlabel bits in (-) / out (+) per ${{graph_period}} graph_category network down.label received down.type DERIVE down.graph no down.cdef down,8,* down.min 0 up.label bps up.type DERIVE up.negative down up.cdef up,8,* up.min 0""".format(**data)) else: print("""down.value {rxbytes} up.value {txbytes}""".format(**data)) print('') def graph_interface_errors(data): if in_config: print("""graph_title {name} errors graph_args --base 1000 --lower-limit 0 graph_vlabel counts RX (-) / TX (+) per ${{graph_period}} graph_category network rxerrs.label errors rxerrs.type COUNTER rxerrs.graph no txerrs.label errors txerrs.type COUNTER txerrs.negative rxerrs rxdrop.label drops rxdrop.type COUNTER rxdrop.graph no txdrop.label drops txdrop.type COUNTER txdrop.negative rxdrop txcolls.label collisions txcolls.type COUNTER""".format(**data)) else: print("""rxerrs.value {rxerrs} txerrs.value {txerrs} rxdrop.value {rxdrop} txdrop.value {txdrop} txcolls.value {txcolls}""".format(**data)) print('') def graph_traffic_with_subifs(ddata, title): if in_config: print('graph_title ' + title) print("""graph_args --base 1000 --lower-limit 0 graph_vlabel bits in (-) / out (+) per ${graph_period} graph_category network""") for ifname in ddata['subifs'] + ['parent',]: data = d[ifname] if ifname == 'parent': label = 'total' drawtype = 'LINE1' else: label = data['name'] drawtype = 'AREASTACK' if in_config: print("""{sname}_down.label {label} {sname}_down.type DERIVE {sname}_down.graph no {sname}_down.cdef {sname}_down,8,* {sname}_down.min 0 {sname}_up.label {label} {sname}_up.type DERIVE {sname}_up.negative {sname}_down {sname}_up.cdef {sname}_up,8,* {sname}_up.draw {drawtype} {sname}_up.min 0""".format(label=label, drawtype=drawtype, **data)) if ifname == 'parent': print('{sname}_up.colour 000000'.format(**data)) else: print("""{sname}_down.value {rxbytes} {sname}_up.value {txbytes}""".format(**data)) print('') for d in plain.values(): print('multigraph interface_{sname}_traffic'.format(**d)) graph_interface_traffic(d) print('multigraph interface_{sname}_errors'.format(**d)) graph_interface_errors(d) for d in bond.values(): parent = d['parent'] print('multigraph bond_{sname}_traffic'.format(**parent)) graph_traffic_with_subifs(d, title='{0} traffic (stacked)'.format(parent['name'])) print('multigraph bond_{sname}_errors'.format(**parent)) graph_interface_errors(parent) for ifname in d['subifs']: if_data = d[ifname] print('multigraph bond_{0}_traffic.{1}'.format(parent['sname'], if_data['sname'])) graph_interface_traffic(if_data) print('multigraph bond_{0}_errors.{1}'.format(parent['sname'], if_data['sname'])) graph_interface_errors(if_data) for d in trunk.values(): parent = d['parent'] print('multigraph trunk_{sname}_traffic'.format(**parent)) graph_traffic_with_subifs(d, title='{0} trunk (stacked)'.format(parent['name'])) for ifname in d['subifs']: if_data = d[ifname] print('multigraph trunk_{0}_traffic.{1}'.format(parent['sname'], if_data['sname'])) graph_interface_traffic(if_data)