#!/usr/bin/perl -w
# -*- cperl -*-

=head1 NAME

snmp__if_multi - SNMP card plugin to monitor the network interfaces of any networked equipment.

=head1 APPLICABLE SYSTEMS

Any SNMP capable networked computer equipment.  Using a command such
as "munin-node-configure --snmp switch.langfeldt.net --snmpversion 2c
--snmpcommunity public | sh -x" should auto-detect all applicable
interfaces.  On a typical switch you will get one plugin pr. ethernet
port.  On a router you might get one plugin pr. VLAN interface.

=head1 CONFIGURATION

As a rule SNMP plugins need site specific configuration.  The default
configuration (shown here) will only work on insecure sites/devices:

   [snmp_*]
	env.version 2
        env.community public

In general SNMP is not very secure at all unless you use SNMP version
3 which supports authentication and privacy (encryption).  But in any
case the community string for your devices should not be "public".

Please see 'perldoc Munin::Plugin::SNMP' for further configuration
information.

=head1 INTERPRETATION

The graph shows a stright forward "bits per second" incomming and
outgoing thruput.  "Incomming" is towards the monitored device.

Note: The internal representation of the speeds is in bytes
pr. second.  The plugin multiplies everyting by 8 to get bits
pr. second.

=head1 MIB INFORMATION

This plugin requires the IF-MIB the standard IETF MIB for network
interfaces.

It reports the contents of the
IF-MIB::ifHCInOctets/IF-MIB::ifHCOutOctets if available,
IF-MIB::ifInOctets/IF-MIB::ifOutOctets if not.  The former are 64 bit
counters only available with SNMP 2 and later.  The later are 32 bit
counters (see FEATURES below).

For errors it reports ifInErrors + ifINDiscards and ifOutErrors +
ifOutDiscards.

=head1 MAGIC MARKERS

  #%# family=snmpauto
  #%# capabilities=snmpconf

=head1 VERSION

  $Id: snmp__if_.in 1818 2009-01-03 19:29:30Z janl $

=head1 BUGS

Should support indexing by

  - ifIndex
  - ifName
  - ifDescr
  - ifAlias
  - mac-address

(and anything else MRTG can index by) in addition to OID-index as now.

Pulling in a user definable set of ifName/ifDescr/ifAlias for textual
description and even graph_title would also be nice.

IFF we get a patch to support the .oldname attribute then we may use
that to let the operator change the indexing dynamicaly without data
loss.

=head1 FEATURES

You may get strange results if you use SNMPv1, or SNMPv2 on
switches that do not support 64 bit byte counters.  If the interface
traffic exceeds about 50Mbps a 32 bit byte counter will wrap around in
less than 5 minutes making the graph for the interface show random
results.

If you have a switch/device that supports 64 bit byte counters this plugin
will use them and the graph will be fine.  The graph information will
inform about this.  You must use SNMPv2c or SNMPv3 to be able to use
64 bit counters - if the device supports them.

This problem is a feature of the device SNMP implementation or your
usage of it, it is nothing the plugin can fix.  In the future Munin
may be able to run the plugin more often than the counter wraps
around.

=head1 AUTHOR

Copyright (C) 2004-2009 Jimmy Olsen, Dagfinn Ilmari Mannsaaker,
Nicolai Langfeldt, Redpill Linpro AS and others.

Original snmp__if_ plugin: Copyright (C) 2004-2009 Jimmy Olsen, Dagfinn
Ilmari Mannsaaker.

Initial SNMPv3 support by "Confusedhacker".

Documentation, porting to Munin::Plugin::SNMP and
further grooming by Nicolai Langfeldt.

Reworked to snmp__if_multi by Nicolai Langfeldt.

=head1 LICENSE

GPLv2

=cut

use strict;
use Munin::Plugin;
use Munin::Plugin::SNMP;

need_multigraph();

my $DEBUG = $ENV{'MUNIN_DEBUG'};

my $response;
my $iface;

