#! %PERL%
#
# $Id: update-dbs.pl,v 1.25 2016/11/08 19:23:34 he Exp $
#
# Copyright (c) 1996, 1997
#      UNINETT and NORDUnet.  All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. All advertising materials mentioning features or use of this software
#    must display the following acknowledgement:
#      This product includes software developed by UNINETT and NORDUnet.
# 4. Neither the name of UNINETT or NORDUnet nor the names
#    of its contributors may be used to endorse or promote
#    products derived from this software without specific prior
#    written permission.
#
# THIS SOFTWARE IS PROVIDED BY UNINETT AND NORDUnet ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL UNINETT OR NORDUnet OR
# THEIR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

# Input: -t topdir
#	points to top directory.  Implicitly uses
#	topdir/conf/polldevs.cf,
#	topdir/db/ntf, topdir/db/ftn hashed files and
#	topdir/db/manual-devs for list of devices where mapping is
#	manually maintained
# Output: updated ntf and ftn hashed files
#
# Check config database for possible updates, do updates
# if required.
#
# Relies on certain conventions for "interface description" field
# on Cisco devices; briefly "bla bla, logical-port-name, bla bla"
# where the ", " is the field delimiter (note: two chars!).

use strict;
use BerkeleyDB;
use Getopt::Std;


sub open_db {
    my($db, $rh) = @_;
    my($dbf) = $db . ".bdb";

    if (-f $dbf) {	# has converted to Berkeley DB
	tie(%{ $rh }, 'BerkeleyDB::Hash', -Filename => $dbf)
	    or die "Cannot open $dbf: $! $BerkeleyDB::Error\n";
    } else {
	dbmopen(%{ $rh }, $db, 0666);
    }
}

sub close_db {
    my($rh) = @_;

    untie(%{ $rh });
}


sub open_dbs {
    my($dbdir) = @_;
    our(%ftn, %ntf);

    open_db("$dbdir/ftn", \%ftn);
    open_db("$dbdir/ntf", \%ntf);
}

sub close_dbs {
    our(%ftn, %ntf);

    close_db(\%ftn);
    close_db(\%ntf);
}

