######################################################################### # package NetworkTools.pm # This Perl module contains functions which ask switches & routers # questions # V Who When What # --------------------------------------------------------------------------- # 2.4.2 skendric 07-03-2008 Wrap NetAddr::IP calls with eval # 2.4.1 skendric 07-02-2008 More debugging # 2.4.0 skendric 06-30-2008 Move extract_ routines to SwitchTools # 2.3.0 skendric 06-16-2008 Move walk_ routines to IPSpace.pm # 2.2.1 skendric 06-12-2008 Handle thread crash in walk_route_table # 2.2.0 skendric 06-08-2008 Rewrite to support Netops-style snmp routines # 2.1.0 skendric 05-30-2008 Clean-up hacks # 2.0.8 skendric 05-30-2008 Accept both names & IP addr for switch, router, # WAP targets # 2.0.7 skendric 05-21-2008 Support more models in flag_uplink # 2.0.6 skendric 03-12-2007 Stylistic mods # 2.0.5 skendric 09-26-2006 More debugging # 2.0.4 skendric 09-23-2006 Support Cat 355x/49xx/65xx at the access layer # 2.0.3 skendric 07-11-2006 Employ Net::IPAddress to validate IP addresses # 2.0.2 skendric 09-24-2005 Employ List::MoreUtils 'any' and 'uniq' # 2.0.1 skendric 09-11-2005 Add hack to skip SCCA router interfaces # 2.0.0 skendric 08-25-2005 Add acquire_port_status # 1.9.4 skendric 08-23-2005 Debugging code in process_arp # 1.9.3 skendric 07-09-2005 Change skip_nodes to skip_routers # 1.9.2 skendric 06-23-2005 Support new %routeTable hash # 1.9.1 skendric 06-12-2005 Enhance process_arp to include interface # identifiers in %arp; generalize # acquire_route_table, more error handling, # check subroutine parameters, add skip_nodes # 1.9.0 skendric 02-25-2005 Add walk_nessus_routes # 1.8.0 skendric 02-17-2005 Add build_ports # 1.7.7 skendric 02-13-2005 Add route exclusion to walk_route_table # 1.7.6 skendric 02-06-2005 Convert %hostname to %nodename # 1.7.5 skendric 01-03-2005 Add error checking to prune_esx/rtr and # flag_uplink # 1.7.4 skendric 10-05-2004 Fixed bug in prune_esx # 1.7.3 skendric 09-02-2004 Retry switches which fail process_cam # 1.7.2 skendric 09-01-2004 Fix pinging routines # 1.7.1 skendric 08-29-2004 Refine debugging # 1.7.0 skendric 08-28-2004 Add thread/serial modes # 1.6.1 skendric 08-26-2004 More debugging output # 1.6.0 skendric 08-16-2004 Add normalize_mac # 1.5.0 skendric 07-19-2004 Add describe_switch # 1.4.1 skendric 07-14-2004 Fix bugs in acquire_cam_table # 1.4.0 skendric 07-13-2004 Add acquire_cam_table # 1.3.0 skendric 07-02-2004 Add acquire_arp_table # 1.2.0 skendric 06-19-2004 Add build_packet_transport # 1.1.0 skendric 06-18-2004 Add esx routines, employ NetAddr::IP # 1.0.0 skendric 06-06-2004 First version # # # # Authors: Stuart Kendrick # # Source: http://www.skendric.com/device/soma # # This software is available under the GNU GENERAL PUBLIC LICENSE, see # http://www.fsf.org/licenses/gpl.html # package FHCRC::VDOPS::NetworkTools; #### Load modules #### use strict; use warnings; use threads; use threads::shared; use Carp qw(carp cluck croak confess); use Data::Dumper; use Exporter; use List::MoreUtils qw(all any notall none uniq); use NetAddr::IP; use Net::IPAddress qw(validaddr); use Perl6::Say; use Socket; use Switch; use Thread::Running qw(running); use lib '/home/soma/lib'; use FHCRC::VDOPS::HostTools; use FHCRC::VDOPS::PingTools; use FHCRC::VDOPS::SomaData; use FHCRC::VDOPS::SNMPTools; use FHCRC::VDOPS::SwitchTools; use FHCRC::VDOPS::Utilities; #### Set-up export stuff #### our @ISA = qw(Exporter); our @EXPORT = qw( acquire_arp_table acquire_cam_table acquire_if_admin_status acquire_route_table acquire_wap_clients build_ports build_routers build_switches build_waps ); # Declare package local variables ##### Only subroutines below here #### ####################################################################### # Acquire ARP table. If we are handed an argument, focus on only that # router (useful for debugging). ######################################################################## sub acquire_arp_table { my $target = shift; # If this is populated, I will process # just this router; normally, I process # all routers in %rtr my $thr; my @threads; # List of thread objects I've spawned my %threads; # Hash of thread objects I've spawned, # keyed by rtr my %tid; # Hash of thread identifiers I've spawned, # keyed by esx my (@addr, @rtr); # Unrolled, coupled versions of %rtr # Debug trace trace_location('begin') if $debug; # If we're focussing, grab the ARP table from this one router if (defined $target) { # If we've been given a name, convert to IP address unless (validaddr($target)) { $target = inet_ntoa(scalar gethostbyname($target)); } print_it("Spawning process_arp for $target\n") if $debug > 1; $thr = threads->new(\&process_arp, $target); push @threads, $thr; $threads{$target} = $thr; $tid{$target} = $thr->tid; $thr->detach; sleep $processArpTimeout * 10; } # Otherwise, grab the ARP table from each router else { # If we are in thread mode, process each router in parallel if ($thrMode == 1) { for my $addr (sort by_ip keys %rtr) { print_it("Spawning process_arp for $rtr{$addr}\n") if $debug > 1; $thr = threads->new(\&process_arp, $addr); push @threads, $thr; $threads{$addr} = $thr; $tid{$addr} = $thr->tid; $thr->detach; sleep 1; } # Wait for threads to complete say 'Waiting for router threads to complete' if $debug > 1; sleep $processArpTimeout * 10; wait_for_threads(\@threads, $processArpTimeout * 5); # If any threads are still running, figure out which ones they are # and log the event if (threads->running( @threads )) { my ($addr, %rev_tid, @running); %rev_tid = reverse %tid; @running = threads->running( @threads ); for my $thr (@running) { $addr = $rev_tid{$thr}; print_it("$rtr{$addr} did not return an ARP table\n"); } } } # End 'process each router in parallel' # Otherwise, process each router serially else { # Walk through routers for my $addr (sort by_ip keys %rtr) { my $rtr = $rtr{$addr}; eval { local $SIG{ALRM} = sub { print_it("process_arp for $rtr did not complete\n"); die 'alarm clock restart'; }; alarm $processArpTimeout * 10; # schedule the alarm eval { print_it("Running process_arp on $rtr\n") if $debug; process_arp($addr); }; alarm 0; # cancel the alarm }; alarm 0; # race condition protection die if ($@ and $@ !~ /alarm clock restart/); # reraise } # End 'Walk through routers' } # End 'process each router serially' } # End 'grab the ARP table from each router' # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Acquire CAM table. If we are handed an argument, focus on only that # switch, ignoring the rest (useful for debugging). ######################################################################## sub acquire_cam_table { my @failed; # List of switches which did not return # a CAM table my $oldProcessCamTimeout; # Temp variable my $old_snmp_timeout; # Temp variable my $target = shift; # If this is populated, I will process # just this switch; normally, I will # process all switches in %esx my $thr; my @threads; # List of thread objects I've spawned my %threads; # Hash of thread objects I've spawned, # keyed by esx my %tid; # Hash of thread identifiers I've spawned, # keyed by esx # Debug trace trace_location('begin') if $debug; # If we are focussing, grab the CAM table from this one switch if (defined $target) { $target = get_ipaddr($target) unless validaddr($target); confess 'Something is wrong with first argument' unless defined $target; say "Focussing: acquire_cam_table on $target" if $debug > 1; $cam{$target} = process_cam($target); } # Otherwise, grab the CAM table from each switch else { # If we are in thread mode, process each switch in parallel if ($thrMode == 1) { # Spawn a thread for each switch for my $addr (sort by_ip keys %esx) { say "Spawning process_cam for $esx{$addr}" if $debug > 1; $thr = threads->new(\&process_cam, $addr); push @threads, $thr; $threads{$addr} = $thr; $tid{$addr} = $thr->tid; sleep 1; } # Wait for threads to complete say 'Waiting for switch threads to complete' if $debug > 1; sleep $processCamTimeout * 10; wait_for_threads(\@threads, $processCamTimeout * 5); # Now that we've waited a while, join whatever threads have finished, # harvesting their return value say 'Join switch threads' if $debug > 1; for my $addr (sort by_ip keys %esx) { if (threads->tojoin($threads{$addr})) { $cam{$addr} = $threads{$addr}->join; } } # If any threads are still running, figure out which ones they are # and log the event if (threads->running( @threads )) { my ($addr, %rev_tid, @running); %rev_tid = reverse %tid; @running = threads->running( @threads ); for my $thr (@running) { $addr = $rev_tid{$thr}; print_it("$esx{$addr} did not return a CAM table\n"); push @failed, $addr; } } } # End 'process each switch in parallel' # Otherwise, process each switch serially else { # Walk through switches for my $addr (sort by_ip keys %esx) { my $esx = $esx{$addr}; eval { local $SIG{ALRM} = sub { print_it "process_cam for $esx did not complete\n"; push @failed, $addr; die 'alarm clock restart'; }; alarm $processCamTimeout * 10; # schedule the alarm eval { print_it("Running process_cam on $esx\n") if $debug; $cam{$addr} = process_cam($addr); }; alarm 0; # cancel the alarm }; alarm 0; # race condition protection die if ($@ and $@ !~ /alarm clock restart/); # reraise } # End 'Walk through switches' } # End 'process each switch serially' } # End 'grab the CAM table from each switch' # Try the failed switches again if (@failed) { print_it("Retrying switches which failed to return a CAM table on the first pass\n"); # Save old timeouts $old_snmp_timeout = $snmp_timeout; $oldProcessCamTimeout = $processCamTimeout; # Try longer timeouts $processCamTimeout = $processCamTimeout * 2; $snmp_timeout = $snmp_timeout * 2; # Walk failed switches for my $addr (@failed) { eval { local $SIG{ALRM} = sub { print_it ("process_cam for $esx{$addr} did not complete on the second pass\n"); die 'alarm clock restart'; }; alarm $processCamTimeout * 10; # schedule the alarm eval { print_it("Running process_cam on $esx{$addr}\n") if $debug; $cam{$addr} = process_cam($addr); }; alarm 0; # cancel the alarm }; alarm 0; # race condition protection die if ($@ and $@ !~ /alarm clock restart/); # reraise } # End 'for my $addr (@failed)' loop # Restore timeouts $processCamTimeout = $oldProcessCamTimeout; $snmp_timeout = $old_snmp_timeout; } # End 'if @failed' # Debug info if ($debug == 4) { for my $esx (sort keys %cam) { say "\n$esx: dumping CAM table"; for my $cam (sort keys %{$cam{$esx}}) { say "$cam lives on $cam{$esx}->{$cam}"; } } } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Acquire ifAdminStatus. If we are handed an argument, focus on only # that switch, ignoring the rest (useful for debugging). ######################################################################## sub acquire_if_admin_status { my @failed; # List of switches which did not return # results my $oldProcessIfAdminTimeout; # Temp variable my $old_snmp_timeout; # Temp variable my $target = shift; # If this is populated, I will process # just this switch; normally, I will # process all switches in %esx my $thr; my @threads; # List of thread objects I've spawned my %threads; # Hash of thread objects I've spawned, # keyed by esx my %tid; # Hash of thread identifiers I've spawned, # keyed by esx my (@addr, @esx); # Unrolled, coupled versions of %esx # Debug trace trace_location('begin') if $debug; # If we are focussing, grab the ifAdminStatus table from this one switch if (defined $target) { $target = get_ipaddr($target) unless validaddr($target); confess "Something is wrong with first argument" unless defined $target; say "Focussing: acquire_if_admin_status on $target" if $debug > 1; $ifAdminStatus{$target} = process_if_admin_status($target); } # Otherwise, grab the ifAdminStatus table from each switch else { # If we are in thread mode, process each switch in parallel if ($thrMode == 1) { # Spawn a thread for each switch for my $addr (sort by_ip keys %esx) { say "Spawning process_if_admin_status for $esx{$addr}" if $debug > 1; $thr = threads->new(\&process_if_admin_status, $addr); push @threads, $thr; $threads{$addr} = $thr; $tid{$addr} = $thr->tid; sleep 1; } # Wait for threads to complete say 'Waiting for switch threads to complete' if $debug > 1; sleep $processIfAdminTimeout * 10; wait_for_threads(\@threads, $processIfAdminTimeout * 5); # Now that we've waited a while, join whatever threads have finished, # harvesting their return value say 'Join switch threads' if $debug > 1; for my $addr (sort by_ip keys %esx) { if (threads->tojoin($threads{$addr})) { $ifAdminStatus{$addr} = $threads{$addr}->join; } } # If any threads are still running, figure out which ones they are # and log the event if (threads->running( @threads )) { my ($addr, %rev_tid, @running); %rev_tid = reverse %tid; @running = threads->running( @threads ); for my $thr (@running) { $addr = $rev_tid{$thr}; print_it("$esx{$addr} did not return an IfAdminStatus table\n"); push @failed, $addr; } } } # End 'process each switch in parallel' # Otherwise, process each switch serially else { # Walk through switches for my $addr (sort by_ip keys %esx) { my $esx = $esx{$addr}; eval { local $SIG{ALRM} = sub { print_it "process_if_admin_status for $esx did not complete\n"; push @failed, $addr; die 'alarm clock restart'; }; alarm $processIfAdminTimeout * 10; # schedule the alarm eval { print_it("Running process_if_admin_status on $esx\n") if $debug; $ifAdminStatus{$addr} = process_if_admin_status($addr); }; alarm 0; # cancel the alarm }; alarm 0; # race condition protection die if ($@ and $@ !~ /alarm clock restart/); # reraise } # End 'Walk through switches' } # End 'process each switch serially' } # End 'grab the ifAdminStatus table from each switch' # Try the failed switches again if (@failed) { print_it("Retrying switches which failed to return an IfAdminStatus table on the first pass\n"); # Save old timeouts $old_snmp_timeout = $snmp_timeout; $oldProcessIfAdminTimeout = $processIfAdminTimeout; # Try longer timeouts $processIfAdminTimeout = $processIfAdminTimeout * 2; $snmp_timeout = $snmp_timeout * 2; # Walk failed switches for my $addr (@failed) { eval { local $SIG{ALRM} = sub { print_it ("process_if_admin_status for $esx{$addr} did not complete on the second pass\n"); die 'alarm clock restart'; }; alarm $processIfAdminTimeout * 10; # schedule the alarm eval { print_it("Running process_if_admin_status on $esx{$addr}\n") if $debug; $ifAdminStatus{$addr} = process_if_admin_status($addr); }; alarm 0; # cancel the alarm }; alarm 0; # race condition protection die if ($@ and $@ !~ /alarm clock restart/); # reraise } # End 'for my $addr (@failed)' loop # Restore timeouts $processIfAdminTimeout = $oldProcessIfAdminTimeout; $snmp_timeout = $old_snmp_timeout; } # End 'if @failed' # Debug info if ($debug == 4) { for my $esx (sort by_ip keys %ifAdminStatus) { say "\n$esx: dumping ifAdminStatus table"; for my $ifName (sort keys %{$ifAdminStatus{$esx}}) { say "$ifName = $ifAdminStatus{$esx}->{$ifName}"; } } } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Given a router name, walk ipRouteMask and return a reference to a # hash of subnet masks keyed by route ######################################################################## sub acquire_route_table { my %arg; my %localMask; my $router = shift; my $val; # Reference to an array of Varbinds # Debug trace trace_location('begin') if $debug; # Sanity check confess 'No parameters' unless defined $router; # Grab the routing table say 'Walking ipRouteMask' if $debug > 3; %arg = ( host => $router, oid => $ipRouteMask_oid ); $val = snmpWalk(\%arg); print_it("Walk of ipRouteMask on $router is empty") unless defined $val; # Walk the routing table ROUTE: for my $varbind (@$val) { my ($flag, $mask, $route, $route_obj); $flag = 0; # Decompose route and mask $route = $varbind->{iid}; $mask = $varbind->{val}; # None of these routes lead to subnets containing end-stations, # so ignore them next ROUTE unless (defined $route and defined $mask); next ROUTE if $route eq '0.0.0.0'; next ROUTE if $route eq '127.0.0.0'; next ROUTE if $mask eq '255.255.255.252'; next ROUTE if $mask eq '255.255.255.254'; next ROUTE if $mask eq '255.255.255.255'; # Create NetAddr::IP object eval { $route_obj = NetAddr::IP->new($route, $mask) }; if ($@ or not defined $route_obj) { log_it("Bad route $route / $mask: $@"); next ROUTE; } # Ignore unless the route is contained within one of %includeRoute NETWORK: for my $network (sort by_ip keys %includeRoute) { my $netmask = $includeRoute{$network}; if ( $route_obj->within(NetAddr::IP->new($network, $netmask)) ) { $flag = 1; last NETWORK; } } next ROUTE unless $flag; # Build hash $localMask{$route} = $mask; } # Debug info if ($debug > 2) { for my $key (sort keys %localMask) { say "localMask{$key} = $localMask{$key}"; } } # Debug trace trace_location('end') if $debug; return \%localMask; } ####################################################################### # Acquire VLAN list ######################################################################## sub acquire_vlan_list { my $thr; my @threads; # List of thread objects I've spawned # Debug trace trace_location('begin') if $debug; # Grab vtpVlanState from each switch for my $esx (sort by_ip keys %esx) { say "Spawning process_vlan thread on $esx" if $debug > 1; $thr = threads->new(\&process_vlan, $esx); push @threads, $thr; $thr->detach; } # Wait for threads to complete say 'Waiting for switch threads to complete' if $debug > 1; sleep $processVlanTimeout; wait_for_threads(\@threads, $processVlanTimeout); # Debug trace trace_location('begin') if $debug; return 1; } ####################################################################### # Acquire cd11IfAssignedSta, which is Cisco's way of reporting the MAC # addresses of clients associated with a given WAP. If we are handed an # argument, focus on only that WAP, ignoring the rest (useful for debugging). ######################################################################## sub acquire_wap_clients { my @failed; # List of switches which did not return # results my $oldProcessWAPTimeout; # Temp variable my $old_snmp_timeout; # Temp variable my $target = shift; # If this is populated, I will process # just this switch; normally, I will # process all switches in %wap my $thr; my @threads; # List of thread objects I've spawned my %threads; # Hash of thread objects I've spawned, # keyed by wap my %tid; # Hash of thread identifiers I've spawned, # keyed by wap my (@addr, @wap); # Unrolled, coupled versions of %wap # Debug trace trace_location('begin') if $debug; # If we are focussing, grab the cd11IfAssignedSta table from this one WAP if (defined $target) { $target = get_ipaddr($target) unless validaddr($target); confess 'Something is wrong with first argument' unless defined $target; say "Focussing: acquire_wap_clients on $target" if $debug > 1; $wapClients{$target} = process_wap_clients($target); } # Otherwise, grab the cd11IfAssignedSta table from each WAP else { # If we are in thread mode, process each WAP in parallel if ($thrMode == 1) { # Spawn a thread for each WAP for my $addr (sort by_ip keys %wap) { say "Spawning process_wap_clients for $wap{$addr}" if $debug > 1; $thr = threads->new(\&process_wap_clients, $addr); push @threads, $thr; $threads{$addr} = $thr; $tid{$addr} = $thr->tid; sleep 1; } # Wait for threads to complete say 'Waiting for WAP threads to complete' if $debug > 1; sleep $processWAPTimeout * 10; wait_for_threads(\@threads, $processWAPTimeout * 5); # Now that we've waited a while, join whatever threads have finished, # harvesting their return value say 'Join wap threads' if $debug > 1; for my $addr (sort by_ip keys %wap) { if (threads->tojoin($threads{$addr})) { $wapClients{$addr} = $threads{$addr}->join; } } # If any threads are still running, figure out which ones they are # and log the event if (threads->running( @threads )) { my ($addr, %rev_tid, @running); %rev_tid = reverse %tid; @running = threads->running( @threads ); for my $thr (@running) { $addr = $rev_tid{$thr}; print_it("$wap{$addr} did not return a cd11IfAssignedSta table\n"); push @failed, $addr; } } } # End 'process each WAP in parallel' # Otherwise, process each WAP serially else { # Walk through WAPs for my $addr (sort by_ip keys %wap) { my $wap; $wap = $wap{$addr}; eval { local $SIG{ALRM} = sub { print_it("process_wap_clients for $wap did not complete\n"); push @failed, $addr; die 'alarm clock restart'; }; alarm $processWAPTimeout * 10; # schedule the alarm eval { print_it("Running process_wap_clients on $wap\n") if $debug; $wapClients{$addr} = process_wap_clients($addr); }; alarm 0; # cancel the alarm }; alarm 0; # race condition protection die if ($@ and $@ !~ /alarm clock restart/); # reraise } # End 'Walk through WAPs' } # End 'process each switch serially' } # End 'grab the cd11IfAssignedSta table from each WAP' # Try the failed WAPs again if (@failed) { print_it("Retrying WAPs which failed to return an IfAdminStatus table on the first pass\n"); # Save old timeouts $old_snmp_timeout = $snmp_timeout; $oldProcessWAPTimeout = $processWAPTimeout; # Try longer timeouts $processWAPTimeout = $processWAPTimeout * 2; $snmp_timeout = $snmp_timeout * 2; # Walk failed WAPs for my $addr (@failed) { eval { local $SIG{ALRM} = sub { print_it("process_wap_clients for $wap{$addr} did not complete on the second pass\n"); die 'alarm clock restart'; }; alarm $processWAPTimeout * 10; # schedule the alarm eval { print_it("Running process_wap_clients on $esx{$addr}\n") if $debug; $wapClients{$addr} = process_wap_clients($addr); }; alarm 0; # cancel the alarm }; alarm 0; # race condition protection die if ($@ and $@ !~ /alarm clock restart/); # reraise } # End 'for my $addr (@failed)' loop # Restore timeouts $processWAPTimeout = $oldProcessWAPTimeout; $snmp_timeout = $old_snmp_timeout; } # End 'if @failed' # Debug info if ($debug == 4) { for my $addr (sort by_ip keys %wapClients) { say "For $wap{$addr}: dumping cd11IfAssignedSta table"; say join($CR, @{$wapClients{$addr}}); } } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Build %ports by walking %esx ######################################################################## sub build_ports { my (@addr, @esx); # Unrolled, coupled versions of %esx # Debug trace trace_location('begin') if $debug; # Walk through switches for my $addr (sort by_ip keys %esx) { my $esx = $esx{$addr}; # Acquire slot/port data structure $ports{$addr} = describe_switch($addr); } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Identify distribution-layer routers, figure out how to speak SNMP to # them, populate various hashes. If handed an argument, focus on that # one router and ignore the rest (useful for debugging). ######################################################################## sub build_routers { my $router = shift; # Debug trace trace_location('begin') if $debug; # Sanity check if (defined $router) { my ($ip, $name); if (validaddr($router)) { $ip = $router; $name = get_nodename($ip); } else { $name = $router; $ip = get_ipaddr($name); } confess "Cannot resolve $router to a name" unless defined $name; confess "Cannot resolve $router to an address" unless defined $ip; $nodename{$ip} = $name; $router = $name; } # Debug info say "Focussing on $router" if defined $router; # Build list of devices populate_rtr($router); # Figure out how to speak SNMP to these devices char_device(\%rtr); # Remove devices which don't interest us prune_rtr(); # Debug info if ($debug > 1) { print_it("I will use this list of routers:\n"); for my $ip (sort by_ip keys %rtr) { print_it("$rtr{$ip} = $ip\n"); } } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Identify access-layer switches, figure out how to speak SNMP to # them, populate various hashes. If handed an argument, then focus # on that one switch and ignore the rest (useful for debugging). ######################################################################## sub build_switches { my $switch = shift; # Debug trace trace_location('begin') if $debug; # Sanity check if (defined $switch) { my ($ip, $name); if (validaddr($switch)) { $ip = $switch; $name = get_nodename($ip); } else { $name = $switch; $ip = get_ipaddr($name); } confess "Cannot resolve $switch to a name" unless defined $name; confess "Cannot resolve $switch to an address" unless defined $ip; $nodename{$ip} = $name; $switch = $name; } # Debug info say "Focussing on $switch" if defined $switch; # Build lists of devices populate_esx($switch); # Figure out how to speak SNMP to these devices char_device(\%esx); # Remove devices which don't interest us prune_esx(); # Debug info if ($debug > 1) { print_it("I will use this list of switches:\n"); for my $ip (sort by_ip keys %esx) { print_it("$esx{$ip} = $ip\n"); } } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Identify wireless access points, figure out how to speak SNMP to # them, populate various hashes. If handed an argument, then focus # on that one WAP and ignore the rest (useful for debugging). ######################################################################## sub build_waps { my $wap = shift; # Debug trace trace_location('begin') if $debug; # Sanity check if (defined $wap) { my ($ip, $name); if (validaddr($wap)) { $ip = $wap; $name = get_nodename($ip); } else { $name = $wap; $ip = get_ipaddr($name); } confess "Cannot resolve $wap to a name" unless defined $name; confess "Cannot resolve $wap to an address" unless defined $ip; $nodename{$ip} = $name; $wap = $name; } # Debug info say "Focussing on $wap\n" if defined $wap; # Build lists of devices populate_wap($wap); # Figure out how to speak SNMP to these devices char_device(\%wap); # Remove devices which don't interest us prune_wap(); # Debug info if ($debug > 1) { print_it("I will use this list of WAPs:\n"); for my $ip (sort by_ip keys %wap) { print_it("$wap{$ip} = $ip\n"); } } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Sort IP addresses ######################################################################## sub by_ip { my ($a1, $a2, $a3, $a4) = split ('\.', $a); my ($b1, $b2, $b3, $b4) = split ('\.', $b); if (($a1 <=> $b1) != 0) { $a1 <=> $b1 } elsif (($a2 <=> $b2) != 0) { $a2 <=> $b2 } elsif (($a3 <=> $b3) != 0) { $a3 <=> $b3 } elsif (($a4 <=> $b4) != 0) { $a4 <=> $b4 } } ######################################################################## # Given an IP address, return the next IP address which answers a ping. # If we reach the end of the subnet, return undef. ######################################################################## sub get_next_ip { my $answer; # 0 or 1, depending on the result of the ping my $ip; # NetAddr::IP object my $lastAddr = shift; # The address which our caller gives us my $lastOctet; # Last field of IP address my ($route, $mask); # route and subnet mask containing $lastIP my $target; # The address we hand to the pinging routine # Debug trace trace_location('begin') if $debug > 2; # Sanity check confess 'Not enough parameters' unless defined $lastAddr; confess "$lastAddr not an IP address" unless validaddr($lastAddr); # Find route and subnet mask associated with $lastAddr ($route, $mask) = route_and_mask($lastAddr); unless (defined $route and defined $mask) { confess "Unable to slot $lastAddr into route table"; } # Initialize variables eval { $ip = NetAddr::IP->new($lastAddr, $mask) }; if ($@ or not defined $ip) { log_it("Bad addr $lastAddr / $mask: $@"); goto END; } $target = 'initialized'; # OK, we've already pinged $lastIP, so increment to the next address $ip++; # Loop until we receive an ICMP Response or until we leave the subnet # I haven't figured out a clean way to write this loop ... IP: for (my $i = 0; $i < 65537; $i++) { # Reset flag-type variables $answer = 0; # If we've reached the broadcast address, then bail undef $target if $ip eq $ip->broadcast; last IP if $ip eq $ip->broadcast; # Strip the "/32" part off ($target) = ($ip =~ /(^.*)\//); # Ping that puppy $answer = ping_it($target); # If that puppy answered, populate %nodename if ($answer == 1) { $nodename{$target} = gethostbyaddr(inet_aton($target), AF_INET); say "$target is alive" if $debug > 2; last IP; } # Otherwise, increment $ip else { $ip++; say "$target is dead" if $debug > 2; } } # End 'for' loop # If $target is defined, then it contains the IP address of the node # which first responded to a ping. If $target is undef, then we # reached the end of the subnet END: # Debug trace trace_location('end') if $debug > 2; # Return response return $target; } ####################################################################### # Given an IP route, get the next one. If the caller doesn't give us # a route, then return the 'first' one in the routing table ######################################################################## sub get_next_route { my $flag; my $previousRoute = shift; my $nextRoute; # Debug trace trace_location('begin') if $debug > 1; # Initialize $flag $flag = 0; # Walk through route table ROUTE: for my $route (sort by_ip keys %routeTable) { # If the caller gave us a starting point if (defined $previousRoute) { # If the starting point the caller gave us exists in the route table if (exists $routeTable{$previousRoute}) { # Walk through the routing table until we find $previousRoute. # When we do, set $flag ... the next time we pass through the # loop, we'll grab the $nextRoute ... and then see that $flag is # set and exit the loop, with $nextRoute happily populated $nextRoute = $route; last ROUTE if $flag; $flag = 1 if $route =~ /$previousRoute/; } # If the starting point the caller gave us does not exist in the # route table ... then I'm not sure what is going on. So I'll # just leave $nextRoute undefined and quit else { print_it("$previousRoute not in routing table --> unable to find nextRoute\n"); last ROUTE; } } # If the caller didn't give us a starting point, then return the # 'first' route in the route table else { $nextRoute = $route; $flag = 1; last ROUTE; } } # End of for loop # If we couldn't find a nextRoute, return undef if ($flag == 0) { undef $nextRoute; } # If nextRoute and previousRoute are the same, we've reached the end # of the routing table --> return undef elsif (defined $previousRoute) { undef $nextRoute if $nextRoute eq $previousRoute; } # Debug info if ($debug > 1) { if (defined $nextRoute) { say "nextRoute = $nextRoute"; } else { say 'nextRoute is undefined'; } } # Debug trace trace_location('end') if $debug > 1; return $nextRoute; } ####################################################################### # Populate %esx. If we are handed an argument, then focus only on that # device (useful for debugging). ######################################################################## sub populate_esx { my $esx; # Reference to an array of switches my $target = shift; # Debug trace trace_location('begin') if $debug; # If we're focussing, hand-pick what goes into $esx if (defined $target) { my @array; $array[0] = gethostbyaddr(inet_aton($target), AF_INET); $esx = \@array; } # Otherwise, fill $esx with as much as we can else { $esx = build_host_list(@esx_suffixes); } # Build %esx for my $esx (@$esx) { if (ping_it($esx)) { my $addr = join($DOT => unpack 'C4' => gethostbyname($esx)); $esx{$addr} = $esx; } } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Populate %rtr. If we are handed an argument, then focus only on that # device (useful for debugging). ######################################################################## sub populate_rtr { my $rtr; # Reference to an array of routers my $target = shift; # Debug trace trace_location('begin') if $debug; # If we are focussing, hand-pick what goes into $rtr if (defined $target) { my @array; $array[0] = gethostbyaddr(inet_aton($target), AF_INET); $rtr = \@array; } # Otherwise, fill $rtr with as much as we can else { $rtr = build_host_list(@rtr_suffixes); } # Build %rtr for my $rtr (@$rtr) { if (ping_it($rtr)) { my $addr = join($DOT => unpack 'C4' => gethostbyname($rtr)); $rtr{$addr} = $rtr; } } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Populate %wap. If we are handed an argument, then focus only on that # device (useful for debugging). ######################################################################## sub populate_wap { my $wap; # Reference to an array of WAPs my $target = shift; # Debug trace trace_location('begin') if $debug; # If we're focussing, hand-pick what goes into $wap if (defined $target) { my @array; $array[0] = gethostbyaddr(inet_aton($target), AF_INET); $wap = \@array; } # Otherwise, fill $wap with as much as we can else { $wap = build_host_list(@wap_suffixes); } # Build %wap for my $wap (@$wap) { if (ping_it($wap)) { my $addr = join($DOT => unpack 'C4' => gethostbyname($wap)); $wap{$addr} = $wap; } } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Given a router, grab its ARP table, process it to extract ifDescr, # IP, and MAC address mappings, and populate the shared data structure # %arp. # # %arp is a fusion of ifDescr 'dash' MAC address keyed by IP address: # ifDescr-MAC => IP # # Here is the basic concept: # -Grab ifDescr, which gets us # ifIndex = ifDescr # -Grab ipNetToMediaPhysAddress, which gets us # ifIndex.IPAddress = MAC # -Build %arp ######################################################################## sub process_arp { my %arg; my %ifDescr; # Hash of ifDescr keyed by ifIndex my $rtr = shift; # Router to query my $val; # Reference to an array of Varbinds # Debug trace trace_location('begin') if $debug > 2; # Sanity check confess 'No parameters' unless defined $rtr; $rtr = get_ipaddr($rtr) unless validaddr($rtr); confess 'Something is wrong with first argument' unless defined $rtr; # Grab the ifDescr data structure say 'Walking ifDescr' if $debug > 3; $val = snmpWalk( {host => $rtr, oid => $ifDescr_oid} ); print_it("Walk of ifDescr on $rtr is empty") unless defined $val; # Walk through the ifDescr data structure VARBIND: for my $varbind (@$val) { my ($ifDescr, $ifIndex, $vlan); # Decompose into ifIndex and ifDescr $ifIndex = $varbind->{iid}; $ifDescr = $varbind->{val}; # Build data structure $ifDescr{$ifIndex} = $ifDescr; } # Grab the ipNetToMediaPhysAddress data structure say 'Walking ipNetToMediaPhysAddress' if $debug > 3; %arg = ( host => $rtr, oid => $ipNetToMediaPhysAddress_oid, translate => 1, ); $val = snmpWalk(\%arg); print_it("Walk of ipNetToMediaPhysAddress on $rtr is empty") unless defined $val; # Walk through the ipNetToMediaPhysAddress data structure VARBIND: for my $varbind (@$val) { my ($addr, $index, $ip, $mac); # Pull out the physical address $addr = $varbind->{iid}; ($index, $ip) = ($addr) =~ /(\d+)\.(\d+\.\d+.\d+.\d+)/; next VARBIND unless (defined $index and defined $ip); next VARBIND unless defined $ifDescr{$index}; next VARBIND if $ip =~ /^127/; # Pull out MAC address $mac = $varbind->{val}; $mac = normalize_mac($mac); next VARBIND unless defined $mac; # Build data structure $arp{$ip} = $ifDescr{$index} . $DASH . $mac; # Debug info say "arp{$ip} = ifDescr{$index} $ifDescr{$index} - $mac" if $debug == 8; } # Debug trace trace_location('end') if $debug > 2; return 1; } ####################################################################### # Given a switch, grab a bunch of tables and glue them together to build # the VLAN <-> MAC address <-> ifName mapping. Return a reference to an # anonymous hash. # Here is the basic concept: # Index <-> MAC # Index <-> bridgePort # bridgePort <-> ifIndex # ifIndex <-> String # where String looks like "MAC portName VLAN" ######################################################################## sub process_cam { my %cam; # Hash containing VLANs, keyed by # "MAC ifName" strings my $esx = shift; # Switch # The next variables are all references # to hashes returned by the subroutines # bearing the same name as the variable my $ref_vlans; # Ref to hash containing vtpVlanState my $ref_macs; # Ref to hash containing dot1dTpFdbAddress my $ref_ports; # Ref to hash containing dot1dTpFdbPort my $ref_ifIndex; # Ref to hash containing dot1dBasePortIfIndex my $ref_portName; # Ref to hash containing ifName # Debug trace trace_location('begin') if $debug > 2; # Sanity check confess 'No parameters' unless defined $esx; $esx = get_ipaddr($esx) unless validaddr($esx); confess 'Something wrong with switch name' unless defined $esx; # Debug info say "Acquiring CAM table for $esx" if $debug > 2; # Gather tables $ref_vlans = extract_vtp_vlan_state($esx); $ref_macs = extract_dot1d_tp_fdb_address($esx, $ref_vlans); $ref_ports = extract_dot1d_tp_fdb_port($esx, $ref_vlans); $ref_ifIndex = extract_dot1d_base_port_if_index($esx, $ref_vlans); $ref_portName = extract_if_name_by_vlan($esx, $ref_vlans); # OK, now that I've acquired all these tables, extract what I need to # build %cam # Walk through the VLANs VLAN: for my $vlan (sort keys %{$ref_macs}) { say " Processing vlan $vlan" if $debug == 8; my ( $ref_dot1dTpFdbAddress, $ref_dot1dTpFdbPort, $ref_dot1dBasePortIfIndex, $ref_ifName ); # Set up references to hashes $ref_dot1dTpFdbAddress = $ref_macs->{$vlan}; $ref_dot1dTpFdbPort = $ref_ports->{$vlan}; $ref_dot1dBasePortIfIndex = $ref_ifIndex->{$vlan}; $ref_ifName = $ref_portName->{$vlan}; # Walk through MAC addresses MAC: for my $index (keys %{$ref_dot1dTpFdbAddress}) { my ($mac, $bridgePort, $ifIndex, $portName); say " Processing bigIndex $index" if $debug == 8; # Tie the tables together $mac = $ref_dot1dTpFdbAddress->{$index}; say " Processing mac $mac" if $debug == 8; if (exists $ref_dot1dTpFdbPort->{$index}) { $bridgePort = $ref_dot1dTpFdbPort->{$index}; say " Processing bridgePort $bridgePort" if $debug == 8; if (exists $ref_dot1dBasePortIfIndex->{$bridgePort}) { $ifIndex = $ref_dot1dBasePortIfIndex->{$bridgePort}; say " Processing ifIndex $ifIndex" if $debug == 8; if (exists $ref_ifName->{$ifIndex}) { $portName = $ref_ifName->{$ifIndex}; say " Processing portName $portName" if $debug == 8; } } } # I don't understand how this happens ... but sometimes, # the chain breaks ... the MAC address appears in # dot1dTpFdbAddress, but bridgePort isn't visible, or ifIndex, # or even portName. That's the reason for all the gross "if # (exists ...)" construct above. In any case, if, at the end # of the day (end of the gross 'if' construct), portName is # undefined, then skip to the next entry next MAC unless defined $portName; # I want to ignore CAM table entries unless they are directly # associated with end-stations. By this I mean, if I'm seeing # a CAM table entry on a port pointing to a router or another # switch ... an 'uplink port', if you will, then I want to ignore # it. In our environment, I can use a few rules to determine # whether or not the port is an 'uplink' port or not. # Ignore uplink ports next MAC if flag_uplink($esx, $portName); # I don't understand how this happens; but until I do, I # log the event if (exists $cam{"$mac $portName"}) { my $key = "$mac $portName"; log_it("for $esx cam{$key} = $cam{$key} exists already; overwriting with '$mac $portName = $vlan'\n"); } # Record result $mac = normalize_mac($mac); next MAC unless defined $mac; $cam{"$mac $portName"} = $vlan; # I don't understand why this sometimes happens; but when it does, # log the event if ($mac =~ /value/i) { print_it("Weird MAC on $esx:\n"); print_it("$vlan / $mac lives on $portName\n"); } say "$vlan / $mac lives on $portName" if $debug == 4; } } # Debug trace trace_location('end') if $debug > 2; return { %cam }; } ####################################################################### # Given the nodename or IP address of a switch, grab VLAN list and # insert a reference to an anonymous array into the shared hash %vlans ######################################################################## sub process_vlan { my $esx = shift; # IP address of switch to query my $ref_vlans; # Reference to an array of vlans # acquired from $esx # Debug trace trace_location('begin') if $debug; # Sanity check confess 'No parameters' unless defined $esx; $esx = get_ipaddr($esx) unless validaddr($esx); confess 'Something is wrong with switch name' unless defined $esx; # Acquire VLAN list $ref_vlans = extract_vtp_vlan_state($esx); # Unroll array, create string, insert into %vlan $vlan{$esx} = join $SPACE, @$ref_vlans; # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Given the nodename or IP address of a WAP, grab cd11IfAssignedSta, # and return a reference to an anonymous array of the results # (a list of MAC addresss associated with this WAP) ######################################################################## sub process_wap_clients { my $addr; # IP address of $wap my %arg; my @mac; # Array of MAC addresses which we will # return to the caller my $val; # Reference to an array of Varbinds my $wap = shift; # wireless access point # Debug trace trace_location('begin') if $debug > 2; # Sanity check confess 'No parameters' unless defined $wap; if (validaddr($wap)) { $addr = $wap; $wap = $wap{$addr}; } else { $addr = get_ipaddr($wap); } confess 'Something is wrong with wap name' unless (defined $addr and defined $wap); # Debug info say "Acquiring cd11IfAssignedSta table for $wap" if $debug > 2; # Grab cd11IfAssignedSta say 'Walking cd11IfAssignedSta' if $debug > 3; %arg = ( host => $addr, oid => $cd11IfAssignedSta_oid, translate => 1 ); $val = snmpWalk(\%arg); print_it("Walk of cd11IfAssignedSta on $wap is empty") unless defined $val; # Walk through the data structure VARBIND: for my $varbind (@$val) { my ($index, $mac); # Pull out MAC address $mac = $varbind->{val}; next VARBIND if $mac eq $SPACE; $mac = normalize_mac($mac); next VARBIND unless defined $mac; # Save results push @mac, $mac; } # Debug info if ($debug > 3) { say "$wap sees the following MAC addresses:"; say join (', ', @mac); } # Debug trace trace_location('end') if $debug > 2; return [ @mac ]; } ######################################################################## # Given a switch, return a reference to a hash of ifAdminStatus values # (1 = up, 2 = dn, 3 = testing) keyed by IfName ######################################################################## sub process_if_admin_status { my $esx = shift; # Switch my $ifAdminStatus_ref; # Reference to table returned by snmpBulkWalk # on 'ifAdminStatus' my $ifName_ref; # Reference to table returned by snmpBulkWalk # on 'ifName' my %status; # Hash containing ifAdminStatus keyed by # ifName # Debug trace trace_location('begin') if $debug > 2; # Sanity check confess 'No parameters' unless defined $esx; $esx = get_ipaddr($esx) unless validaddr($esx); confess 'Something is wrong with first argument' unless defined $esx; # Debug info say "Acquiring ifAdminStatus table for $esx" if $debug > 2; # Gather tables $ifName_ref = extract_if_name($esx); $ifAdminStatus_ref = extract_if_admin_status($esx); # OK, now that I've acquired these tables, extract what I need to # build %status INDEX: for my $ifIndex (keys %$ifName_ref) { my $ifName; $ifName = $ifName_ref->{$ifIndex}; next INDEX if flag_uplink($esx, $ifName); $status{$ifName} = $ifAdminStatus_ref->{$ifIndex}; } # Debug trace trace_location('end') if $debug > 2; return { %status }; } ######################################################################## # Prune %esx. Yank anything for which we haven't identified the SNMP # strings or sysObjectID. This routine assumes that populate_esx and # char_device(\%esx) have already been called ######################################################################## sub prune_esx { my (%seen, @uniq); # Duplicate processing my @yank; # Addresses which we will remove from %rtr # Debug trace trace_location('begin') if $debug; # Flag devices for which we couldn't find an SNMP read string for my $addr (sort by_ip keys %esx) { push @yank, $addr unless exists $snmp_read{$addr}; push @yank, $addr unless exists $snmp_version{$addr}; } # Flag devices for which we couldn't find a sysObjectID. And if we # could find one, then flag C6K for my $addr (sort by_ip keys %esx) { push @yank, $addr unless exists $sysObjectID{$addr}; } # Flag devices for which we couldn't find a nodename for my $addr (sort by_ip keys %esx) { push @yank, $addr unless exists $nodename{$addr}; } # Remove duplicate elements in @yank %seen=(); for my $item (@yank) { push(@uniq, $item) unless $seen{$item}++; } @yank = @uniq; # Delete @yank from %esx for my $yank (@yank) { say "Yanking $esx{$yank}\n" if $debug > 2; delete $esx{$yank}; } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Prune %rtr. Yank anything for which we haven't identified the SNMP # string or sysObjectID. This routine assumes that populate_rtr and # char_device(\%rtr) have already been called. ######################################################################## sub prune_rtr { my (%seen, @uniq); # Duplicate processing my @yank; # Addresses which we will remove from %rtr # Debug trace trace_location('begin') if $debug; # Flag devices for which we couldn't find an SNMP read string for my $addr (sort by_ip keys %rtr) { push @yank, $addr unless exists $snmp_read{$addr}; push @yank, $addr unless exists $snmp_version{$addr}; } # Flag devices for which we couldn't find a sysObjectID for my $addr (sort by_ip keys %rtr) { push @yank, $addr unless exists $sysObjectID{$addr}; } # Flag devices for which we couldn't find a nodename for my $addr (sort by_ip keys %rtr) { push @yank, $addr unless exists $nodename{$addr}; } # Remove duplicate elements in @yank %seen=(); for my $item (@yank) { push(@uniq, $item) unless $seen{$item}++; } @yank = @uniq; # Delete from %rtr for my $yank (@yank) { say "Yanking $rtr{$yank}" if $debug > 2; delete $rtr{$yank}; } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Prune %wap. Yank anything for which we haven't identified the SNMP # string or sysObjectID. This routine assumes that populate_wap and # char_device(\%wap) have already been called. ######################################################################## sub prune_wap { my (%seen, @uniq); # Duplicate processing my @yank; # Addresses which we will remove from %wap # Debug trace trace_location('begin') if $debug; # Flag devices for which we couldn't find an SNMP read string for my $addr (sort by_ip keys %wap) { push @yank, $addr unless exists $snmp_read{$addr}; push @yank, $addr unless exists $snmp_version{$addr}; } # Flag devices for which we couldn't find a sysObjectID for my $addr (sort by_ip keys %wap) { push @yank, $addr unless exists $sysObjectID{$addr}; } # Flag devices for which we couldn't find a nodename for my $addr (sort by_ip keys %wap) { push @yank, $addr unless exists $nodename{$addr}; } # Remove duplicate elements in @yank %seen=(); for my $item (@yank) { push(@uniq, $item) unless $seen{$item}++; } @yank = @uniq; # Delete from %wap for my $yank (@yank) { say "Yanking $wap{$yank}" if $debug > 2; delete $wap{$yank}; } # Debug trace trace_location('end') if $debug; return 1; } ####################################################################### # Given an IP address, find the associated route and mask ######################################################################## sub route_and_mask { my $addr = shift; my $addr_obj; # NetAddr::IP objects my ($route, $mask); # route and subnet mask containing $addr # Debug trace trace_location('begin') if $debug == 8; # Sanity check confess 'No parameters' unless defined $addr; confess "$addr not an IP address" unless validaddr($addr); # Create NetAddr:IP object eval { $addr_obj = NetAddr::IP->new($route, $mask) }; if ($@ or not defined $addr_obj) { log_it("Bad addr $addr / $mask: $@"); goto END; } # Walk through route table ROUTE: for my $key (sort by_ip keys %routeTable) { my $route_obj; # Create NetAddr::IP object eval { $route_obj = NetAddr::IP->new($key, $routeTable{$key}) }; if ($@ or not defined $route_obj) { log_it("Bad route $key / $routeTable{$key}: $@"); next ROUTE; } if ($addr_obj->within($route_obj)) { $mask = $routeTable{$key}; $route = $key; last ROUTE; } } # Debug info say "$addr lives within $route / $mask" if $debug == 8; END: # Debug trace trace_location('end') if $debug == 8; return ($route, $mask); } 1;