#
# Dump bridge table from 3Com SuperStack II switches
#
# $Id: BridgeTable.pm,v 1.21 1999/06/03 15:09:10 trockij Exp $
#
# Copyright (C) 1998 Jim Trocki
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
package A3Com::BridgeTable;

use strict;
use vars qw($VERSION);
use A3Com::Base;
use Expect;
use SNMP;

$VERSION = "0.01";

sub _convert_bridgetable;

sub new {
    my $proto = shift;
    my %vars = @_;
    my $class = ref($proto) || $proto;
    my $self  = {};

    $self->{"PORTS"} = {};
    $self->{"MACS"} = {};
    $self->{"ERROR"} = "";
    $self->{"SWITCH"} = "";
    $self->{"READ_METHOD"} = \&snmp_read;
    $self->{"LOGIN"} = "read";
    $self->{"PASSWORD"} = "";
    $self->{"LOG"} = 0;
    $self->{"CACHE"} = 1;
    $self->{"GLOBALCACHE"} = 1;
    $self->{"CACHETIME"} = $A3Com::Base::DEFAULT_CACHETIME;
    $self->{"FILE"} = "";

    $self->{"GLOBALCACHEDIR"} = "/usr/local/share/a3com";
    if (defined $ENV{"HOME"}) {
	$self->{CACHEDIR} = $ENV{"HOME"} . "/.a3com";
    } else {
	$self->{CACHEDIR} = ".a3com";
    }

    my %c = &A3Com::Base::_read_conf ($self, $ENV{"A3COM_CONF"});
    for (keys %c) {
	$self->{$_} = $c{$_};
    }

    if (defined $ENV{"A3COM_GLOBALCACHEDIR"}) {
    	$self->{"GLOBALCACHEDIR"} = $ENV{"A3COM_GLOBALCACHEDIR"};
    }

    if (defined $ENV{"A3COM_CACHEDIR"}) {
	$self->{"CACHEDIR"} = $ENV{"A3COM_CACHEDIR"};
    }
    $self->{_LOADED} = 0;

    for my $k (keys %vars) {
    	$self->{$k} = $vars{$k} if ($vars{$k} ne "");
    }

    bless ($self, $class);
    return $self;
}


sub cachedir {
    my $self = shift;
    if (@_) { $self->{CACHEDIR} = shift }
    return $self->{CACHEDIR};
}


sub globalcachedir {
    my $self = shift;
    if (@_) { $self->{"GLOBALCACHEDIR"} = shift }
    return $self->{"GLOBALCACHEDIR"};
}


sub cachetime {
    my $self = shift;
    if (@_) { $self->{CACHETIME} = shift }
    return $self->{CACHETIME};
}


sub switch {
    my $self = shift;
    if (@_) { $self->{SWITCH} = shift }
    return $self->{SWITCH};
}


sub error {
    my $self = shift;
    if (@_) { $self->{ERROR} = shift }
    return $self->{ERROR};
}


sub log {
    my $self = shift;
    if (@_) { $self->{LOG} = shift }
    return $self->{LOG};
}


sub cache {
    my $self = shift;
    if (@_) { $self->{CACHE} = shift }
    return $self->{CACHE};
}


sub globalcache {
    my $self = shift;
    if (@_) { $self->{"GLOBALCACHE"} = shift }
    return $self->{"GLOBALCACHE"};
}


sub port {
    my $self = shift;
    my $portnum = shift;

    $self->{"ERROR"} = "";

    return undef if (!defined $self->_load_cache ($self));

    if (!defined $self->{"PORTS"}->{$portnum}) {
    	$self->{"ERROR"} = "port $portnum not found";
	return undef;
    }
    return @{$self->{"PORTS"}->{$portnum}};
}


sub ports {
    my $self = shift;

    $self->{"ERROR"} = "";

    return undef if (!defined $self->_load_cache ($self));

    return keys %{$self->{"PORTS"}};
}


sub mac {
    my $self = shift;
    my $macaddr = A3Com::Base::_mac_normalize (shift);

    $self->{"ERROR"} = "";

    return undef if (!defined $self->_load_cache ($self));

    if (!defined $self->{MACS}->{$macaddr}) {
    	$self->{"ERROR"} = "mac $macaddr not found";
	return undef;
    }
    return @{$self->{"MACS"}->{$macaddr}};
}


sub macs {
    my $self = shift;

    $self->{"ERROR"} = "";

    return undef if (!defined $self->_load_cache ($self));

    return keys %{$self->{"MACS"}};
}


