#! /usr/bin/perl -w

# Ce script génère une alarme quand des interfaces sont 
# administrativement ouvertes mais opérationnellement down.

use strict;
use Net::SNMP qw/:debug DEBUG_ALL/;
use Getopt::Std;
use vars qw/$opt_h $opt_c $opt_u $opt_x/;
use Date::Manip;
# use Data::Dumper;

my $debug = 0;
sub usage {
   print <<EOF;

Usage:
$0 -h host -c community
 -h host : hôte cible
 -c community : communauté SNMP (v1/v2c)
 -u user : user SNMP SNMPv3
 -x password : user password SNMP SNMPv3

EOF
   exit(3);
}

getopt("h:c:u:x:");
my $host = $opt_h || usage();
my $community = $opt_c;
my $snmp_user = $opt_u;
my $snmp_password = $opt_x;

my $snmp_version;
if ($snmp_user && $snmp_password) {
	$snmp_version = 3
} else {
	$snmp_version = 1;
	if (! $community) { 
		usage();
	}
}
	

=pod

On veut générer une alarme si une interface est opérationnellement
DOWN alors qu'elle est administrativement UP.

On a quelques cas particuliers :

- port nouvellement provisionné
- port temporaire (pour migration, tests etc)
- port marqué comme DOWN, on ne souhaite pas recevoir d'alarme (client
  parti mais le contrat n'a pas expiré, port en litige)

Cas du port qui vient d'être provisionné :

- dans la description de l'interface, on configure
  la date de provisionnement du client (format "NEW YYYY/MM/DD").

- on ne veut pas générer d'alarme quand l'interface est DOWN
  jusqu'à un mois après la date de provisionnement (attente d'arrivée du client).

- dès que le client est arrivé (l'interface est UP), on génère une alarme WARNING,
  il faut alors retirer la date de provisionnement de la description, et le port
  passe en mode normal.

Cas d'un port temporaire :

- dans la description de l'interface, on configure la date à laquelle
  on peut récupérer le port (format "TEMP YYYY/MM/DD"). Cette date ne peut
  pas être postérieure de plus d'un mois à la date courante (sinon on a le risque
  d'avoir un port configuré indéfiniment comme temporaire, et donc non surveillé).

- après la date de récupération du port, on génère un warning que le port
  soit UP ou DOWN.

Cas d'un port marqué comme DOWN :

- dans la description de l'interface, on configure
  la date jusqu'à laquelle on ne s'occupe pas de l'état du port
  (format "DOWN YYYY/MM/DD").
  Cette date ne peut pas être postérieure de plus d'un mois à la date courante
  (sinon on a le risque d'avoir un port configuré indéfiniment comme DOWN,
  et donc non surveillé).

- après la date de fin, on génère un warning que le port
  soit UP ou DOWN.


On récupère d'abord les infos SNMP sur les interfaces avant de vérifier
l'état des ports :

1) description des interfaces :
      ifDescr.$if_index = "foo"
2) alias des interfaces (correspond à la description d'une interface Cisco) :
      ifAlias.$if_index = "foo"
3) status administratif :
      ifAdminStatus.$if_index = $admin_status
4) status opérationnel :
      ifOperStatus.$if_index = $oper_status

On récupère ensuite les informations suivantes (dans la
description de l'interface pour le moment, dans une base de
données plus tard) :
5) date de provisionnement (date NEW)
5) date de fin pour un port temporaire ou DOWN
      

=cut

my %if_descr;
my %if_alias;
my %if_admin_status;
my %if_oper_status;

my $descr_table_oid = ".1.3.6.1.2.1.2.2.1.2";
my $alias_table_oid = ".1.3.6.1.2.1.31.1.1.1.18";
my $oper_status_table_oid = ".1.3.6.1.2.1.2.2.1.8";
my $admin_status_table_oid = ".1.3.6.1.2.1.2.2.1.7";

my ($session, $error);

if ($snmp_version == 3) {
	($session, $error) =
  	 Net::SNMP->session(-hostname => $host, -version => "3", -username => $snmp_user,
									-authpassword => $snmp_password, -authprotocol => "sha");
} else {
	($session, $error) =
   	Net::SNMP->session(-hostname => $host, -version => "1", -community => $community);
}

