#
# Miscellaneous routines to manipulate things in A3COM-SWITCHING-SYSTEMS-MIB
#
# Jim Trocki
#
# $Id: Sys.pm,v 1.7 1999/05/29 02:15:44 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::Sys;

use strict;
use vars qw($VERSION);
use SNMP;
use Socket;
use Expect;

sub filetransfer;
sub reset;
sub console_password;
sub system_info;
sub get_vlaninfo;

sub _delete_row;
sub _handle_bad_set;

$VERSION = "0.0202";

#
# %var = (
#	host => "hostname of switch",
#	community => "snmp write community name",
#	owner => "mgmt owner",
#	operation => "save_nvdata|restore_nvdata|softwareupdate",
#	tftphost => "hostname",
#	file => "filename",
#	status => \&status_callback,
#	force => "true|false",
# )
#
sub filetransfer {
    my %var = @_;
    my ($r, $errstr);

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

    my $delete_row = \&_delete_row;

    if ($var{"operation"} ne "save_nvdata" &&
    	$var{"operation"} ne "restore_nvdata" &&
	$var{"operation"} ne "softwareupdate") {
	return (undef, "unknown operation");
    }

    if ($var{"host"} eq "") {
    	return (undef, "no host specified");
    } elsif ($var{"community"} eq "") {
    	return (undef, "no community specified");
    } elsif ($var{"owner"} eq "") {
    	return (undef, "no owner specified");
    } elsif ($var{"tftphost"} eq "") {
    	return (undef, "no TFTP host specified");
    } elsif ($var{"file"} eq "") {
    	return (undef, "no file specified");
    } elsif ($var{"force"} !~ /^true|false$/) {
    	return (undef, "force not set to true of false");
    }

    my $dir;
    if ($var{"operation"} eq "save_nvdata") {
    	$dir = "localToRemote";
    } else {
    	$dir = "remoteToLocal";
    }

    my $src;
    if ($var{"operation"} eq "softwareupdate") {
    	$src = "storageFlashMemory";
    } else {
    	$src = "storageNonVolatileMemory";
    }

    my $rsrcattr;
    if ($var{"operation"} eq "softwareupdate") {
	$rsrcattr = SNMP::translateObj ("a3ComSysFtSystemOperationalCode");
    } else {
	$rsrcattr = SNMP::translateObj ("a3ComSysFtSystemConfiguration");
    }

    my $s = new SNMP::Session (
	    DestHost => $var{"host"},
	    Timeout => 10_000_000,
	    Retries => 3,
	    Community => $var{"community"},
	    UseEnums => 1,
    );

    return (undef, "could not create session") if (!defined $s);

    #
    # start from scratch
    #
    &{$delete_row}($s);

    #
    # begin row creation
    #
    $s->set ([
	["a3ComSysFtRowStatus", 1,
		    "createAndWait", "INTEGER"],
	["a3ComSysFtOwnerString", 1,
		    $var{"owner"}, "OCTETSTR"],
    ]);

    ($r, $errstr) = _handle_bad_set ($s, "could not create row", $delete_row);
    return (undef, $errstr) if (!defined $r);

    $s->set ([
	["a3ComSysFtDirection", 1,
		$dir, "INTEGER"],
	["a3ComSysFtLocalStorageType", 1,
		$src, "INTEGER"],
	["a3ComSysFtLocalResourceType", 1,
		"resourceSystem", "INTEGER"],
	["a3ComSysFtLocalResourceMask", 1,
		"\x00\x00\x00\x80", "OCTETSTR"],
    ]);

    ($r, $errstr) = _handle_bad_set ($s, "could not set resource", $delete_row);
    return (undef, $errstr) if (!defined $r);

    $s->set ([
	["a3ComSysFtLocalResourceAttribute", 1,
		$rsrcattr, "OBJECTID"],
	["a3ComSysFtRemoteAddressType", 1,
		"addrIp", "INTEGER"],
	["a3ComSysFtRemoteAddress", 1,
		inet_aton($var{"tftphost"}), "OCTETSTR"],
	["a3ComSysFtRemoteFileName", 1,
		$var{"file"}, "OCTETSTR"],
    ]);

    ($r, $errstr) = _handle_bad_set ($s, "could not set data source", $delete_row);
    return (undef, $errstr) if (!defined $r);

    #
    # clear username and stuff
    #
    $s->set ([
	["a3ComSysFtRemoteUserName", 1,
		"",
		"OCTETSTR"],
	["a3ComSysFtRemoteUserPassword", 1,
		"",
		"OCTETSTR"],
	["a3ComSysFtForceTransfer", 1,
		$var{"force"},
		"INTEGER"],
    ]);
    ($r, $errstr) = _handle_bad_set ($s, "could not set username", $delete_row);
    return (undef, $errstr) if (!defined $r);

    #
    # start the transfer
    #
    $s->set ([
	["a3ComSysFtRowStatus", 1,
	    "active", "INTEGER"],
    ]);

    ($r, $errstr) = _handle_bad_set ($s, "could not start transfer", $delete_row);
    return (undef, $errstr) if (!defined $r);

    #
    # monitor the transfer
    #
    my @a;
    while ((@a = $s->get ([
	    ["a3ComSysFtStatus", 1],
	    ["a3ComSysFtBytesTransferred", 1],
	]))[0] eq "statusInProgress") {
	&{$var{"status"}}(@a) if (ref($var{"status"}) eq "CODE");
	sleep 2;
    }

    if ($a[0] eq "statusSuccessfulCompletion" || $s->{"ErrorStr"} eq "Timeout") {
    	# all is OK
    } else {
	&{$delete_row}($s);
	my $e = $s->{"ErrorStr"};
	return (undef, "file transfer not completed: status[$a[0]] error[$e]");
    }

    #
    # delete row
    #
    &{$delete_row}($s);

    return 1;
}


