diff --git a/ChangeLog b/ChangeLog index b03fa54d..ff822fc5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -13,6 +13,8 @@ version 3.00 [NEW FEATURES] * [3160037] - Support _raw suffix on methods to skip munging + * [3185391] - Support for F5 devices in new class L3::F5 + * [3599277] - Q-BRIDGE Support to collect VLAN in macsuck * Support for Avaya VSP 9000 series in L3::Passport * Support for Avaya VSP 7000 series in L2::Baystack * Support Avaya (Trapeze) Wireless Controllers in new class L2::NWSS2300 @@ -20,10 +22,12 @@ version 3.00 older AWS 2000/3000 series in existing L3::AlteonAD * Support for H3C & HP A-series in new class L3::H3C * Support for Citrix Netscaler appliances in new class L7::Netscaler - * [3185391] Support for F5 devices in new class L3::F5 * New configuration option IgnoreNetSNMPConf will ignore Net-SNMP configuration files on object initialization - * [3599277] - Q-BRIDGE Support to collect VLAN in macsuck + * Two new utilities added in t/util to assist in developing device + support; make_snmpdata.pl gathers SNMP data (snmpwalk) in a format that + can be used with test_class_mocked.pl which mocks an SNMP agent to + enable testing with no network access to a device. [ENHANCEMENTS] diff --git a/t/util/make_snmpdata.pl b/t/util/make_snmpdata.pl new file mode 100755 index 00000000..81c19827 --- /dev/null +++ b/t/util/make_snmpdata.pl @@ -0,0 +1,207 @@ +#!/usr/bin/perl +# +# make_snmpdata.pl +# +# Copyright (c) 2012 Eric Miller +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of the University of California, Santa Cruz 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR 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. + +use strict; +use warnings; +use Carp; +use Getopt::Long; +use Pod::Usage; +use SNMP; + +local $| = 1; + +my $mibdirs = ['/usr/local/share/snmp/mibs']; +my $comm = 'public'; +my $ver = '2c'; +my $dev; +my $ignore = 0; +my $help = 0; + +GetOptions( + 'community=s' => \$comm, + 'device=s' => \$dev, + 'ignore' => \$ignore, + 'mibdir=s' => \$mibdirs, + 'version' => \$ver, + 'help|?' => sub { pod2usage(2); }, +) or pod2usage(2); + +unless ( defined $dev && $ver =~ /[1|2c]/ ) { + pod2usage(1); +} + +local $ENV{'SNMPCONFPATH'} = '' if $ignore; +local $ENV{'MIBDIRS'} = "$mibdirs" if $ignore; + +SNMP::addMibDirs($mibdirs); + +# Connect to Device +my $sess = SNMP::Session->new( + 'UseEnums' => 1, + 'RetryNoSuch' => 1, + 'DestHost' => $dev, + 'Community' => $comm, + 'Version' => $ver, + 'UseSprintValue' => 1 +); + +my $sysdescr = $sess->get('sysDescr.0'); +unless ( defined $sysdescr ) { + die "Couldn't connect to $dev via snmp.\n"; +} + +SNMP::loadModules(@ARGV); + +# Create a hash of MIB Modules for which we want results +my %mib_hash = map {$_ => 1} @ARGV; +# Add the common MIB Modules we always want +my @common_mibs = ('SNMPv2-MIB', 'IF-MIB'); +foreach my $mib (@common_mibs) { + $mib_hash{$mib} = 1; +} + +foreach my $key ( sort( keys %SNMP::MIB ) ) { + my $module = $SNMP::MIB{$key}{moduleID} || ''; + # IMPORTS pulls in many modules we don't want to walk + # Only walk those we've specified + next unless (defined $mib_hash{$module}); + my $access = $SNMP::MIB{$key}{'access'} || ''; + next unless ( $access =~ /Read|Create/x ); + + my $label = SNMP::translateObj( $key, 0, 1 ) || ''; + snmpwalk($label); +} + +sub snmpwalk { + return unless defined $sess; + my $label = shift; + my $var = SNMP::Varbind->new( [$label] ); + my $e = 0; + my $last_iid = ''; + my %seen = (); + while ( !$e ) { + $sess->getnext($var); + $e = $sess->{ErrorNum}; + + return if $var->[0] ne $label; + my $iid = $var->[1]; + my $val = $var->[2]; + return unless defined $iid; + + # Check to see if we've already seen this IID (looping) + if ( defined $seen{$iid} and $seen{$iid} ) { + warn "Looping on $label iid:$iid. Skipped.\n"; + return; + } + else { $seen{$iid}++; } + + # why is it looping? + return if $last_iid eq $iid; + $last_iid = $iid; + + my $line = "$label.$iid = $val"; + print "$line\n"; + } + return; +} + +__END__ + +=head1 NAME + +make_snmpdata.pl - Tool to get SNMP data for the SNMP::Info testing framework + +=head1 AUTHOR + +Eric Miller + +=head1 SYNOPSIS + +make_snmpdata.pl [options] MIB-MODULE-1 MIB-MODULE-2 + +Options: + + -community SNMP Community + -device IP Address to query + -ignore Ignore Net-SNMP configuration file + -mibdir Directory containing MIB Files + -version SNMP version to use + -help Brief help message + +=head1 OPTIONS + +=over 8 + +=item B<-community> + +SNMP Community, either 1 or 2c. Defaults to version 2c + +-community 2c + +=item B<-device> + +IP Address to query for the SNMP data. No default and a mandatory option. + +-device 127.0.0.1 + +=item B<-ignore > + +Ignore Net-SNMP configuration file snmp.conf. If this used mibdirs must be +provided + +-ignore + +=item B<-mibdir> + +Directory containing MIB Files. Mutiple directories should be separated by a +colon ':'. Defaults to /usr/local/share/snmp/mibs. + +-mibdir /usr/local/share/snmp/mibs/rfc:/usr/local/share/snmp/mibs/net-snmp + +=item B<-version> + +SNMP version to use. Only version 1 and 2c are supported. Defaults to 2c + +-version 2c + +=item B<-help> + +Print help message and exits. + +=back + +=head1 DESCRIPTION + +B will gather SNMP data by walking specified MIB files and +output the data to a file which can be used by the SNMP::Info testing +framework. + +=cut diff --git a/t/util/test_class_mocked.pl b/t/util/test_class_mocked.pl new file mode 100755 index 00000000..92aceecf --- /dev/null +++ b/t/util/test_class_mocked.pl @@ -0,0 +1,453 @@ +#!/usr/bin/perl +# +# test_class_mocked.pl +# +# Copyright (c) 2012 Eric Miller +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of the University of California, Santa Cruz 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR 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. + +use strict; +use warnings; +use Carp; +use FindBin; +use lib "$FindBin::Bin/../../.."; +use File::Slurp qw(slurp); +use Getopt::Long; +use Pod::Usage; +use SNMP::Info; +use Test::MockObject::Extends; + +my $EMPTY = q{}; + +# Default Values +my $class = $EMPTY; +my @dump = (); +my $debug = 0; +my $mibdirs; +my $ignore = 0; +my $help = 0; +my $file; +my %dumped; + +GetOptions( + 'c|class=s' => \$class, + 'i|ignore' => \$ignore, + 'p|print=s' => \@dump, + 'x|debug+' => \$debug, + 'm|mibdir=s' => \$mibdirs, + 'file=s' => \$file, + 'h|?|help' => sub { pod2usage(1); }, +); + +if ( !$file ) { + pod2usage(1); +} + +if ( $ignore && !defined $mibdirs ) { + print "mibdirs must be provided if ignoring snmp.conf \n\n"; + pod2usage(1); +} + +if ($ignore) { local $ENV{'SNMPCONFPATH'} = $EMPTY } +if ($ignore) { local $ENV{'MIBDIRS'} = "$mibdirs" } + +if ( defined $mibdirs ) { + SNMP::addMibDirs($mibdirs); +} + +$class = $class ? "SNMP::Info::$class" : 'SNMP::Info'; + +( my $mod = "$class.pm" ) + =~ s{::}{/}g; # SNMP::Info::Layer3 => SNMP/Info/Layer3.pm +if ( !eval { require $mod; 1; } ) { + croak "Could not load $class. Error Message: $@\n"; +} + +my $class_ver = $class->VERSION(); + +print + "Class $class ($class_ver) loaded from SNMP::Info $SNMP::Info::VERSION.\n"; + +if ( scalar @dump ) { print 'Dumping : ', join( q{,}, @dump ), "\n" } + +my $mocked = create_mock_session(); + +my $dev = $class->new( + 'AutoSpecify' => 0, + 'BulkWalk' => 0, + 'Debug' => $debug, + 'MibDirs' => $mibdirs, + 'Session' => $mocked, +) or die "\n"; + +print 'Detected Class: ', $dev->device_type(), "\n"; +print "Using Class: $class (-c to change)\n"; + +my $layers = $dev->layers(); +my $descr = $dev->description(); + +if ( !defined $layers || !defined $descr ) { + die "Are you sure you specified a file created with make_snmpdata.pl ?\n"; +} + +print "\nFetching base info...\n\n"; + +my @base_fns = qw/vendor model os os_ver description contact location + layers mac serial/; + +foreach my $fn (@base_fns) { + test_global( $dev, $fn ); +} + +print "\nFetching interface info...\n\n"; + +my @fns = qw/interfaces i_type i_ignore i_description i_mtu i_speed i_mac i_up + i_up_admin i_name i_duplex i_duplex_admin i_stp_state + i_vlan i_pvid i_lastchange/; + +foreach my $fn (@fns) { + test_fn( $dev, $fn ); +} + +print "\nFetching VLAN info...\n\n"; + +my @vlan = qw/v_index v_name/; + +foreach my $fn (@vlan) { + test_fn( $dev, $fn ); +} + +print "\nFetching topology info...\n\n"; + +my @topo = qw/c_if c_ip c_port/; + +foreach my $fn (@topo) { + test_fn( $dev, $fn ); +} + +print "\nFetching module info...\n\n"; + +my @modules = qw/e_descr e_type e_parent e_name e_class e_pos e_hwver + e_fwver e_swver e_model e_serial e_fru/; + +foreach my $fn (@modules) { + test_fn( $dev, $fn ); +} + +foreach my $fn (@dump) { + if ( !$dumped{$fn} ) { test_fn( $dev, $fn ) } +} + +#-------------------------------- + +sub load_snmpdata { + my $data_file = shift; + + my @lines = slurp($data_file); + + my $snmp_data = {}; + foreach my $line (@lines) { + next if !$line; + next if ( $line =~ /^#/ ); + if ( $line =~ /^(\S+::\w+)[.]?(\S+)*\s=\s(.*)$/ ) { + my ( $leaf, $iid, $val ) = ( $1, $2, $3 ); + next if !$leaf; + $iid ||= 0; + $val =~ s/\"//g; + $snmp_data->{$leaf}->{$iid} = $val; + } + } + return $snmp_data; +} + +sub create_mock_session { + + my $snmp_data = load_snmpdata($file); + + my $session = SNMP::Session->new( + UseEnums => 1, + RetryNoSuch => 1, + Data => $snmp_data, + DestHost => '127.0.0.1', + Community => 'public', + Version => 2, + ); + + my $mock_session = Test::MockObject::Extends->new($session); + + mock_get($mock_session); + mock_getnext($mock_session); + + return $mock_session; +} + +sub mock_get { + my $mock_session = shift; + + $mock_session->mock( + 'get', + sub { + my $self = shift; + my $vars = shift; + my ( $leaf, $iid, $oid, $oid_name ); + my $c_data = $self->{Data}; + + # From SNMP::Info get will only be passed either an OID or + # SNMP::Varbind with a fully qualified leaf and potentially + # a partial + if ( ref($vars) =~ /SNMP::Varbind/ ) { + ( $leaf, $iid ) = @{$vars}; + } + else { + $oid = $vars; + $oid_name = SNMP::translateObj( $oid, 0, 1 ) || $EMPTY; + ( $leaf, $iid ) = $oid_name =~ /^(\S+::\w+)[.]?(\S+)*$/; + } + + $iid ||= 0; + my $new_iid = $iid; + my $val = $EMPTY; + my $data = $c_data->{$leaf} || {}; + my $count = scalar keys %{$data} || 0; + if ( $count > 1 ) { + my $found = 0; + foreach my $d_iid ( sort keys %{$data} ) { + if ( $d_iid eq $iid ) { + $val = $data->{$d_iid}; + $found = 1; + next; + } + elsif ( $found == 1 ) { + $new_iid = $d_iid; + last; + } + } + if ( $found && ( $new_iid eq $iid ) ) { + $leaf = 'unknown'; + } + } + else { + $val = $data->{$iid}; + $leaf = 'unknown'; + } + + if ( ref $vars =~ /SNMP::Varbind/ ) { + $vars->[0] = $leaf; + $vars->[1] = $new_iid; + $vars->[2] = $val; + } + return ( wantarray() ? $vars : $val ); + } + ); + return; +} + +sub mock_getnext { + my $mock_session = shift; + + $mock_session->mock( + 'getnext', + sub { + my $self = shift; + my $vars = shift; + my ( $leaf, $iid, $oid, $oid_name ); + my $c_data = $self->{Data}; + + # From SNMP::Info getnext will only be passed a SNMP::Varbind + # with a fully qualified leaf and potentially a partial + ( $leaf, $iid ) = @{$vars}; + + $iid ||= 0; + my $new_iid = $iid; + my $val = $EMPTY; + my $data = $c_data->{$leaf}; + my $count = scalar keys %{$data} || 0; + if ( $count > 1 ) { + my $found = 0; + foreach my $d_iid ( sort keys %{$data} ) { + if ( $d_iid gt $iid && !$found ) { + $val = $data->{$d_iid}; + $new_iid = $d_iid; + $found = 1; + next; + } + elsif ( $found == 1 ) { + last; + } + } + if ( $found && ( $new_iid eq $iid ) ) { + $leaf = 'unknown'; + } + } + else { + $val = $data->{$iid}; + $leaf = 'unknown'; + } + + $vars->[0] = $leaf; + $vars->[1] = $new_iid; + $vars->[2] = $val; + return ( wantarray() ? $vars : $val ); + } + ); + return; +} + +sub test_global { + my $device = shift; + my $method = shift; + + my $value = $device->$method(); + + if ( !defined $value ) { + printf "%-20s Does not exist.\n", $method; + return 0; + } + $value =~ s/[[:cntrl:]]+/ /g; + if ( length $value > 60 ) { + $value = substr $value, 0, 60; + $value .= '...'; + } + printf "%-20s %s \n", $method, $value; + return 1; +} + +sub test_fn { + my $device = shift; + my $method = shift; + + my $results = $device->$method(); + + # If accidentally called on a global, pass it along nicely. + if ( defined $results && !ref $results ) { + return test_global( $dev, $method ); + } + if ( !defined $results && !scalar keys %{$results} ) { + printf "%-20s Empty Results.\n", $method; + return 0; + } + + printf "%-20s %d rows.\n", $method, scalar keys %{$results}; + if ( grep {/^$method$/} @dump ) { + $dumped{$method} = 1; + foreach my $iid ( keys %{$results} ) { + print " $iid : "; + if ( ref( $results->{$iid} ) eq 'ARRAY' ) { + print '[ ', join( ', ', @{ $results->{$iid} } ), ' ]'; + } + else { + print $results->{$iid}; + } + print "\n"; + } + } + return 1; +} + +__END__ + +=head1 NAME + +test_class_mocked.pl - Test a device against an SNMP::Info class using +output from make_snmpdata.pl stored in a text file. + +=head1 AUTHOR + +Eric Miller + +=head1 SYNOPSIS + +test_class_mocked.pl [options] + +Options: + + -class SNMP::Info class to use, Layer2::Catalyst + -file File containing data gathered using make_snmpdata.pl + -print Print values + -debug Debugging flag + -ignore Ignore Net-SNMP configuration file + -mibdir Directory containing MIB Files + -help Brief help message + +=head1 OPTIONS + +=over 8 + +=item B<-class> + +Specific SNMP::Info class to use. Defaults to SNMP::Info if no specific +class provided. + +-class Layer2::Catalyst + +=item B<-file> + +File containing data gathered using make_snmpdata.pl. No default and a +mandatory option. + +-file /data/mydevice.txt + +=item B<-print> + +Print values of a class method rather than summarizing. May be repeated +multiple times. + +-print i_description -print i_type + +=item B<-debug> + +Turns on SNMP::Info debug. + +-debug + +=item B<-ignore > + +Ignore Net-SNMP configuration file snmp.conf. If this used mibdirs must be +provided. + +-ignore + +=item B<-mibdir> + +Directory containing MIB Files. Multiple directories should be separated by a +colon ':'. + +-mibdir /usr/local/share/snmp/mibs/rfc:/usr/local/share/snmp/mibs/net-snmp + +=item B<-help> + +Print help message and exits. + +=back + +=head1 DESCRIPTION + +B will test a device against an SNMP::Info class using +snmpwalk output from the utility B stored in a text file. +This allows debugging and testing without requiring network access to the +device being tested. + +=cut