if (not defined($session)) {
   print "Impossible d'ouvrir une session SNMP avec $host: $error !\n";
   exit(3); # UNKNOWN
}

# On récupère l'index des interfaces

my $result = $session->get_table(-baseoid => $descr_table_oid);

if (not defined($result)) {
    my $error = $session->error();
    print "Impossible de récupérer la table ifDescr : $error\n";
    exit(3); # UNKNOWN
}

for my $index ( keys(%{$result}) ) {

    my $descr = $result->{$index};
    ($index) = reverse split /\./, $index;
    
    my $admin_status_oid = "$admin_status_table_oid.$index";
    my $oper_status_oid = "$oper_status_table_oid.$index";
    my $alias_oid = "$alias_table_oid.$index";

    my $result = $session->get_request(-varbindlist =>
                [ $admin_status_oid, $oper_status_oid, $alias_oid ]);

    if (not defined($result)) {
        my $error = $session->error();
        print "Impossible de récupérer le status de l'interface " .
                    "$descr : $error\n";
        exit(3); # UNKNOWN
    }
    
    my $admin_status = $result->{$admin_status_oid};
    my $oper_status = $result->{$oper_status_oid};
    my $alias = $result->{$alias_oid};

    # Avant de remplir les hashs, j'élimine les interfaces
    # qui ne doivent pas être vérifiées
    next if $descr =~ /Virtual-Template/;
    next if $descr =~ /Virtual-Access/;
    next if $descr =~ /StackSub/;
    next if $descr =~ /StackPort/;
    next if $descr =~ /^E1 \d+$/;
    next if $alias =~ /^APC$/;
    next if $alias =~ /DRAC/;
    next if $alias =~ /portable/i;
    next if $alias =~ /PC SRI/i;
    next if $alias =~ /backup-router/i;

    $if_admin_status{$index} = $admin_status;
    $if_oper_status{$index} = $oper_status;
    $if_alias{$index} = $alias;
    $if_descr{$index} = $descr;
}
    
if (! keys %if_descr) {
    print "Aucune interface détectée\n";
    exit(3); # UNKNOWN
}

my $warning_message;
my $critical_message;

=pod

On va détecter les interfaces à problème.

On ne se préoccupe que des interfaces qui sont en status *administratif*
UP, les autres ne peuvent jamais générer d'alarme.
Donc quand je dis l'interface est UP, je veux dire opérationnellement
UP à partir de maintenant.

Premier cas : l'interface est UP, on génère un warning
dans ces cas :
- il y a une description "NEW YYYY/MM/DD"
  (le client est arrivé, la description doit être changée)