sub _delete_row {
    my $s = shift;
    $s->set ([
	["a3ComSysFtRowStatus", 1,
		"destroy",
		"INTEGER"],
    ]);
}


sub _handle_bad_set {
    my ($s, $msg, $del) = @_;

    if ($s->{"ErrorStr"}) {
	my $e = $s->{"ErrorStr"};
	my $ind = $s->{"ErrorInd"};
	&{$del};
	return (undef, "$msg: $e item $ind");
    }
    return 1;
}


#
# Resets the system
#
# reset (
#	host => "hostname",
#	community => "community",
# )
#
sub reset {
    my %var = @_;
    my ($r, $errstr);

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

    my $s = new SNMP::Session (
	    DestHost => $var{"host"},
	    Timeout => 5_000_000,
	    Retries => 1,
	    Community => $var{"community"},
	    UseEnums => 1,
    );

    return (undef, "could not create session") if (!defined $s);

    if ($var{"host"} eq "") {
    	return (undef, "no host specified");
    } elsif ($var{"community"} eq "") {
    	return (undef, "no community specified");
    }

    $s->set (["a3ComSysSystemAction", 0, "reset", "INTEGER"]);

    if ($s->{"ErrorStr"} || $s->{"ErrorStr"} ne "Timeout") {
	return (undef, "reset failed: " . $s->{"ErrorStr"});
    }

    return 1;
}


sub system_info {
    my %var = @_;

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

    my $s = new SNMP::Session (
	    DestHost => $var{"host"},
	    Timeout => 5_000_000,
	    Retries => 1,
	    Community => $var{"community"},
	    UseEnums => 1,
    );

    return (undef, "could not create session") if (!defined $s);

    if ($var{"host"} eq "") {
    	return (undef, "no host specified");
    } elsif ($var{"community"} eq "") {
    	return (undef, "no community specified");
    }

    my $vb = new SNMP::VarList (
    	["a3ComSysSystemType", 0],
    	["a3ComSysSystemHardwareRevision", 0],
    	["a3ComSysSystemSoftwareRevision", 0],
    	["a3ComSysSystemChassisSerialNumber", 0],
    	["a3ComSysSystemName", 0],
    	["a3ComSysSystemId", 0],
	["a3ComSysModuleCardInfoModuleSerialNumber", 1],
    );

    $s->get ($vb);

    if ($s->{"ErrorStr"}) {
	return (undef, "could not get system info: " . $s->{"ErrorStr"});
    }

    my $v = {
	type => $vb->[0]->val,
	hardware_revision => join (".", unpack ("C C", $vb->[1]->val)),
	software_revision => join (".", unpack ("C C C", $vb->[2]->val)),
	part_number => $vb->[3]->val,
	name => $vb->[4]->val,
	id => sprintf ("%x", $vb->[5]->val),
	serial_number => $vb->[6]->val,
    };

    return (1, $v);
}