# This is the snmpwalk:
# .1.3.6.1.2.1.2.1.0 = INTEGER: 2
# .1.3.6.1.2.1.2.2.1.1.1 = INTEGER: 1
# .1.3.6.1.2.1.2.2.1.1.65539 = INTEGER: 65539
# .1.3.6.1.2.1.2.2.1.2.1 = STRING: MS TCP Loopback interface
# .1.3.6.1.2.1.2.2.1.2.65539 = STRING: Broadcom NetXtreme Gigabit Ethernet
# .1.3.6.1.2.1.2.2.1.3.1 = INTEGER: softwareLoopback(24)
# .1.3.6.1.2.1.2.2.1.3.65539 = INTEGER: ethernetCsmacd(6)
# .1.3.6.1.2.1.2.2.1.4.1 = INTEGER: 1520
# .1.3.6.1.2.1.2.2.1.4.65539 = INTEGER: 1500
# .1.3.6.1.2.1.2.2.1.5.1 = Gauge32: 10000000
# .1.3.6.1.2.1.2.2.1.5.65539 = Gauge32: 1000000000
# .1.3.6.1.2.1.2.2.1.6.1 = STRING:
# .1.3.6.1.2.1.2.2.1.6.65539 = STRING: 0:30:48:75:65:5e
# .1.3.6.1.2.1.2.2.1.7.1 = INTEGER: up(1)
# .1.3.6.1.2.1.2.2.1.7.65539 = INTEGER: up(1)
# .1.3.6.1.2.1.2.2.1.8.1 = INTEGER: up(1)
# .1.3.6.1.2.1.2.2.1.8.65539 = INTEGER: up(1)
#
# 64 bit counters:
# .1.3.6.1.2.1.31.1.1.1.6.   Counter64 ifHCInOctets
# .1.3.6.1.2.1.31.1.1.1.10.  Counter64 ifHCOutOctets

if (defined $ARGV[0] and $ARGV[0] eq "snmpconf") {
	print "number  1.3.6.1.2.1.2.1.0\n";
	print "index   1.3.6.1.2.1.2.2.1.1.\n";
	print "require 1.3.6.1.2.1.2.2.1.3. ^(6|23)\$\n"; # Type
	print "require 1.3.6.1.2.1.2.2.1.5. [1-9]\n"; # Speed
	exit 0;
}

my $sysDescr           = '1.3.6.1.2.1.1.1.0';
my $sysLocation        = '1.3.6.1.2.1.1.6.0';
my $sysContact         = '1.3.6.1.2.1.1.4.0';
my $sysName            = '1.3.6.1.2.1.1.5.0';

my $ifOIDBase          = "1.3.6.1.2.1.2.2.1";		# ifEntry
my $ifv2OIDBase        = "1.3.6.1.2.1.31.1.1.1";	# ifXEntry

my ($session, $error);

# SNMP needed for both config and fetch.
$session = Munin::Plugin::SNMP->session();

my $snmpinfo = $session->get_hash(
				  -baseoid => $ifOIDBase,
				  -cols    =>
				  { 2  => 'ifDescr',
				    # Type: 6) ethernetCsmacd 23) ppp
				    # 32) frameRelay(DTE) 37) adm
				    # 44) FrameRelayService 49) aal5
				    # 53) propVirtual
				    # 56) fibreChannel 94) adsl 95) radsl
				    # 96) sdsl 97) vdsl 105) atmDxi
				    # 106) atmFuni 107) atmIma
				    # 108) pppMultilinkBundle
				    # 114) ipOverAtm 131) tunnel
				    # 125) fast - depreciated
				    # 149) atmVirtual 150) mplsTunnel
				    # 160) usb 166) mpls 168) hdsl2
				    # 169) shdsl 199) infiniband 230) adsl2
				    3  => 'ifType',
				    4  => 'ifMtu',
				    5  => 'ifSpeed',
				    6  => 'ifMac',
				    7  => 'ifAdminStatus',
				    # Oper: 1) up 2) down 3) testing
				    # 4) unknown, 5) dormant 6) not present
				    # 7) lowerLayerDown
				    8  => 'ifOperStatus',
				    10 => 'ifInOctets',
				    11 => 'ifInUcastPkts',
				    13 => 'ifInDiscards',
				    14 => 'ifInErrors',
				    15 => 'ifUnUnknownProtos',
				    16 => 'ifOutOctets',
				    18 => 'ifOutNUcastPkts',
				    19 => 'ifOutDiscards',
				    20 => 'ifOutErrors',
				  });


my $snmpinfoX = $session->get_hash( # ifXEntry - SNMP v2 and up only
				    -baseoid => $ifv2OIDBase,
				    -cols    =>
				    { 2  => 'ifInMulticastPkts',
				      3  => 'ifInBroadcastPkts',
				      4  => 'ifOutMulticastPkts',
				      5  => 'ifOutBroadcastPkts',
				      6  => 'ifHCInOctets',
				      10 => 'ifHCOutOctets',
				      18 => 'ifAlias',
				    });


