#!/usr/bin/perl use strict; use warnings; use Net::SNMP (); use Net::Netmask; use Data::Dumper; use Getopt::Long; use List::Util; no warnings 'uninitialized'; sub usage { die <] Options: --debug Show debugging outputs --mac Show one MAC address learned per port --allmac Show all MAC addresses learned per port --force-v1 Use SNMPv1 (work-around for buggy switches) AMEN } my $debug = 0; my $mac = 0; my $all_mac = 0; my $force_v1 = 0; GetOptions( 'debug' => \$debug, 'mac' => \$mac, 'allmac' => \$all_mac, 'force-v1' => \$force_v1, ) or usage; @ARGV == 1 or usage; my ($switch_ip) = @ARGV; my $community = 'public'; $mac ||= $all_mac; my $is_tty = -t STDOUT; sub attr { my ($a) = @_; return $is_tty ? `tput $a` : ""; } my $t_red = attr("setaf 1"); my $t_green = attr("setaf 2"); my $t_yellow = attr("setaf 3"); my $t_magenta = attr("setaf 5"); my $t_norm = attr("sgr0"); my ($snmp, $err) = Net::SNMP->session( -hostname => $switch_ip, -version => ($force_v1 ? '1' : '2c'), -community => $community, ); $snmp or die "Cannot establish session: $err\n"; $snmp->translate(0); sub my_get_table { my ($cols) = @_; my $tab = {}; for my $c (keys %$cols) { my $depth = split /\./, $cols->{$c}; my $t = $snmp->get_table(-baseoid => $cols->{$c}) or next; for my $k (keys %$t) { my @k = split /\./, $k; my $kk = join('.', @k[$depth..$#k]); $tab->{$kk}->{$c} = $t->{$k}; } } return $tab; } sub format_uptime { my ($t) = @_; my $d = ""; $t = int($t/100); if ($t >= 86400) { $d = int($t/86400) . " days, "; $t %= 86400; } return $d . sprintf "%02d:%02d:%02d", int($t/3600), int(($t%3600)/60), $t%60; } my $OID_basic = '1.3.6.1.2.1.1'; my $basics = my_get_table({ 'desc' => "$OID_basic.1", 'uptime' => "$OID_basic.3", 'contact' => "$OID_basic.4", 'name' => "$OID_basic.5", 'location' => "$OID_basic.6", }); print Dumper($basics) if $debug; my $bas = $basics->{0} or die "Cannot find basic info"; for (values %$bas) { s{\r}{}gs; s{\n}{ | }gs; } print "### Basics ###\n\n"; print "Device: ", $bas->{desc}, "\n"; print "Uptime: ", format_uptime($bas->{uptime}), "\n"; print "Contact: ", $bas->{contact}, "\n"; print "Name: ", $bas->{name}, "\n"; print "Location: ", $bas->{location}, "\n"; my $OID_ifTable = '1.3.6.1.2.1.2.2'; my $OID_ifTablev2 = '1.3.6.1.2.1.31.1.1'; my $if_table = my_get_table({ 'desc' => "$OID_ifTable.1.2", 'speed' => "$OID_ifTable.1.5", 'mac' => "$OID_ifTable.1.6", 'admin' => "$OID_ifTable.1.7", 'oper' => "$OID_ifTable.1.8", 'name' => "$OID_ifTablev2.1.1", 'hispeed' => "$OID_ifTablev2.1.15", 'alias' => "$OID_ifTablev2.1.18", }); my %if_macs = (); for my $if (values %$if_table) { $if->{mac} = join(':', map { sprintf "%02x", ord $_ } split(//, $if->{mac})); $if_macs{$if->{mac}} = 1; } print Dumper($if_table) if $debug; my @ifaces = sort { $a <=> $b } keys %$if_table; print "MAC addr: ", join(" ", sort keys %if_macs), "\n"; my $OID_ipTable = '1.3.6.1.2.1.4.20'; my $ip_table = my_get_table({ 'iface' => "$OID_ipTable.1.2", 'mask' => "$OID_ipTable.1.3", }); print Dumper($ip_table) if $debug; # XXX: IPv6 not supported yet for my $ipa (keys %$ip_table) { my $ip = $ip_table->{$ipa}; my $if = $if_table->{$ip->{iface}} or die "IP table refers to unknown iface"; my $nm = Net::Netmask->new2($ipa, $ip->{mask}) or die "Cannot parse IP prefix"; push @{$if->{ip_addrs}}, $ipa . '/' . $nm->bits; } print Dumper($if_table) if $debug; if ($mac) { my $OID_1qTpFdbTable = '1.3.6.1.2.1.17.7.1.2.2'; my $vlan_fdb_table = my_get_table({ 'port' => "$OID_1qTpFdbTable.1.2", 'status' => "$OID_1qTpFdbTable.1.3", }); print Dumper($vlan_fdb_table) if $debug; if (%$vlan_fdb_table) { for my $m (keys %$vlan_fdb_table) { my $fdb = $vlan_fdb_table->{$m}; $fdb->{status} == 3 or next; # Only learned MACs my $port = $if_table->{$fdb->{port}} or die "Forwarding DB refers to unknown iface"; my @m = split /\./, $m; my $vlan = shift @m; my $mac = join(':', map { sprintf('%02x', $_) } @m); push @{$port->{macs}}, $mac; } } else { print "# Trying fall-fack to .1d FDB\n" if $debug; my $OID_1dTpFdbTable = '1.3.6.1.2.1.17.4.3'; my $fdb_table = my_get_table({ 'port' => "$OID_1dTpFdbTable.1.2", 'status' => "$OID_1dTpFdbTable.1.3", }); print Dumper($fdb_table) if $debug; for my $m (keys %$fdb_table) { my $fdb = $fdb_table->{$m}; $fdb->{status} == 3 or next; # Only learned MACs my $port = $if_table->{$fdb->{port}} or die "Forwarding DB refers to unknown iface"; my @m = split /\./, $m; my $mac = join(':', map { sprintf('%02x', $_) } @m); push @{$port->{macs}}, $mac; } } for my $if (values %$if_table) { $if->{macs} or next; $if->{macs} = [ sort(List::Util::uniq(@{$if->{macs}})) ]; } } my $OID_VlanStaticTable = '1.3.6.1.2.1.17.7.1.4.3'; my $vlan_table = my_get_table({ 'name' => "$OID_VlanStaticTable.1.1", 'egress-ports' => "$OID_VlanStaticTable.1.2", 'untagged-ports' => "$OID_VlanStaticTable.1.4", 'row-status' => "$OID_VlanStaticTable.1.5", }); for my $vlan (values %$vlan_table) { for my $k ('egress-ports', 'untagged-ports') { $vlan->{$k} = [ split //, unpack("B*", $vlan->{$k} // "") ]; } } print Dumper($vlan_table) if $debug; my @vlans = sort { $a <=> $b } grep { $vlan_table->{$_}->{'row-status'} == 1 } keys %$vlan_table; print "\n### VLANs ###\n\n"; if (@vlans) { for my $vid (@vlans) { printf "%-4d %s\n", $vid, $vlan_table->{$vid}->{'name'} // '-'; } } else { print "No VLAN support.\n"; } print "\n### Ports ###\n\n"; # XXX: We assume that 802.1d switch ports IDs are equal to interface IDs for my $port (@ifaces) { my $if = $if_table->{$port}; my $state; my $scolor = $t_norm; if ($if->{'admin'} != 1) { $state = 'OFF'; } elsif ($if->{'oper'} != 1) { $state = 'DOWN'; $scolor = $t_red; } else { $state = 'UP'; $scolor = $t_green; } my $speed = $if->{hispeed} || int($if->{speed} / 1000000); if (!$speed) { $speed = ""; } elsif ($speed < 1000) { $speed = "${speed}M"; } else { $speed = int($speed/1000); $speed = "${speed}G"; } printf "%-4d %-15.15s %s%-4s %-5s %s%-25.25s%s", $port, $if->{name}, $scolor, $state, $speed, $t_yellow, $if->{alias}, $t_norm; if ($mac) { my $show_mac = ""; my $more_macs = " "; my @macs = @{$if->{macs} // []}; if (@macs) { $show_mac = $macs[0]; $more_macs = "${t_yellow}+${t_norm}" if @macs > 1; } printf " %-17s%s ", $show_mac, $more_macs; } for my $vid (@vlans) { my $vlan = $vlan_table->{$vid}; if ($vlan->{'egress-ports'}->[$port]) { if ($vlan->{'untagged-ports'}->[$port]) { if ($t_green ne "") { print " ${t_green}${vid}${t_norm}"; } else { print " ${vid}U"; } } else { print " $vid"; } } } print "\n"; if ($if->{ip_addrs}) { print "${t_yellow} IP: ", join(" ", @{$if->{ip_addrs}}), "${t_norm}\n"; } if ($all_mac) { if ($if->{mac}) { print "${t_yellow} Local MAC: ", $if->{mac}, "${t_norm}\n"; } if ($if->{macs}) { for my $m (@{$if->{macs}}) { print "${t_magenta} MAC: $m${t_norm}\n"; } } } }