sub console_password {
    my %var = @_;
    my ($r, $errstr);

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

    if ($var{"host"} eq "") {
    	return (undef, "no host specified");
    } elsif ($var{"community"} eq "") {
    	return (undef, "no community specified");
    }

    my $s = new SNMP::Session (
	    DestHost => $var{"host"},
	    Timeout => 5_000_000,
	    Retries => 1,
	    Community => $var{"community"},
	    UseEnums => 1,
    );

    return (undef, "could not create session") if (!defined $s);

    my @p;
    if (defined $var{"read"}) {
    	push @p, ["a3ComSysSystemConsoleReadPwd", 0, $var{"read"}, "OCTETSTR"];
    }
    if (defined $var{"write"}) {
    	push @p, ["a3ComSysSystemConsoleWritePwd", 0, $var{"write"}, "OCTETSTR"];
    }
    if (defined $var{"adm"}) {
    	push @p, ["a3ComSysSystemConsoleAdminPwd", 0, $var{"adm"}, "OCTETSTR"];
    }

    if (@p == 0) {
	return (undef, "must set read, write, or adm");
    }

    my $v = new SNMP::VarList (@p);

    $s->set ($v);

    if ($s->{"ErrorStr"}) {
	return (undef, "set failed: error[" . $s->{"ErrorStr"} .
		"] ind[" . $s->{"ErrorInd"} . "]");
    }

    return 1;
}


sub get_password {
    my $prompt = shift;
    my $pass;

    if (-t STDIN) {
	my $sigint = $SIG{"INT"};
	$SIG{"INT"} = \&_restore_echo_and_exit;
	system "stty -echo";
	print STDERR $prompt;
	chop ($pass = <STDIN>);
	print STDERR "\n";
	system "stty echo";
	$SIG{"INT"} = $sigint;
    } else {
    	chomp ($pass = <STDIN>);
    }

    return $pass;
}

sub _restore_echo_and_exit {
    system "stty echo";
    exit 1;
}