sub do_config_root {
    my ($host) = @_;

    print "multigraph if_bytes\n";
    print "graph_title $host interface traffic\n";
    print "graph_order recv send\n";
    print "graph_args --base 1000\n";
    print "graph_vlabel bits in (-) / out (+) per \${graph_period}\n";
    print "graph_category network\n";
    print "graph_info This graph shows the total traffic for $host\n";

    print "send.info Bits sent/received by $host\n";
    print "recv.label recv\n";
    print "recv.type DERIVE\n";
    print "recv.graph no\n";
    print "recv.cdef recv,8,*\n";
    print "recv.min 0\n";
    print "send.label bps\n";
    print "send.type DERIVE\n";
    print "send.negative recv\n";
    print "send.cdef send,8,*\n";
    print "send.min 0\n";

    print "multigraph if_errors\n";
    print "graph_title $host interface errors\n";
    print "graph_order recv send\n";
    print "graph_args --base 1000\n";
    print "graph_vlabel errors in (-) / out (+) per \${graph_period}\n";
    print "graph_category network\n";
    print "graph_info This graph shows the total errors for $host\n";

    print "send.info Errors in outgoing/incomming traffic on $host\n";
    print "recv.label recv\n";
    print "recv.type DERIVE\n";
    print "recv.graph no\n";
    print "recv.cdef recv,8,*\n";
    print "recv.min 0\n";
    print "send.label bps\n";
    print "send.type DERIVE\n";
    print "send.negative recv\n";
    print "send.cdef send,8,*\n";
    print "send.min 0\n";
}


sub do_config_if {
    my ($host,$version,$if) = @_;

    my $alias = $snmpinfo->{$if}->{ifDescr} || "Interface $if";

    if (! ($alias =~ /\d+/) ) {
	# If there are no numbers in the $alias add the if index
	$alias .=" (if $if)";
    }

    my $extrainfo = '';

    if (defined ($response = $snmpinfo->{$if}->{ifOperStatus})) {
	if ($response == 2) {
	    # Interface is down
	    $extrainfo .= ' The interface is currently down.'
	} elsif ($response != 1) {
	    # This interface is fishy
	    delete $snmpinfo->{$if} if exists $snmpinfo->{$if};
	    delete $snmpinfoX->{$if} if exists $snmpinfoX->{$if};
	    return;
	}
    }

    my $warn = undef;
    my $speed = undef;

    if (defined ($speed = $snmpinfo->{$if}->{ifSpeed})) {
	$warn = $speed*100/75;

	my $textspeed = scaleNumber($speed,,'bps','',
				    'The interface speed is %.1f%s%s.');

	$extrainfo .= " ".$textspeed if $textspeed;
    }

    if (defined ($snmpinfoX->{$if}->{ifHCInOctets})) {
	# If we get an answer at the 64 bit OID then this switch
	# supports the extended MIB

	$extrainfo .= " This switch supports 64 bit byte counters and these are used by this plugin.";
    } else {
	# If not we only have a 32 bit counter and are lost.
	$extrainfo .= " NOTE! This switch supports only 32 bit byte counters which makes the plugin unreliable and unsuitable for most 100Mb (or faster) interfaces, where bursts are expected to exceed 50Mbps.  This means that for interfaces where much traffic is sent this plugin will report false thruputs and cannot be trusted.";

	# unless perhaps the operator can get us snmp version 2c or 3?
	$extrainfo .= " I notice that you use SNMP version 1 which does not support 64 bit quantities.  You may get better results if you switch to SNMP version 2c or 3.  Please refer to the plugin documentation."
	  if $version == 1;
    }

    print "multigraph if_bytes.if_$if\n";

    print "graph_title Interface $alias traffic\n";
    print "graph_order recv send\n";
    print "graph_args --base 1000\n";
    print "graph_vlabel bits in (-) / out (+) per \${graph_period}\n";
    print "graph_category network\n";
    print "graph_info This graph shows traffic for the \"$alias\" network interface.$extrainfo\n";
    print "send.info Bits sent/received by this interface.\n";
    print "recv.label recv\n";
    print "recv.type DERIVE\n";
    print "recv.graph no\n";
    print "recv.cdef recv,8,*\n";
    print "recv.max $speed\n" if $speed;
    print "recv.min 0\n";
    print "recv.warning ", (-$warn), "\n" if defined $warn;
    print "send.label bps\n";
    print "send.type DERIVE\n";
    print "send.negative recv\n";
    print "send.cdef send,8,*\n";
    print "send.max $speed\n" if $speed;
    print "send.min 0\n";
    print "send.warning $warn\n" if defined $warn;


    print "multigraph if_errors.if_$if\n";

    print "graph_title Interface $alias errors\n";
    print "graph_order recv send\n";
    print "graph_args --base 1000\n";
    print "graph_vlabel bits in (-) / out (+) per \${graph_period}\n";
    print "graph_category network\n";
    print "graph_info This graph shows errors for the \"$alias\" network interface.$extrainfo\n";
    print "send.info Errors in outgoing/incomming traffic on this interface.\n";
    print "recv.label recv\n";
    print "recv.type DERIVE\n";
    print "recv.graph no\n";
    print "recv.cdef recv,8,*\n";
    print "recv.max $speed\n" if $speed;
    print "recv.min 0\n";
    print "recv.warning 1\n";
    print "send.label bps\n";
    print "send.type DERIVE\n";
    print "send.negative recv\n";
    print "send.cdef send,8,*\n";
    print "send.max $speed\n" if $speed;
    print "send.min 0\n";
    print "send.warning 1\n";
}


