#!/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 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..
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:
* implement '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)

