Repository
Munin (contrib)
Last change
2020-10-24
Graph Categories
Family
manual
Capabilities
Keywords
Language
Python (2.x)

linux_if

Example graph: 1 Example graph: 2

Name

linux_if - munin plugin monitoring Linux network interfaces

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:

  • bonding - group bonding slave interfaces with master
  • vlans - group vlan sub-interfaces with main (dot1q trunk) interface

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:

    1. if matched by any exclude pattern, then exclude. Otherwise next step.
    1. if matched by any include pattern, then include, Otherwise next step.
    1. if ‘include_configured_if’ is true and ‘ifcfg-*’ file exists then include
    1. default is not to include interface in monitoring
    1. 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 interface speed

Magic Markers

#%# family=manual
#!/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)