#
# retrieve VLAN info
#
sub get_vlaninfo {
    my %var = @_;

    my $login = $var{"login"} || "read";
    my $password = $var{"password"} || "";
    my $host = $var{"host"};

    return (undef, "no host supplied") if ($host eq "");

    $Expect::Log_Stdout = 0;

    my $s = Expect->spawn ("telnet $host");
    return (undef, "could not telnet to $host") if (!$s);

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

    print $s "$login\r";

    if (!$s->expect (15, ("Password:"))) {
    	$s->hard_close();
	return (undef, "did not get password prompt in time");
    }
    $s->clear_accum;

    print $s "$password\r";

    my @n = $s->expect (8, "Incorrect password", "Select menu option: ");
    if ($n[0] == 1) {
    	$s->hard_close();
	return (undef, "incorrect password");
    } elsif (!defined $n[0] && $n[1] eq "1:TIMEOUT") {
    	$s->hard_close();
	return (undef, "timeout waiting for prompt");
    }
    $s->clear_accum;

    print $s "brid vlan detail all\r";
    
    my $vlaninfo;
    my ($pos, $err, $string, $before, $after) = $s->expect (30, ("Select menu option:"));
    
    if ($err eq "1:TIMEOUT") {
    	$s->hard_close();
	return (undef, "timeout waiting for vlan info");
    }

    $vlaninfo = $before;

    print $s "logout\r";

    ($pos, $err, $string, $before, $after) = $s->expect (30, ("Exiting"));

    if ($err eq "1:TIMEOUT") {
    	$s->hard_close();
	return (undef, "timeout after issuing 'logout'");
    }

    $s->hard_close();

    #
    # decode junk
    #
    my $tbl = "";
    my %idx_vid = ();
    my %vlan = ();

    for (split (/\n\r/, $vlaninfo)) {
    	next if (/^\s*$/);
	s/^\s*//;

	last if (/^Menu options/);

	if (/index \s+ vid \s+ type \s+ origin \s*$/ix) {
	    $tbl = "vlan";
	    next;
	} elsif (/index \s+ vid \s+ type \s+ origin \s+ name/ix) {
	    $tbl = "vlanCB";
	    next;
	} elsif (/index \s+ name \s+ ports/ix) {
	    $tbl = "port";
	    next;
	} elsif (/index \s+ port \s+ tag/ix) {
	    $tbl = "tag";
	    next;
	} elsif (/index \s+ protocol/ix) {
	    $tbl = "protCB";
	    next;
	} elsif (/index \s+ layer \s+ 3/ix) {
	    $tbl = "l3infoCB";
	    next;
	} elsif (/^index/ix) {
	    $tbl = "unknown";
	    next;
	}

	s/[\r\n]+//g;
	s/\s*$//;

	if ($tbl eq "vlan") {
	    my ($idx, $vid, $type, $origin) = split (/\s+/, $_);
	    $idx_vid{$idx} = $vid;
	    $vlan{$vid}->{"type"} = $type;
	    $vlan{$vid}->{"origin"} = $origin;

	} elsif ($tbl eq "vlanCB") {
	    my ($idx, $vid, $type, $origin, $name, $ports) = split (/\s+/, $_);
	    $idx_vid{$idx} = $vid;
	    $vlan{$vid}->{"type"} = $type;
	    $vlan{$vid}->{"origin"} = $origin;
	    $vlan{$vid}->{"name"} = $name;
	    $vlan{$vid}->{"ports"} = "$ports";

	} elsif ($tbl eq "port") {
	    my ($idx, $name, $ports) = split (/\s+/, $_);
	    $vlan{$idx_vid{$idx}}->{"name"} = $name;
	    $vlan{$idx_vid{$idx}}->{"ports"} = "$ports";

	} elsif ($tbl eq "tag") {
	    my ($idx, $port, $tag) = split (/\s+/, $_);
	    my $vid = $idx_vid{$idx};
	    $vlan{$vid}->{"porttags"}->{$port} = $tag;

	} elsif ($tbl eq "protCB") {
	    my ($idx, $prot) = split (/\s+/, $_);
	    $vlan{$idx_vid{$idx}}->{"protocol"} = $prot;

	} elsif ($tbl eq "l3infoCB") {
	    my ($idx, $addr) = split (/\s+/, $_, 2);
	    $vlan{$idx_vid{$idx}}->{"l3addr"} = $addr;

	} else {
	    # unknown
	}
    }

    return (1, {%vlan});
}


sub set_screenheight {
    my %var = @_;

    my $SWITCH = $var{"host"};
    my $LOGIN  = $var{"login"};
    my $PASSWORD = $var{"password"};

    my $s = Expect->spawn ("telnet $SWITCH") ||
	    return (undef, "could not telnet to $SWITCH");

    $s->log_stdout(0) unless $var{"verbose"};


    #
    # log in
    #
    $s->expect (15, ("Select access level (read, write, administer): ")) ||
	return (undef, "did not get login prompt in time");

    $s->clear_accum;

    print $s "$LOGIN\r";

    $s->expect (8, ("Password:")) || return (undef, "did not get password prompt");
    $s->clear_accum;

    print $s "$PASSWORD\r";

    my @n = $s->expect (8, "Incorrect password", "Select menu option: ");

    if ($n[0] == 1) {
	$s->hard_close();
	return (undef, "incorrect password for $SWITCH");

    } elsif (!defined $n[0] && $n[1] eq "1:TIMEOUT") {
	$s->hard_close();
	return (undef, "timeout logging in to $SWITCH, it says {$n[3]}");
    }
    $s->clear_accum;

    print $s "system screenHeight 0\r";

    @n = $s->expect (45, "the new default screen height");
    $s->clear_accum;

    if (!defined ($n[0])) {
	$s->hard_close();
	return (undef, "error ($n[1]) while setting screen height $SWITCH\n$n[3]");
    }

    print $s "y\r";

    @n = $s->expect (45, "Select menu option:");
    $s->clear_accum;

    if (!defined ($n[0])) {
	$s->hard_close();
	return (undef, "error ($n[1]) no menu prompt after setting scrn height $SWITCH\n$n[3]");
    }


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

    $s->hard_close();

    return (1, 1);
}
