diff --git a/plugins/network/linux_if/README.md b/plugins/network/linux_if/README.md new file mode 100644 index 00000000..d39bbaa0 --- /dev/null +++ b/plugins/network/linux_if/README.md @@ -0,0 +1,22 @@ +linux_if +======== + +Linux network monitoring plugin with support for *bonding* and *vlans*. + +Plugin will group all bonding slaves into one aggregated graph. It removes clutter +of having many single-metric graphs for individual interfaces. You still can click +trough to individual graphs. + +In this example, `p1p1`, `p2p1` are physical interfaces of one bond0. Only p1p1 is active, +as we are using Active-Backup bonding mode. `Total` (black) is the throughput of parent +interface `bond0`. + +![linux_if_bonding text](linux_if_bonding.png "linux_if bonding feature") + +Similar aggregation is done also on higher level for vlans. All vlan sub-interfaces +are aggregated into one graph, based on their parent interface. Interfaces do not even +have to follow the same naming convention (_interface_._vlanid_). Interfaces `bond0.279`, +`vlan101`, `vlan102` are all vlan sub-interfaces of bond0. + +![linux_if_vlans_text](linux_if_vlans.png "linux_if vlan feature") + diff --git a/plugins/network/linux_if/linux_if b/plugins/network/linux_if/linux_if new file mode 100755 index 00000000..b418f675 --- /dev/null +++ b/plugins/network/linux_if/linux_if @@ -0,0 +1,297 @@ +#!/usr/bin/env python + +""" +linux_if - munin plugin monitoring Linux network interfaces + +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: +* bonding - group bonding slave interfaces with master +* vlans - group vlan sub-interfaces with main (dot1q trunk) interface + +plugin configuration: + +[linux_if] + # run plugin as root (required if you have VLAN sub-interfaces) + user = root + + # comma separated list of intreface patterns to exclude from monitoring + # default: lo + # example: + env.exclude = lo,vnet* + + # comma separated list of intreface patterns to include in monitoring + # default: (empty) + # example: + env.include = br_* + + # should staticly configured interfaces be included (they have ifcfg-* file) + # default: true + env.include_configured_if = true + +Include/exclude logic in detail. Interface name is matched.. +1) if matched by any exclude pattern, then exclude. Otherwise next step. +2) if matched by any include pattern, then include, Otherwise next step. +3) if 'include_configured_if' is true and 'ifcfg-*' file exists then include +4) default is not to include interface in monitoring +5) automatically include sub-interface, if the parent interface is monitored + +Tested on: RHEL 6.x and clones (with Python 2.6) + +TODO: +* implment 'data loaning' between graphs, removes duplicit measures +* add support for bridging +* configurable graph max based on intreface speed + +MUNIN MAGIC MARKER +#%# family=manual +""" + +__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) + diff --git a/plugins/network/linux_if/linux_if_bonding.png b/plugins/network/linux_if/linux_if_bonding.png new file mode 100644 index 00000000..36860fe3 Binary files /dev/null and b/plugins/network/linux_if/linux_if_bonding.png differ diff --git a/plugins/network/linux_if/linux_if_vlans.png b/plugins/network/linux_if/linux_if_vlans.png new file mode 100644 index 00000000..9cc1e190 Binary files /dev/null and b/plugins/network/linux_if/linux_if_vlans.png differ