sub do_fetch_root {
    # Construct root graphs fro the if_bytes and if_error spaces

    my $in = 0;
    my $out = 0;
    my $inerr = 0;
    my $outerr = 0;

    foreach my $if (keys %{$snmpinfo}) {
	# Do not take interface up or down-ness into account, the
	# interface counters are not reset when the interface goes up
	# so we must always account for all interfaces.

	# But note that do_fetch_if has previously removed all
	# interfaces with a "fishy" status.  That is a possible bug.
	# Or a feature.  We'll see.

	$in += $response = $snmpinfo->{$if}->{ifHCInOctets} ||
	  $snmpinfo->{$if}->{ifInOctets};

	$out += $snmpinfo->{$if}->{ifHCOutOctets} ||
	  $snmpinfo->{$if}->{ifOutOctets};

	$inerr += ( $snmpinfo->{$if}->{ifInErrors} || 0 ) +
	  ( $snmpinfo->{$if}->{ifInDiscards} || 0 );

	$outerr += ( $snmpinfo->{$if}->{ifOutErrors} || 0 ) +
	  ( $snmpinfo->{$if}->{ifOutDiscards} || 0 );

    }

    print "multigraph if_bytes\n";
    print "recv.value $in\n";
    print "send.value $out\n";

    print "multigraph if_errors\n";
    print "recv.value $inerr\n";
    print "send.value $outerr\n";
}


sub do_fetch_if {
    my($if) = @_;

    my $status = $snmpinfo->{$if}->{ifOperStatus} || 1;

    my $response;

    # 1 means up
    # 2 means set to down
    # Everything else we ignore.

    if ($status < 1 or $status > 2) {
	delete $snmpinfo->{$if} if exists $snmpinfo->{$if};
	delete $snmpinfoX->{$if} if exists $snmpinfoX->{$if};
	return;
    }

    print "multigraph if_bytes.if_$if\n";

    if ($status == 2) {
	# Interface is down
	print "recv.value U\n";
	print "send.value U\n";
	print "send.extinfo This interface is down\n";
	goto if_errors;
    }

    if (defined ($response = $snmpinfo->{$if}->{ifHCInOctets} ||
		 $snmpinfo->{$if}->{ifInOctets})) {
	print "recv.value ", $response, "\n";
    } else {
	# No response...
	print "recv.value U\n";
   }

    if (defined ($response = $snmpinfo->{$if}->{ifHCOutOctets} ||
		 $snmpinfo->{$if}->{ifOutOctets})) {
	print "send.value ", $response, "\n";
    } else {
	# No response...
	print "send.value U\n";
    }

  if_errors:

    print "multigraph if_errors.if_$if\n";

    if ($status == 2) {
	print "recv.value U\n";
	print "send.value U\n";
	print "send.extinfo This interface is down\n";
	return;
    }

    $response = ( $snmpinfo->{$if}->{ifInErrors} || 0 ) +
      ( $snmpinfo->{$if}->{ifInDiscards} || 0 );

    print "recv.value $response\n";

    $response = ( $snmpinfo->{$if}->{ifOutErrors} || 0 ) +
      ( $snmpinfo->{$if}->{ifOutDiscards} || 0 );

    print "send.value $response\n";
}


sub do_config {

    my ($host,undef,$version) = Munin::Plugin::SNMP->config_session();

    print "host_name $host\n" unless $host eq 'localhost';

    # Also do overview graph with all ports summarized
    foreach my $if (sort {$a <=> $b} keys %{$snmpinfo}) {
	do_config_if($host,$version,$if);
    }

    do_config_root($host);
}


sub do_fetch {
    # Also do overview graph with all ports summarized and collected
    # meta information in graph_info.
    # Add to this error graphs and so on.  And meta information about
    # interface type.

    foreach my $if (sort {$a <=> $b} keys %{$snmpinfo}) {
	do_fetch_if($if);
    }
    do_fetch_root();
}


if ($ARGV[0] and $ARGV[0] eq "config") {
    do_config();
    exit 0;
}

do_fetch();