sub btable {
    my $self = shift;

    $self->{"ERROR"} = "";

    return undef if (!defined $self->_load_cache ($self));

    return %{$self->{"PORTS"}};
}


#
# write bridge table
#
sub file_write {
    my $self = shift;
    my $file = shift;

    my ($port);

    $self->{"ERROR"} = "";

    if (!defined $file) {
        my $cd;

        if ($self->{"GLOBALCACHE"}) {
            $cd = "GLOBALCACHEDIR";
        } else {
            $cd = "CACHEDIR";
        }

        if (! -d $self->{$cd}) {
            $self->{"ERROR"} = "$cd is not a directory";
            return undef;
        }
        $file = "$self->{$cd}/$self->{SWITCH}.bt";
    }

    if (!open (O, ">$file")) {
    	$self->{"ERROR"} = "$!";
	return undef;
    }

    print O "#\n# bridge table for $self->{SWITCH} at " . time . "\n#\n";
    foreach $port (sort {$a <=> $b} keys %{$self->{"PORTS"}}) {
	print O "$port @{$self->{PORTS}->{$port}}\n";
    }
    close (O);
}


#
# read bridge table
#
sub file_read {
    my $self = shift;
    my $file = shift;
    my ($port, $mac, @macs);

    $self->{"ERROR"} = "";

    if (!defined $file) {
        if (! -d $self->{"CACHEDIR"}) {
            $self->{"ERROR"} = "CACHEDIR is not a directory";
            return undef;
        }
        $file = "$self->{CACHEDIR}/$self->{SWITCH}.bt";
    }

    if (!open (I, $file)) {
    	$self->{"ERROR"} = "$!";
	return undef;
    }

    $self->{"PORTS"} = {};
    $self->{"MACS"} = {};

    while (<I>) {
	chomp;
	next if (/^\s*#/ || /^\s*$/);
    	($port, @macs) = split;
	@{$self->{"PORTS"}->{$port}} = @macs;
	for (@macs) {
	    push @{$self->{"MACS"}->{$_}}, $port;
	}
    }
    close (I);
}


#
# returns 2-element array, anonymous refs to
# porthash and machash.
#
# If first element is undef, then a failure ocurred,
# and error text is in second element.
#
sub telnet_read {
    my $self = shift;

    my ($ADDRS, $s, @n);

    $s = Expect->spawn ("telnet $self->{SWITCH}");
    if (!$s) {
	$self->{"ERROR"} = "could not telnet to $self->{SWITCH}";
    	return undef;
    }
    $s->log_stdout($self->{"LOG"});

    #
    # log in
    #
    if (!$s->expect (15, ("Select access level (read, write, administer): "))) {
	$self->{"ERROR"} = "did not get login prompt in time";
	return undef;
    }
    $s->clear_accum;

    print $s "$self->{LOGIN}\r";

    if (!$s->expect (8, ("Password:"))) {
	$self->{"ERROR"} = "did not get password prompt";
	return undef;
    }
    $s->clear_accum;

    print $s "$self->{PASSWORD}\r";

    @n = $s->expect (8, "Incorrect password", "Select menu option: ");
    if ($n[0] == 1) {
	$s->hard_close();
	$self->{"ERROR"} = "incorrect password for $self->{SWITCH}";
	return undef;
    } elsif (!defined $n[0] && $n[1] eq "1:TIMEOUT") {
	$s->hard_close();
	$self->{"ERROR"} = "timeout logging in to $self->{SWITCH}, it says {$n[3]}";
	return undef;
    }
    $s->clear_accum;

    print $s "bridge port addr list\r";

    @n = $s->expect (10, "Enter VLAN interface indexes", "Select bridge ports");
    $s->clear_accum;

    if (!defined ($n[0])) {
	$s->hard_close();
	$self->{"ERROR"} = "error ($n[1]) during VLAN/bridge port interface prompt $self->{SWITCH}\n$n[3]";
	return undef;

    } elsif ($n[0] == 1) {
	print $s "all\r";

	@n = $s->expect (45, "Select bridge ports");
	$s->clear_accum;

	if (!defined ($n[0])) {
	    $s->hard_close();
	    $self->{"ERROR"} = "error ($n[1]) during bridge ports prompt $self->{SWITCH}\n$n[3]";
	    return undef;
	}
    }

    print $s "all\r";

    @n = $s->expect (120, "Select menu option:");
    $ADDRS = $n[3];

    $s->clear_accum;

    #
    # log out politely
    #
    print $s "logout\r";
    if ($s->expect (8, "Exiting") != 1) {
	$s->hard_close();
	$self->{"ERROR"} = "did not get logout confirmation";
	return undef;
    }

    $s->hard_close();

    _convert_bridgetable $self, $ADDRS;
}


sub _convert_bridgetable {
    my $self = shift;
    my $ADDRS = shift;
    my ($l);
    my ($vlan, $port, $mac);

    $vlan = 0;

    while ($ADDRS =~ /([^\n\r]+\n\r)/sg) {
	$l = $1;
	$l =~ s/[\r\n]*$//;
	
	if ($l =~ /Addresses for port (\d+)/) {
	    $port = $1;
	    next;

	} elsif ($l =~ /There are no addresses for port (\d+)/) {
	    $port = $1;
	    @{$self->{"PORTS"}->{$port}} = ();

	} elsif ($l =~ /Addresses for.*interface\s+(\d+),\s+port\s+(\d+)/i) {
	    ($vlan, $port) = ($1, $2);
	    next;

	} elsif ($l =~ /There are no addresses for.*interface\s+(\d+),\s+port\s+(\d+)/i) {
	    ($vlan, $port) = ($1, $2);
	    @{$self->{"PORTS"}->{$port}} = ();
	    next;
	} elsif ($l =~ /^\s*(\w\w-\w\w-\w\w-\w\w-\w\w-\w\w)/) {
	    $mac = $1;
	    $mac =~ s/-/:/g;
	    $mac = A3Com::Base::_mac_normalize ($mac);
	    push @{$self->{"PORTS"}->{$port}}, $mac;
	    push @{$self->{"MACS"}->{$mac}}, $port;
	    next;
	}
    }
}


sub snmp_read {
    my $self = shift;

    if ($self->{"SWITCH"} eq "") {
    	$self->{"ERROR"} eq "no switch supplied";
	return undef;
    }

    $ENV{"MIBS"} = "+A3COM-SWITCHING-SYSTEMS-MIB";

    my $s = new SNMP::Session (
	DestHost => $self->{"SWITCH"},
	Community => ($self->{"PASSWORD"} or "public"),
	UseEnums => 1,
	Timeout => 30_000_000,
    );

    if (!defined $s) {
	$self->{"ERROR"} = "could not create session";
	return undef;
    }

    #
    # get revision via a3ComSysSystemSoftwareRevision.0
    #
    my $rev = $s->get(["a3ComSysSystemSoftwareRevision", 0]);
    if (!defined $rev) {
    	$self->{"ERROR"} = "error retrieving switch version:" . $s->{"ErrorStr"};
	return undef;
    }

    if (unpack ("C", $rev) < 2) {
	return $self->telnet_read;
    }

    my $v = new SNMP::Varbind (["a3ComSysBridgeVlanPortAddressBridgeIndex"]);

    $s->getnext ($v);

    while (!$s->{"ErrorStr"} && $v->tag eq "a3ComSysBridgeVlanPortAddressBridgeIndex") {
	my @q = $s->get ([
	    ["a3ComSysBridgeVlanPortAddressVlanIndex", $v->iid],
	    ["a3ComSysBridgeVlanPortAddressPortIndex", $v->iid],
	    ["a3ComSysBridgeVlanPortAddressIndex", $v->iid],
	    ["a3ComSysBridgeVlanPortAddressRemoteAddress", $v->iid],
	    ["a3ComSysBridgeVlanPortAddressType", $v->iid],
	    ["a3ComSysBridgeVlanPortAddressIsStatic", $v->iid],
	    ["a3ComSysBridgeVlanPortAddressStaticPort", $v->iid],
	    ["a3ComSysBridgeVlanPortAddressAge", $v->iid],
	]);

	last if ($s->{"ErrorStr"});

	$q[3] = A3Com::Base::_mac_normalize (
		sprintf "%02x:%02x:%02x:%02x:%02x:%02x", unpack ("C6", $q[3])
	);

	push @{$self->{PORTS}->{$q[1]}}, $q[3];
	push @{$self->{MACS}->{$q[3]}}, $q[1];

	$s->getnext ($v);
    }

    if ($s->{"ErrorStr"}) {
	$self->{"ERROR"} = $s->{"ErrorStr"};
	return undef;
    }
}


sub _load_cache {
    my $self = shift;

    if (!$self->{"_LOADED"}) {
	if ($self->{"GLOBALCACHE"}) {
	    return undef if (!defined (A3Com::Base::_load_global ($self, ".bt")));
	} else {
	    return undef if (!defined (A3Com::Base::_load ($self, ".bt")));
	}
	$self->{"_LOADED"} = 1;
    }
}