- il y a une description "TEMP YYYY/MM/DD", et la date temporaire
  est postérieure à la date courante
  (la date de fin d'utilisation temporaire doit être ajustée ou bien
  le port n'est plus à usage temporaire). 
  On vérifie aussi que la date temporaire n'est pas postérieure
  de plus d'un mois à la date courante.
- il y a une description "DOWN YYYY/MM/DD", et la date DOWN ACK
  est postérieure à la date courante
  (la date DOWN ACK être ajustée ou bien le port n'est plus marqué comme DOWN). 
  On vérifie aussi que la date temporaire n'est pas postérieure
  de plus d'un mois à la date courante.


Deuxième cas : l'interface est DOWN, on génère une alarme
*sauf* dans les cas suivants :
- il y a une description "NEW YYYY/MM/DD", avec une date
  courante qui n'est pas postérieure de plus d'un mois à la date
  NEW configurée.
  On vérifie aussi que la date NEW est antérieure à la date courante.
- il y a une description "TEMP YYYY/MM/DD", avec une date courante
  inférieure à la date temporaire configurée.
  On vérifie aussi que la date TEMP n'est pas postérieure
  de plus d'un mois à la date courante.
- il y a une description "DOWN YYYY/MM/DD", avec une date courante
  inférieure à la date DOWN ACK configurée.
  On vérifie aussi que la date DOWN ACK n'est pas postérieure
  de plus d'un mois à la date courante.

=cut


# Fonction auxiliaire qui renvoie une chaîne warning si
# une interface TEMP doit générer un warning (utilisé à la fois
# dans les cas où l'interface est UP ou DOWN, d'où cette fonction pour éviter
# de dupliquer le code).
sub check_temp_interface {

    my ($temporary_end_date, $today_date, $month_after_date) = @_;

    print "In check_temp_interface...\n" if $debug;

    # on génère un warning si l'interface est TEMP et que la date
    # temporaire est dépassée ou que la date temporaire est postérieure
    # de plus d'un mois à la date courante

    my $warning_return;
    my $critical_return;

    if ( Date_Cmp($temporary_end_date, $month_after_date) > 0 ) {
        print "TEMP end date too late (one month max)!\n" if $debug;
        $warning_return = "TEMP end date too late (one month max)!\n";
    }

    elsif ( Date_Cmp($today_date, $temporary_end_date) > 0 ) {
        print "TEMP end date exceedeed!\n" if $debug;
        $warning_return = "TEMP end date exceedeed!\n";
    }

    else {
        print "OK\n" if $debug;
    }

    return ($warning_return, $critical_return);

}
 
# Fonction auxiliaire qui renvoie une chaîne warning si
# une interface marquée DOWN doit générer un warning (utilisé à la fois
# dans les cas où l'interface est UP ou DOWN, d'où cette fonction pour éviter
# de dupliquer le code).
sub check_down_ack_interface {

    my ($down_ack_end_date, $today_date, $month_after_date) = @_;

    print "In check_down_ack_interface...\n" if $debug;

    # on génère un warning si l'interface est marquée DOWN et que la date
    # temporaire est dépassée ou que la date temporaire est postérieure
    # de plus d'un mois à la date courante

    my $warning_return;
    my $critical_return;

    if ( Date_Cmp($down_ack_end_date, $month_after_date) > 0 ) {
        print "DOWN end date too late (one month max)!\n" if $debug;
        $warning_return = "DOWN end date too late (one month max)!\n";
    }

    elsif ( Date_Cmp($today_date, $down_ack_end_date) > 0 ) {
        print "DOWN end date exceedeed!\n" if $debug;
        $warning_return = "DOWN end date exceedeed!\n";
    }

    else {
        print "OK\n" if $debug;
    }

    return ($warning_return, $critical_return);

}
             
# Fonction qui permet de récupérer la date à laquelle on peut récupérer
# un port temporaire. 
sub get_temp_end_date {

    my ($alias) = @_;

    print "get_temp_end_date: " if $debug;

    if ( my ($date) = ($alias =~ m|TEMP \s+ (\d+/\d+/\d+)|x) ) { 

        my $temp_end_date = ParseDate($date);

        if ($debug) {
            print UnixDate($temp_end_date, "temporary end date is %b %e, %Y\n");
        }

        return $temp_end_date;
    }

    else {
        print "no TEMP date.\n" if $debug;
        return undef;
    }

}
              
# Fonction qui permet de récupérer la date d'expiration d'une interface
# marquée DOWN ACK
sub get_down_ack_end_date {

    my ($alias) = @_;

    print "get_down_ack_end_date: " if $debug;

    if ( my ($date) = ($alias =~ m|DOWN \s+ (\d+/\d+/\d+)|x) ) { 

        my $down_ack_end_date = ParseDate($date);

        if ($debug) {
            print UnixDate($down_ack_end_date, "DOWN ACK end date is %b %e, %Y\n");
        }

        return $down_ack_end_date;
    
    }

    else {
        print "no DOWN ACK date.\n" if $debug;
        return undef; # pas de date DOWN ACK
    }

}
             
# Fonction qui permet de récupérer la date à laquelle un port
# a été provisionné.
sub get_new_date {

    my ($alias) = @_;

    print "get_new_date: " if $debug;

    if ( my ($new_date) = ($alias =~ m|NEW \s+ (\d+/\d+/\d+)|x) ) { 

        if ($debug) {
            print UnixDate($new_date, "NEW date is %b %e, %Y\n");
        }

        return $new_date;
    }

    else {
        print "no NEW date.\n" if $debug;
        return undef;
    }

}


my @if_index = grep { $if_admin_status{$_} == 1 } keys(%if_descr);

my $today_date = ParseDate("midnight today");
my $month_after_date = DateCalc("today", "+1 month");
my $month_ago_date = DateCalc("today", "-1 month");

if ($debug) {
    print UnixDate($today_date, "Today's date is %b %e, %Y\n");
    print UnixDate($month_ago_date, "Month ago date is %b %e, %Y\n");
    print UnixDate($month_after_date, "One month later date is %b %e, %Y\n");
}
 
for my $index (@if_index) {

    my $alias = $if_alias{$index};
    my $descr = $if_descr{$index};
    my $oper_status = $if_oper_status{$index};

    print "\nLooking at interface (alias = \"$alias\", descr = \"$descr\")...\n"
        if $debug;

    # undef si les dates ne sont pas disponibles
    my $temp_end_date = get_temp_end_date($alias);
    my $new_date = get_new_date($alias);
    my $down_ack_end_date = get_down_ack_end_date($alias);

    my $is_temp_interface = defined $temp_end_date?1:0;
    my $is_new_interface = defined $new_date?1:0;
    my $is_down_ack_interface = defined $down_ack_end_date?1:0;

    # Si pas de description, utiliser le nom de l'interface
    # pour les messages
    my $label = $alias?$alias:$descr;


    # L'interface est UP
    if ( $oper_status == 1 ) {

        print "Interface is operationaly UP.\n" if $debug;

        # on génère un warning si l'interface est NEW
        if ($is_new_interface) { 
            print "Interface is NEW\n" if $debug;
            $warning_message .= "$alias: UP, remove NEW description!\n";
        }

        # on génère un warning si l'interface est TEMP et que la date
        # temporaire est dépassée ou postérieure de plus d'un mois à la
        # date courante

        elsif ($is_temp_interface) {
            my ($warning) = check_temp_interface($temp_end_date,
                                        $today_date, $month_after_date);
            $warning_message .= "$label: $warning" if $warning;
        }

        # on génère un warning si l'interface est DOWN ACK et que la date
        # DOWN ACK est dépassée ou postérieure de plus d'un mois à la date
        # courante

        elsif ($is_down_ack_interface) {
            my ($warning) = check_down_ack_interface($down_ack_end_date,
                                        $today_date, $month_after_date);
            $warning_message .= "$label: $warning" if $warning;
        }

    } # END interface is UP

    # L'interface n'est pas UP
    else {
        
        print "Interface is *not* operationaly UP.\n" if $debug;
    
        # L'interface est DOWN mais avec un label NEW
        if ($is_new_interface) { 
            
            if ( Date_Cmp($new_date, $today_date) > 0 ) {
                print "NEW date in the future!\n" if $debug;
                $warning_message .= "$label: NEW date in the future!\n";
            }

            elsif ( Date_Cmp($new_date, $month_ago_date) < 0 ) {
                print "NEW date too old!\n" if $debug;
                $warning_message .= "$label: NEW date too old!\n";
            }
                
        } 

        # L'interface est DOWN mais avec un label TEMP
        elsif ($is_temp_interface) { 
                my ($warning) = check_temp_interface($temp_end_date,
                                        $today_date, $month_after_date);
                $warning_message .= "$label: $warning" if $warning;
        }

        # L'interface est DOWN mais avec un label DOWN ACK
        elsif ($is_down_ack_interface) { 
                my ($warning) = check_down_ack_interface($down_ack_end_date,
                                        $today_date, $month_after_date);
                $warning_message .= "$label: $warning" if $warning;
        }


        # L'interface est DOWN, pas de circonstances atténuantes
        else {

            $critical_message .= "$label DOWN\n";

        }

    } # END interface is not UP

} # END admin. UP interface loop
        
        
if ($critical_message) {
    print "CRITICAL: $critical_message";
    exit(2); # CRITICAL
}

elsif ($warning_message) {
    print "WARNING: $warning_message";
    exit(1); # WARNING
}
    
else {
    print "OK: ", scalar @if_index, " interfaces.\n";
}