sub read_config {
    my($f) = @_;
    my($dev, $poll_seen, $def_community);
    our(%community, %polladdr, %device);

    open(IN, $f) || die "Could not open $f: $!\n";
    while(<IN>) {
	chomp;
	if (/^\#/) { next; }
	if (/\s+$/) {
	    printf (STDERR "Extraneous trailing whitespace: %s\n", $_);
	    $_ =~ s/\s+$//;
	}
	if (/^default community:\s(\S+)$/) {
	    $def_community = $1;
	    next;
	}
	if (/^default /) { next; }
	if (! /^name:\s+(\S+)$/) { next; };
	$dev = $1;
	$poll_seen = 0;
	while (!/^$/) {
	    $_ = <IN>;
	    chomp;
	    if (/^address:\s+([0-9].+)$/) {
		$polladdr{$dev} = $1;
		$device{$1} = $dev;
		$poll_seen = 1;
	    }
	    if (/^community:\s+(\S+)$/) {
		$community{$dev} = $1;
	    }
	}
        if (!$poll_seen) {
	    printf (STDERR "Poll address for $dev not found!\n");
	}
	if (!defined($community{$dev})) {
	    $community{$dev} = $def_community;
	}
    }
    close(IN);
}

sub read_manuals {
    my($fh) = @_;
    our(%manual);

    while (<$fh>) {
	if (/^;/) { next; }
	chop;
	$manual{$_} = 1;
    }
}

sub alive {
    my($address) = @_;
    our($opt_n, %device);

    if (defined($opt_n)) { return 1; }
    # XXX Name of command should go in a config file
    open(IN, "%PING% -s 56 -c 5 -i 2 $address 2>&1 |");
    while(<IN>) {
	if (! /packet loss/) { next; }
	if (/100% packet loss/) {
	    printf(STDERR "No ping response from %s\n", $device{$address});
	    close(IN); return undef;
	}
    }
    close(IN);
    return 1;
}

sub get_addrs {
    my($dev, $polladdr) = @_;
    our(%dead, %address, %as, %ix_map, %community);

    if ($dead{$polladdr} || ! &alive($polladdr)) {
	$dead{$polladdr} = 1;
	return undef;
    }
    # XXX Name of command should go in a config file
    open(IN, "%SNMPNETSTAT% -inh -t 30 -y $community{$dev} $polladdr 2>&1|");
    while(<IN>) {
	if (/noResponse/) {
	    printf(STDERR "noResponse from %s\n", $dev);
	    close(IN);
	    $dead{$polladdr} = 1;
	    return undef;
	}
	chop;
	@_ = split;
	$as{$dev . ":" . $_[0]} = $_[2];
	$address{$dev . ":" . $_[0]} = $_[4];
	if (!defined($ix_map{$dev}->{$_[0]})) {
	    $ix_map{$dev}->{$_[0]} = 1;
	}
    }
    close(IN);
    return 1;
}

sub get_line_names {
    my($dev, $polladdr) = @_;
    my(@a);
    our(%dead, %line_name, %ix_map, %community);

    if ($dead{$polladdr} || ! &alive($polladdr)) {
	$dead{$polladdr} = 1;
	return undef;
    }
    open(IN, "%SNMPNETSTAT% -idnh -t 30 -y $community{$dev} $polladdr 2>&1|");
    while(<IN>) {
	if (/noResponse/) {
	    printf(STDERR "noResponse from %s\n", $dev);
	    close(IN);
	    $dead{$polladdr} = 1;
	    return undef;
	}
	if (/^(\d+)\s+\S+\s+\S+\s+"(.*)"$/) {
	    my($ix) = $1;
	    @a = split(/,[ _]/,$2);
	    if ($#a < 1) { next; }
	    # Trim leading and trailing spaces,
	    $a[1] =~ s/^ +//;
	    $a[1] =~ s/ +$//;
	    # replace remaining spaces with hyphen.
	    # ...so that later use of default "split"
	    # doesn't produce mismatch, new entry and
	    # eventual overflow for the DB(M) entry (!).
	    $a[1] =~ s/ /-/g;
	    # replace any slashes with hyphens, so that
	    # the resulting name can be used as a file name.
	    $a[1] =~ s/\//-/g;

	    $line_name{$dev . ":" . $ix} = $a[1];
	    if (!defined($ix_map{$dev}->{$ix})) {
		$ix_map{$dev}->{$ix} = 1;
	    }
	}
    }
    close(IN);
    return 1;
}

sub get_current_state {
    my($k);
    my($fn);
    our($out_d, %polladdr, %manual, %ix_map, %as, %address);
    our(%file, %line, %line_name, $opt_d);

    if (defined($opt_d)) {
	printf(STDERR "Getting current state\n");
    }
    foreach my $r (keys %polladdr) {
	if (defined($manual{$r})) {
	    if (defined($opt_d)) {
		printf(STDERR "Router %s - manual mapping\n", $r);
	    }
	    next;
	}
	if (defined($opt_d)) {
	    printf(STDERR "Checking %s for addresses...", $r);
	}
	&get_addrs ($r, $polladdr{$r});
	if (defined($opt_d)) {
	    printf(STDERR "interface descs...");
	}
	&get_line_names ($r, $polladdr{$r});
	if (defined($opt_d)) {
	    printf(STDERR "\n");
	}

	for my $i (sort { $a <=> $b } keys(%{$ix_map{$r}})) {
	    $k = $r . ":" . $i;
	    if (!defined($address{$k})) {
		next;		# skip holes in ifIndex space
	    }
	    if ($as{$k} eq "off") {
		next;		# skip administratively down ifs.
	    }
	    if (!defined($line_name{$k})) {
		next;		# skip "unnamed" lines
	    }

	    if ($address{$k} eq "none") {
		$address{$k} = "#" . $i;
	    }
	    $fn = $r . "." . $address{$k};
	    # Check if we should override existing mapping
	    if (defined($file{$line_name{$k}})) {
		# Only override if previous value didn't have an address
		# ...and current one has
		if ($file{$line_name{$k}} =~ /\#/ && $fn !~ /\#/) {
		    $file{$line_name{$k}} = $fn;
		}
	    } else {
		$file{$line_name{$k}} = $fn;
	    }
	    $line{$fn} = $line_name{$k};
	}
    }
}

sub fn_to_rn {
    my($fn) = @_;

    if ($fn =~ /(\S+)\.\d+\.\d+\.\d+\.\d+$/) {
	return $1;
    }
    if ($fn =~ /(\S+)\.\#\d+$/) {
	return $1;
    }
    return undef;
}

sub closed_entry {
    my($e) = @_;

    if ($e =~ /-$/) {
	return undef;
    }
    return 1;
}

sub update_dbs {
    my($now);
    my(@a, @b);
    my($fn, $ln, $r);
    our(%file, %line);
    our(%ftn, %ntf);
    our($opt_d, $opt_v, $opt_D);
    our(%polladdr, %dead);
    
    $now = &get_today_str();

    if (defined($opt_d)) {
	printf(STDERR "\nChecking name -> file DB\n\n");
    }
    foreach $ln (keys %file) {
	if (!defined $file{$ln}) { next; }
	$r = &fn_to_rn($file{$ln});
	if (defined $ntf{$ln}) {
	    # separate entries in name-to-file database
	    @a = split(/:/, $ntf{$ln});
	    # split last entry on spaces
	    @b = split(/ /, $a[$#a]);
	    if ($b[0] eq $file{$ln} && !&closed_entry($ntf{$ln})) {
		# same as before
		if (defined($opt_D) && ($opt_D eq $r)) {
		    printf(STDERR "%s: entry for %s as before: %s\n",
			   $r, $ln, $b[0]);
		}
		next;
	    }
	    # Tack on new entry at the end
	    if (!&closed_entry($ntf{$ln})) {
		$ntf{$ln} .= $now;
	    }
	    $ntf{$ln} .= ":" . $file{$ln} . " " . $now . "-";
	    if (defined($opt_v) || defined($opt_d)) {
		printf(STDERR "Updated entry: %s -> %s\n",
		       $ln, $file{$ln});
	    }
	} else {
	    # Add completely new entry
	    $ntf{$ln} = $file{$ln} . " " . $now . "-";
	    if (defined($opt_v) || defined($opt_d)) {
		printf(STDERR "Added new entry: %s -> %s\n",
		       $ln, $file{$ln});
	    }
	}
    }
    if (defined($opt_d)) {
	printf(STDERR "\nChecking file -> name DB\n\n");
    }
    foreach $fn (keys %line) {
	if (!defined $line{$fn}) { next; }
	$r = &fn_to_rn($fn);
	if (defined $ftn{$fn}) {
	    # separate entries in file-to-name database
	    @a = split(/:/, $ftn{$fn});
	    # split last entry on spaces
	    @b = split(/ /, $a[$#a]);
	    if ($b[0] eq $line{$fn} && !&closed_entry($ftn{$fn})) {
		# same as before
		if (defined($opt_D) && ($opt_D eq $r)) {
		    printf(STDERR "%s: entry for %s as before: %s\n",
			   $r, $fn, $b[0]);
		}
		next;
	    }
	    # Tack on new entry at the end
	    if (!&closed_entry($ftn{$fn})) {
		$ftn{$fn} .= $now;
	    }
	    $ftn{$fn} .= ":" . $line{$fn} . " " . $now . "-";

	    if (defined($opt_v) || defined($opt_d)) {
		printf(STDERR "Updated entry: %s -> %s\n",
		       $fn, $line{$fn});
	    }
	} else {
	    # Add completely new entry
	    $ftn{$fn} = $line{$fn} . " " . $now . "-";

	    if (defined($opt_v) || defined($opt_d)) {
		printf(STDERR "Added new entry: %s -> %s\n",
		       $fn, $line{$fn});
	    }
	}
    }
    # Check if entries need to be closed
    if (defined($opt_v) || defined($opt_d)) {
	printf(STDERR "\nChecking for closeable file -> name entries\n\n");
    }
    foreach $fn (keys %ftn) {
	if (!defined($ftn{$fn})) { next; } # needed?
	if (&closed_entry($ftn{$fn})) { next; }
	$r = &fn_to_rn($fn);
	if (!defined($r)) { next; }
	$a = $polladdr{$r};
	if (!defined($a)) {
	    if (defined($opt_v) || defined($opt_d)) {
		printf(STDERR "Closing %s on nonexisting router %s\n",
		       $fn, $r);
	    }
	    $ftn{$fn} .= $now;
	    next;
	}
	if ($dead{$a}) { next; }

	if (!defined($line{$fn})) {
	    # no longer seen on device, and not closed, so close entry
	    if (defined($opt_v) || defined($opt_d)) {
		printf(STDERR "Closing %s, last on %s\n", $fn, $r);
	    }
	    $ftn{$fn} .= $now;
	}
    }
    if (defined($opt_v) || defined($opt_d)) {
	printf(STDERR "\nChecking for closeable name -> file entries\n\n");
    }
    foreach $ln (keys %ntf) {
	if (!defined($ntf{$ln})) { next; } # needed?
	# look for already-closed corresponding entries in $ntf?
	if (&closed_entry($ntf{$ln})) { next; }
	
	# pick apart entry in name-to-file database
	@a = split(/:/, $ntf{$ln});
	# split last entry on spaces
	@b = split(/ /, $a[$#a]);
	$fn = $b[0];
	$r = &fn_to_rn($fn);
	if (!defined($r)) { next; }
	$a = $polladdr{$r};
	if (!defined($a)) {
	    if (defined($opt_v) || defined($opt_d)) {
		printf(STDERR "Closing %s / %s on nonexisting router %s\n",
		       $ln, $fn, $r);
	    }
	    $ntf{$ln} .= $now;
	    next;
	}
	if ($dead{$a}) { next; }

	if (!defined($file{$ln})) {
	    # no longer seen on device, and not closed, so close
	    if (defined($opt_v) || defined($opt_d)) {
		printf(STDERR "Closing %s, file %s, last on %s\n",
		       $ln, $fn, $r);
	    }
	    $ntf{$ln} .= $now;
	}
    }
}

sub get_today_str {
    my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst);
    my($today_str);
    
    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
	localtime(time);
    
    $today_str = sprintf("%04d%02d%02d", $year+1900, $mon+1, $mday);
    return $today_str;
}

sub usage {
    die "usage: $0 -t topdir\n";
}

#
# Main
#

our($opt_t, $opt_v, $opt_d, $opt_n);

&getopts("D:dnt:v");

if (!defined($opt_t)) { &usage(); }
if (defined($opt_v) || defined($opt_d)) {
    select(STDERR);
    $| = 1;
    select(STDOUT);
}

&open_dbs($opt_t . "/db");
&read_config($opt_t . "/conf/polldevs.cf");

my($fh);
open($fh, "$opt_t/db/manual-devs") ||
    die "Could not open $opt_t/db/manual-devs\n";
&read_manuals($fh);
close($fh);

&get_current_state();
&update_dbs();
reset;
&close_dbs();
