Commit b9dffc3e authored by Kirk Webb's avatar Kirk Webb
Browse files

Pull in new comware (h3c) module, courtesy of Keith Sklower @ DETER.

For now I'm adding this as a separate file, wherein I have renamed the
base procurve module as "snmpit_hpupd" until we have more time to test the
integration.  I did a fairly quick-and-dirty port of this code to work
with modern Emulab snmpit.

The comware module will be used for switches with "comware" in their
'type' string.
parent cee881a6
......@@ -40,7 +40,7 @@ LIB_STUFF = portstats snmpit_intel.pm \
snmpit_nortel.pm snmpit_hp.pm snmpit_apcon.pm \
snmpit_arista.pm snmpit_arista_switch_daemon.py \
snmpit_mellanox.pm MLNX_XMLGateway.pm \
snmpit_force10.pm force10_expect.pm
snmpit_force10.pm force10_expect.pm snmpit_h3c.pm
#
# Force dependencies on the scripts so that they will be rerun through
......
#!/usr/bin/perl -w
#
# {{{EMULAB-LGPL
#
# This file is part of the Emulab network testbed software.
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or (at
# your option) any later version.
#
# This file 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
# Copyright (c) 2000-2014 University of Utah and the Flux Group.
# Copyright (c) 2004-2014 Regents, University of California.
# All rights reserved.
#
#
# snmpit module for HP procurve and flexfabric (comware) switches.
# The Comware module (snmpit_h3c) inherits from the base (snmpit_hp) class.
#
# XXX: krw: Rename base HP module here so that we don't actually use
# this for the procurve models until I have more time to test.
package snmpit_hpupd;
use strict;
$| = 1; # Turn off line buffering on output
use English;
use SNMP;
use Carp qw(cluck);
use lib '/usr/testbed/lib';
use snmpit_lib;
use libtestbed;
use Lan;
use Port;
#
# These are the commands that can be passed to the portControl function
# below
#
my %cmdOIDs =
(
"enable" => ["ifAdminStatus","up"],
"disable"=> ["ifAdminStatus","down"],
"1000mbit"=> ["hpSwitchPortFastEtherMode","auto-1000Mbits"],
"100mbit"=> ["hpSwitchPortFastEtherMode","auto-100Mbits"],
"10mbit" => ["hpSwitchPortFastEtherMode","auto-10Mbits"],
"auto" => ["hpSwitchPortFastEtherMode","auto-neg"],
"full" => ["hpSwitchPortFastEtherMode","full"],
"half" => ["hpSwitchPortFastEtherMode","half"],
);
#
# some long OIDs that get used frequently.
#
my $normOID = "dot1qVlanStaticUntaggedPorts";
my $forbidOID = "dot1qVlanForbiddenEgressPorts";
my $egressOID = "dot1qVlanStaticEgressPorts";
my $aftOID = "dot1qPortAcceptableFrameTypes";
my $createOID = "dot1qVlanStaticRowStatus";
#
# Enterprise OID for toggling jumbo frame support on a vlan
#
my $jumboOID = '1.3.6.1.4.1.11.2.14.11.5.1.12.1.8.1.1.1';
#
# Openflow OIDs, only number format now.
#
#my $ofOID = 'iso.org.dod.internet.private.enterprises.11.2.14.11.5.1.7.1.35';
my $ofOID = '1.3.6.1.4.1.11.2.14.11.5.1.7.1.35';
my $ofEnableOID = $ofOID.'.1.1.2';
my $ofControllerOID = $ofOID.'.1.1.3';
my $ofListenerOID = $ofOID.'.1.1.4';
my $ofFailModeOID = $ofOID.'.1.1.11';
my $ofSupportOID = $ofOID.'.2.1.0';
# This string is enough now, but the Openflow OID may change in future.
# The maintainers should keep in mind of this ID.
my $ofListenerVarNameMarker = '35.1.1.4';
#
# Ports can be passed around in three formats:
# ifindex: positive integer corresponding to the interface index (eg. 42)
# modport: dotted module.port format, following the physical reality of
# Cisco switches (eg. 5.42)
# nodeport: node:port pair, referring to the node that the switch port is
# connected to (eg. "pc42:1")
#
# See the function convertPortFormat below for conversions between these
# formats
#
my $PORT_FORMAT_IFINDEX = 1;
my $PORT_FORMAT_MODPORT = 2;
my $PORT_FORMAT_NODEPORT = 3; # XXX - not used anymore here.
my $PORT_FORMAT_PORT = 4;
#
# Creates a new object.
#
# usage: new($classname,$devicename,$debuglevel,$community)
# returns a new object, blessed into the snmpit_intel class.
#
sub new($$$;$) {
# The next two lines are some voodoo taken from perltoot(1)
my $proto = shift;
my $class = ref($proto) || $proto;
my $name = shift;
my $debugLevel = shift;
my $community = shift;
#
# Create the actual object
#
my $self = {};
#
# Set the defaults for this object
#
if (defined($debugLevel)) {
$self->{DEBUG} = $debugLevel;
} else {
$self->{DEBUG} = 0;
}
$self->{BLOCK} = 1;
$self->{CONFIRM} = 1;
$self->{NAME} = $name;
#
# Get config options from the database
#
my $options = getDeviceOptions($self->{NAME});
if (!$options) {
warn "ERROR: Getting switch options for $self->{NAME}\n";
return undef;
}
$self->{MIN_VLAN} = $options->{'min_vlan'};
$self->{MAX_VLAN} = $options->{'max_vlan'};
if ($community) { # Allow this to over-ride the default
$self->{COMMUNITY} = $community;
} else {
$self->{COMMUNITY} = $options->{'snmp_community'};
}
# Use jumbo frames?
if (exists($options->{'use_jumbo'})
&& $options->{'use_jumbo'} == 1) {
$self->{DOJUMBO} = 1;
} else {
$self->{DOJUMBO} = 0;
}
#
# set up hashes for internal use
#
$self->{IFINDEX} = {};
$self->{TRUNKINDEX} = {};
$self->{TRUNKS} = {};
$self->{cmdOIDs} = \%cmdOIDs;
# other global variables
$self->{DOALLPORTS} = 0;
$self->{DOALLPORTS} = 1;
$self->{SKIPIGMP} = 1;
if ($self->{DEBUG}) {
print "snmpit_hp initializing $self->{NAME}, " .
"debug level $self->{DEBUG}\n" ;
}
#
# Set up SNMP module variables, and connect to the device
#
$SNMP::debugging = ($self->{DEBUG} - 2) if $self->{DEBUG} > 2;
my $mibpath = '/usr/local/share/snmp/mibs';
&SNMP::addMibDirs($mibpath);
&SNMP::addMibFiles("$mibpath/SNMPv2-SMI.txt", "$mibpath/SNMPv2-TC.txt",
"$mibpath/SNMPv2-MIB.txt", "$mibpath/IANAifType-MIB.txt",
"$mibpath/IF-MIB.txt", "$mibpath/BRIDGE-MIB.txt",
"$mibpath/IEEE8023-LAG-MIB.txt",
"$mibpath/HP-ICF-OID.txt", "$mibpath/HH3C-OID-MIB.txt");
$SNMP::save_descriptions = 1; # must be set prior to mib initialization
SNMP::initMib(); # parses default list of Mib modules
$SNMP::use_enums = 1; # use enum values instead of only ints
warn ("Opening SNMP session to $self->{NAME}...") if ($self->{DEBUG});
$self->{SESS} = new SNMP::Session(DestHost => $self->{NAME},Version => "2c",
Timeout => 3000000, Retries=> 9, Community => $self->{COMMUNITY});
if (!$self->{SESS}) {
#
# Bomb out if the session could not be established
#
warn "WARNING: Unable to connect via SNMP to $self->{NAME}\n";
return undef;
}
#
# The bless needs to occur before readifIndex(), since it's a class
# method
#
#
# Sometimes the SNMP session gets created when there is no connectivity
# to the device so let's try something simple
#
my $test_case = snmpitGet($self->{SESS}, ["sysObjectID", 0], 1);
if (!defined($test_case)) {
warn "WARNING: Unable to retrieve via SNMP from $self->{NAME}\n";
return undef;
}
$self->{HPTYPE} = SNMP::translateObj($test_case);
$class = $self->{H3C} = 'snmpit_h3c'
if ($test_case =~ '^.1.3.6.1.4.1.25506');
bless($self,$class);
$self->readifIndex();
return $self;
}
# Attempt to repeat an action until it succeeds
sub hammer($$$;$) {
my ($self, $closure, $id, $retries) = @_;
if (!defined($retries)) { $retries = 12; }
for my $i (1 .. $retries) {
my $result = $closure->();
if (defined($result) || ($retries == 1)) { return $result; }
warn $id . " ... will try again\n";
sleep 1;
}
warn $id . " .. giving up\n";
return undef;
}
# shorthand
sub printSnmpErr($$) {
my ($sess, $id) = @_;
if ($sess->{ErrorNum}) {
print "$id had error number " . $sess->{ErrorNum} .
" and had error string " . $sess->{ErrorStr} . "\n";
}
}
sub get1($$$) {
my ($self, $obj, $instance) = @_;
my $id = $self->{NAME} . "::get1($obj.$instance)";
my $closure = sub () {
my $RetVal = snmpitGet($self->{SESS}, [$obj, $instance], 1);
if (!defined($RetVal)) { sleep 4;}
return $RetVal;
};
my $RetVal = $self->hammer($closure, $id, 40);
printSnmpErr($self->{SESS}, $id)
if (!defined($RetVal));
return $RetVal;
}
sub set($$;$$) {
my ($self, $varbind, $id, $retries) = @_;
if (!defined($id)) { $id = $self->{NAME} . ":set "; }
if (!defined($retries)) { $retries = 2; }
my $sess = $self->{SESS};
my $closure = sub () {
my $RetVal = $sess->set($varbind);
my $status = $RetVal;
if (!defined($RetVal)) {
$status = "(undefined)";
printSnmpErr($sess, $id);
}
return $RetVal;
};
my $RetVal = $self->hammer($closure, $id, $retries);
return $RetVal;
}
sub mirvPortSet($) {
my ($bitfield) = @_;
$bitfield = substr($bitfield,0,127);
my $unpacked = unpack("B*",$bitfield);
return [split //, $unpacked];
}
sub testPortSet($$) {
my ($bitfield, $index) = @_;
return @{mirvPortSet($bitfield)}[$index];
}
#
# opPortSet($op, $bitfield, @indices)
# set or clear bits in a port set, $op > 0 means set, otherwise clear
#
sub opPortSet($$@) {
my ($op, $bitfield, @indices) = @_;
my @bits = mirvPortSet($bitfield);
foreach my $index (@indices) { $bits[$index] = $op > 0 ? 1 : 0; }
return pack("B*", join('',@bits));
}
# Translate a list of ifIndexes to a PortSet
sub listToPortSet($@)
{
my $self = shift;
my @ports = @_;
my ($max, $portbitstring, $portSet, $port);
my @portbits;
$self->debug("listToPortsSet: input @ports\n",2);
if (scalar @ports) {
@ports = sort { $b <=> $a} @ports;
$max = ($ports[0] | 7);
} else { $max = 7 ; }
$port = 0;
while ($port <= $max) { $portbits[$port] = 48 ; $port++; }
while (scalar @ports) { $port = pop @ports; $portbits[$port - 1] = 49; }
$self->debug("portbits after insertions: @portbits\n",2);
$portbitstring = pack "C*", @portbits;
$self->debug("listToPortSet output string $portbitstring \n");
$portSet = pack "B*", $portbitstring ;
return $portSet;
}
# Translate a PortSet to a list of ifIndexes
sub bitSetToList($)
{
my ($arrayref) = @_;
my @ports = ();
my $max = scalar (@$arrayref);
for (my $port = 0; $port < $max; $port++)
{ if (@$arrayref[$port]) { push @ports, (1 + $port); } }
return @ports;
}
sub portSetToList($$) {
my ($self, $portset) = @_;
return bitSetToList(mirvPortSet($portset));
}
sub getOidToMappedList($$$) {
my ($self, $oid, $idx) = @_;
if (my $bits = $self->get1($oid,$idx)) {
return $self->portSetToList($bits);
} else { return (); }
}
sub trunkedPorts($@) {
my ($self, @ports) = @_;
my ($trunks, $dualPorts, $modes, $pids, $j, @nports) = ({}, {});
@$dualPorts{$self->portSetToList($self->get1($forbidOID,1))} = undef;
if (@ports) {
$modes = [ map { [ $aftOID, $_ ] } @ports ];
$j = $self->{SESS}->get($modes);
} else {
($modes) = $self->{SESS}->bulkwalk(0,32, [$aftOID]);
}
foreach my $aref (@$modes) {
my ($name, $ifIndex, $val) = @$aref;
if ($val eq 'admitOnlyVlanTagged') {
$$trunks{$ifIndex} = 1;
} elsif (exists($$dualPorts{$ifIndex})) {
$$trunks{$ifIndex} = $self->get1('dot1qPvid', $ifIndex);
}
}
return ($trunks);
}
#
# Set a variable associated with a port. The commands to execute are given
# in the cmdOIs hash above
#
# usage: portControl($self, $command, @ports)
# returns 0 on success.
# returns number of failed ports on failure.
# returns -1 if the operation is unsupported
#
sub portControl ($$@) {
my $self = shift;
my $cmd = shift;
my @ports = @_;
my $cmdOIDs = $self->{cmdOIDs};
$self->debug("portControl: $cmd -> (@ports)\n");
#
# Find the command in the %cmdOIDs hash (defined at the top of this file).
# Some commands involve multiple SNMP commands, so we need to make sure
# we get all of them
#
if (defined $$cmdOIDs{$cmd}) {
my @oid = @{$$cmdOIDs{$cmd}};
my $errors = 0;
while (@oid) {
my $myoid = shift @oid;
my $myval = shift @oid;
$errors += $self->UpdateField($myoid,$myval,@ports);
}
return $errors;
} else {
#
# Command not supported
#
$self->debug("Unsupported port control command '$cmd' ignored.\n");
return 0;
}
}
#
# Get a vlan's ifindex given it's tag
#
sub getVlanIfindexFromTag($$) {
my ($self, $tag) = @_;
my $id = $self->{NAME} . "::getVlanIfindexFromTag";
my ($rows) = snmpitBulkwalkFatal($self->{SESS}, ["ifDescr"]);
if (!@$rows) {
warn "$id: ERROR: No interface description rows returned ".
"while attempting to search for vlan ifindex!\n";
return undef;
}
foreach my $rowref (@$rows) {
my ($name,$ifindex,$descr) = @$rowref;
next unless $descr =~ /VLAN(\d+)/i;
if ($tag == $1) {
return $ifindex;
}
}
warn "$id: no ifindex found for vlan with tag: $tag\n";
return undef;
}
#
# Set jumbo frames on a vlan
#
sub setVlanJumbo($$) {
my ($self, $tag) = @_;
my $id = $self->{NAME} . "::setVlanJumbo";
my $vifindex = $self->getVlanIfindexFromTag($tag);
goto bad if !defined($vifindex);
$self->debug("id: Enabling jumbo frames on vlan $tag (ifindex: $vifindex)\n");
my $res = $self->set([$jumboOID,$vifindex,1,"INTEGER"], $id);
goto bad if !defined($res);
return 0;
bad:
warn "$id: Could not enable jumbo frames for vlan with tag: $tag\n";
return 1;
}
#
# HP's refuse to create vlans with display names that can
# be interpreted as vlan numbers
#
sub convertVlanName($) {
my $id = shift;
my $new;
if ( $id =~ /^_(\d+)$/) {
$new = $1;
return ((($new > 0) && ($new < 4095)) ? $new : $id);
}
if ( $id =~ /^(\d+)$/) {
$new = $1;
return ((($new > 0) && ($new < 4095)) ? "_$new" : $id);
}
return $id;
}
#
# Try to pull a VLAN number out of a long OID string
#
sub parseVlanNumberFromOID($) {
my ($oid) = @_;
# OID must be a dotted string
my (@elts) = split /\./, $oid;
if (scalar(@elts) < 2) {
return undef;
}
# Second-to-last element must be the right text string or numeric ID
if ($elts[$#elts-1] eq "dot1qVlanStaticName" || $elts[$#elts-1] eq "1") {
# Last element must be numeric
if ($elts[$#elts] =~ /\d+/) {
return $elts[$#elts];
} else {
return undef;
}
} else {
return undef;
}
}
sub checkLACP($$) {
my ($self, $port) = @_;
if (my $j = $self->{TRUNKINDEX}{$port})
{ $port = $j + $self->{TRUNKOFFSET}; }
return $port;
}
#
# Convert a set of ports to an alternate format. The input format is detected
# automatically. See the declarations of the constants at the top of this
# file for a description of the different port formats.
#
# usage: convertPortFormat($self, $output format, @ports)
# returns a list of ports in the specified output format
# returns undef if the output format is unknown
#
# TODO: Add debugging output, better comments, more sanity checking
#
sub convertPortFormat($$@) {
my $self = shift;
my $output = shift;
my @ports = @_;
my @results = ();
#
# Avoid warnings by exiting if no ports given
#
if (!@ports) {
return ();
}
#
# We determine the type by sampling the first port given
#
my $sample = $ports[0];
if (!defined($sample)) {
warn "convertPortFormat: Given a bad list of ports\n";
return undef;
}
my $input;
SWITCH: for ($sample) {
(Port->isPort($sample)) && do { $input = $PORT_FORMAT_PORT; last; };
(/^\d+$/) && do { $input = $PORT_FORMAT_IFINDEX; last; };
(/^\d+\.\d+$/) && do { $input = $PORT_FORMAT_MODPORT; last; };
(/^$self->{NAME}\.\d+\/\d+$/) && do { $input = $PORT_FORMAT_MODPORT;
@ports = map {/^$self->{NAME}\.(\d+)\/(\d+)$/; "$1.$2";} @ports; last; };
warn "convertPortFormat: Unknown input port format: $sample\n";
return ();
}
#
# It's possible the ports are already in the right format
#
if ($input == $output) {
if ($input == $PORT_FORMAT_IFINDEX) {
@results = map $self->checkLACP($_), @ports;
goto done;
}
$self->debug("Not converting, input format = output format\n",3);
return @ports;
}
my $name = $self->{NAME};
if ($input == $PORT_FORMAT_IFINDEX) {
my $ifxModport = sub ($) {
my ($port, $modport) = ($_, $self->{IFINDEX}{$_});
print "$name: no modport for ifindex $port\n" unless ($modport);
return $modport ? $modport : "1.$port";
};
my @modports = map $ifxModport->($_), @ports;
if ($output == $PORT_FORMAT_MODPORT) {
$self->debug("Converting ifindex to modport\n",3);
@results = @modports;
goto done;
} elsif ($output == $PORT_FORMAT_PORT) {