Cammer_c.pl

This code is, with permission, derived from cammer.pl (c) by Tobi Oetiker ETH Zurich. Like its parent it will interrogate network switches and identify the devices attached to each port on those switches. However, cammer_c.pl provides additional information as set out below.

The _c in the name is merely to avoid confusion with the original. The _c can be thought of as representing that the code below writes its output in CSV format.

Why write CSV output?

  • CSV files can be imported direct into a spreadsheet. This allows the results to be manipulated further in many ways. For example, you may wish to sort by a certain column or filter out certain rows.
  • CSV files can also be concatenated. This allows you, for example, to query many switches in parallel and then join the data from them into one unified report.

What will the code do for me?

It will scan your network switches and provide information to help identify the devices attached to each port.

What will be reported?

Switches report the mac addresses visible on each of their ports but the mac address on its own is cryptic. Cammer_c.pl will take the mac address and look up the IP address, DNS name, and NIC manufacturer. And it will report data from the switch such as vlan and port description. Some example output is

Sales
switch6
port1/4
hc5
113
00:12:79:a5:09:2c
192.168.4.55
printer5
oui 00:12:79
HP
Sales
switch6
port1/5
hc6
113
00:11:85:81:ec:11
192.168.4.56
printer6
oui 00:11:85
HP
Sales
switch6
port1/6

2
00:14:22:5f:b8:b4
192.168.2.20
PC20
oui 00:14:22
Dell
Sales
switch6
port1/7

2
00:14:22:59:7c:37
192.168.2.16
PC16
oui 00:14:22
Dell

The columns are
  • Group of switches - in this case the switches in the Sales department are being scanned
  • Switch name - in this case switch6
  • Port name - as shown
  • Description from the switch port, if any. The first two ports have descriptions - hc5 and hc6. The other two ports do not have descriptions
  • The vlan the port is in - in this case vlans 113 and 2 are in use
  • Mac address of attached device
  • IP address of the attached device
  • Name of the attached device as reported by DNS
  • OUI (Organisationally Unique Identifier) - the first half of the mac address indicating the manufacturer of the network interface card on the attached device
  • Name of the NIC manufacturer

The example output shows two HP printers in vlan 113 and two Dell PCs in vlan 2.

How do I install cammer_c.pl?

  • First, you will need to know read-permission SNMP community strings for your switches and routers.
  • Second, ensure Perl is installed on your system. (e.g. type "perl -v" to check the perl version)
  • Third, download the required files. There are three components
    1. Copy the code below to a file called cammer_c.pl.
    2. Download the SNMP support code. It used to be held at http://www.switch.ch/misc/leinen/snmp/perl/ but is in the process of being moved to http://code.google.com/p/snmp-session/. You need the SNMP_Session... file. Download it but do not unpack it yet. The support code provides the SNMP libraries if they are not already installed at your site.
    3. Using the procedure below create a file called ouishort.txt and put it in the same directory as cammer_c.pl.


To create the ouishort.txt file go to http://standards.ieee.org/regauth/oui/index.shtml. Click the link marked "Download the public OUI listing" and save to a file called oui.txt. Now extract the required data from that file using the commands for your OS from the following table.
DOS/Windows
Unix/Linux
type oui.txt | find "(base 16)" > oui2.txt
cat oui.txt | grep "(base 16)" | sed "s/ (base 16)//" > ouishort.txt
Open oui2.txt in an editor (notepad or edit)

Replace all "(base 16)" with nothing - i.e. delete the (base 16) text

Save as ouishort.txt


The target of the above changes is a file which has every line of the format
  • The six-digit OUI
  • White space
  • The short name for the NIC manufacturer
As mentioned, ouishort.txt must be in the same directory as cammer_c.pl so it can be found.


  • Fourth, test run cammer_c.pl as follows.
$ perl cammer_c.pl community@router community@switch test1
where you supply the correct router and switch and the appropriate SNMP community strings. The text "test1" is the grouping string shown as Sales in the example above and can be omitted in which case it will appear as a blank column in the output.
  • Fifth, if the above fails with a message saying that SNMP_Session cannot be located this means that the SNMP support libraries are not installed at your site. You then need to extract them from the downloaded file and either install them at your site or copy them to the same directory that holds cammer_c.pl.

You should now be ready to run cammer_c.pl. The router that you use should be the one that is the default gateway for the vlans on the switch you are scanning. If there is more than one router for those vlans check the code for the word "kludge" where other routers can be added. No harm will result if too many routers are added but the scan will take longer. If you have too few routers or the wrong router you won't get the IP addresses and DNS names for all hosts. Normally just one router will be enough and this can be specified on the command line.

You may find it beneficial to send a couple of broadcast pings (on Cisco, ping ip 255.255.255.255 rep 2) from the appropriate router(s) before scanning. This helps to populate the switch mac address tables by asking all devices to respond. Some print servers in particular do not send traffic unless they are contacted and will not then appear in the switch mac tables. Switches can time-out their mac address tables in short periods such as 5 minutes unless they see a frame from the device.

I can improve the code below or the text above

Feel free to do so. That is why the code is published on a wiki. Similarly, feel free to check the history to see when changes were made and to obtain older versions if needed.
  • 2008-08-20 - The original code was published on 20 August 2008
  • 2010-07-26 - Updates - mainly Perl syntax issues - were made by an anonymous user
  • 2011-07-10 - Reverted to fix some typographic issues

Is there some background on what the code is doing?

Yes, there is some. See cammer_c_explan for details.

The source code

#!/usr/bin/env perl
# -*- mode: Perl -*-
#
# Created by Tobias Oetiker <oetiker@ee.ethz.ch>
#
# Updated by James Harris between Nov 2006 and Jan 2008.
# - Added some devices to work in same way as Catalyst 2900
# - Included voice vlans
# - Now outputs CSV for easy import to spreadsheet
# - Added columns
# - OUI and NIC maker
# - Port short names
# - Port descriptions
# - DNS hostnames and domain names
# - Operational status - Up or Down
# - Caller-supplied text (for grouping)
# - Identify trunk (multi-vlan) ports
# - Handle switches which report their own macs as visible
#
# A note is in order on the caller-supplied text column. Now that
# the script produces results in CSV format the output from
# multiple runs can be concatenated. The switch name allows a first
# level of grouping but sometimes the user may wish to group
# switches. The user-supplied text may be used for this. For
# example, we scan five sites concurrently and the user-supplied
# text indicates the site.
 
require 5.005;
use strict;
my $DEBUG = 0;
BEGIN {
  # Automatic OS detection ... do NOT touch
  if ( $^O =~ /^(?:(ms)?(dos|win(32|nt)?))/i ) {
    $main::OS = 'NT';
    $main::SL = '\\';
    $main::PS = ';';
  } elsif ( $^O =~ /^VMS$/i ) {
    $main::OS = 'VMS';
    $main::SL = '.';
    $main::PS = ':';
  } else {
    $main::OS = 'UNIX';
    $main::SL = '/';
    $main::PS = ':';
  }
}
 
use FindBin;
use lib "${FindBin::Bin}";
use lib "${FindBin::Bin}${main::SL}..${main::SL}lib${main::SL}mrtg2";
 
use SNMP_Session "0.78";
use BER "0.77";
use SNMP_util; # "0.77";
use Getopt::Long;
use Pod::Usage;
use Socket;
 
 
my %OID = (
  'vmVlan' => [1,3,6,1,4,1,9,9,68,1,2,2,1,2],
  'vmVoiceVlanId' => [1,3,6,1,4,1,9,9,68,1,5,1,1,1],
  'vlanIndex' => [1,3,6,1,4,1,9,5,1,9,2,1,1],
  'ifPhysAddress' => [1,3,6,1,2,1,2,2,1,6],
  'ifName' => [1,3,6,1,2,1,31,1,1,1,1],
  'ipNetToMediaPhysAddress' => [1,3,6,1,2,1,4,22,1,2],
  'dot1dTpFdbPort' => [1,3,6,1,2,1,17,4,3,1,2],
  'dot1dBasePortIfIndex' => [1,3,6,1,2,1,17,1,4,1,2],
  'ifOperStatus' => [1,3,6,1,2,1,2,2,1,8],
  'ifAlias' => [1,3,6,1,2,1,31,1,1,1,18],
  'vlanTrunkPortDynamicStatus' => [1,3,6,1,4,1,9,9,46,1,6,1,1,14],
  #unused 'sysObjectID' => [1,3,6,1,2,1,1,2,0],
  #unused 'CiscolocIfDescr' => [1,3,6,1,4,1,9,2,2,1,1,28],
);
 
#Extract from the (base 16) lines from the IEEE download
my $ouibasefilename = "ouishort.txt";
 
#Some of the following descriptions are based on MIBs found by Cisco's
#SNMP Object Navigator.
 
 
#vmVlan .1.3.6.1.4.1.9.9.68.1.2.2.1.2
#
#For each port this identifies the Vlan to which the port has been assigned.
#
#The VLAN id of the VLAN the port is assigned to
#when vmVlanType is set to static or dynamic.
#This object is not instantiated if not applicable.
#
#The value may be 0 if the port is not assigned
#to a VLAN.
#
#If vmVlanType is static, the port is always
#assigned to a VLAN and the object may not be
#set to 0.
#
#If vmVlanType is dynamic the object's value is
#0 if the port is currently not assigned to a VLAN.
#In addition, the object may be set to 0 only.
 
 
 
 
#vmVoiceVlanId .1.3.6.1.4.1.9.9.68.1.5.1.1.1
#
#For each port with an auxilliary voice vlan this reports the vlan
#
#The Voice Vlan ID (VVID) to which this port belongs
 
 
 
#vlanIndex .1.3.6.1.4.1.9.5.1.9.2.1.1
#
#A list of the vlans known to the device whether there are ports assigned to
#them or not.
#
#An index value that uniquely identifies the
#Virtual LAN associated with this information.
 
 
 
 
#ifPhysAddress .1.3.6.1.2.1.2.2.1.6
#
#The mac address of the interface - this needs to be excluded as it is
#reported by some switches.
#
#The interface's address at its protocol sub-layer. For
#example, for an 802.x interface, this object normally
#contains a mac address. The interface's media-specific MIB
#must define the bit and byte ordering and the format of the
#value of this object. For interfaces which do not have such
#an address (e.g., a serial line), this object should contain
#an octet string of zero length.
 
 
 
 
#ifName .1.3.6.1.2.1.31.1.1.1.1
#
#A short form of the interface name such as "Fa0/1", "4/27", "VLAN-50". Note
#that this maps the interface 'number' used in MIB entries to a recognised
#name. Note, too, that this is under the public subtree and is not
#Cisco specific.
#
#The textual name of the interface. The value of this
#object should be the name of the interface as assigned by
#the local device and should be suitable for use in commands
#entered at the device's `console'. This might be a text
#name, such as `le0' or a simple port number, such as `1',
#depending on the interface naming syntax of the device. If
#several entries in the ifTable together represent a single
#interface as named by the device, then each will have the
#same value of ifName. Note that for an agent which responds
#to SNMP queries concerning an interface on some other
#(proxied) device, then the value of ifName for such an
#interface is the proxied device's local name for it.
#
#If there is no local name, or this object is otherwise not
#applicable, then this object contains a zero-length string.
 
 
 
 
 
#ipNetToMediaPhysAddress .1.3.6.1.2.1.4.22.1.2
#
#The router's ARP table.
#
#The media-dependent `physical' address. This object should
#return 0 when this entry is in the 'incomplete' state.
#
#As the entries in this table are typically not persistent
#when this object is written the entity should not save the
#change to non-volatile storage. Note: a stronger
#requirement is not used because this object was previously
#defined.
 
 
 
 
 
#dot1dTpFdbPort .1.3.6.1.2.1.17.4.3.1.2
#
#All known mac addresses and their corresponding output ports.
#N.B. if the community string value is given as comstring@vlan the
#returned data will apply to the specified vlan only.
#
#Either the value '0', or the port number of the
#port on which a frame having a source address
#equal to the value of the corresponding instance
#of dot1dTpFdbAddress has been seen. A value of
#'0' indicates that the port number has not been
#learned but that the bridge does have some
#forwarding/filtering information about this
#address (e.g. in the dot1dStaticTable).
#Implementors are encouraged to assign the port
#value to this object whenever it is learned even
#for addresses for which the corresponding value of
#dot1dTpFdbStatus is not learned(3).
 
 
 
#dot1dBasePortIfIndex .1.3.6.1.2.1.17.1.4.1.2
#
#For each port in the bridging tables this identifies the
#device "interface" which is associated thereto.
#
#The value of the instance of the ifIndex object,
#defined in MIB-II, for the interface corresponding
#to this port.
 
 
 
#vlanTrunkPortDynamicStatus .1.3.6.1.4.1.9.9.46.1.6.1.1.14
#
#Value is 1 if this port is known to VTP as a trunk.
#
#Indicates whether the specified interface is either
#acting as a trunk or not. This is a result of the
#vlanTrunkPortDynamicState and the ifOperStatus of the
#trunk port itself.
 
 
 
#Note, using snmp version 1 rather than the potentially faster snmpv2s
#as we may need to scan some devices which don't support getbulk
 
sub main {
  my %opt;
  options(\%opt);
  # which vlans do exist on the device
  my @vlans;
  my $vlani;
  my %vlan;
  my @local_mac; #mac addresses of self indexed by interface
 
 
  #Read in the OUI table - maps OUIs (leftmost three octets of mac) to
  #the company issuing the mac address
  #print "Env vars:$0";
  my $location = rindex($0, $main::SL);
  my $ouifilename = substr($0, 0, $location + 1) . $ouibasefilename;
  my %ouis;
  open (OUIFILE, $ouifilename) or die "Cannot open the oui file: $!\n";
  while (<OUIFILE>) {
    my ($oui, $owner) = split(' ',$_,2); #Split the line to two fields by spaces
    #OUI needs consistency for later matching - arbitrary preference for lower case
    $oui=lc($oui);
    chomp($owner); #Strip trailing newline
    $owner =~ s/"//g; #Remove any double quote marks
    if (length $oui != 6) {
      die "invalid line ($_) OUI length (length $oui) in $ouifilename\n";
    };
  $ouis{$oui} = $owner;
  # print "OUI line added: \$ouis\{$oui\}=$owner.\n";
  }
 
 
 
  my $sws = &snmp_session($opt{sw}, $opt{swco}, 161);
  print STDERR "SNMP switch session is $sws\n" if $DEBUG;
 
  # warn "* Gather VLAN index Table from Switch @($OID{'vlanIndex'})\n";
  warn "* Gather VLAN index Table from Switch $opt{sw}\n";
  my $sysdesc = (snmpget($opt{swco}.'@'.$opt{sw},'sysDescr'))[0];
  warn "$sysdesc\n\n";
 
 
  if ($sysdesc =~ /C2900|C2940|C3500|C3550|C3750|cat4000|s720/) {
    warn "* Going into Cisco vmVlan mode (special OID: $OID{'vmVlan'})\n";
    $sws->map_table_4 (
      [$OID{'vmVlan'}],
      sub {
        my($x,$value) = pretty(@_);
        $vlan{$x} = $value; # catalyst 2900 etc
        print STDERR "if: $x, vlan: $value\n" if $DEBUG;
        if (not scalar grep {$_ eq $value} @vlans) { #if no matches
          push @vlans, $value;
          print STDERR "New vlan: $value\n" if $DEBUG;
        }
      }, # End of the subroutine
      100
    ); # End of map_table_4
 
 
    print STDERR "* Adding any voice vlans\n";
    $sws->map_table_4 (
      [$OID{'vmVoiceVlanId'}],
      sub {
        my($x,$value) = pretty(@_);
        print STDERR "if: $x, voice vlan: $value\n" if $DEBUG;
        if ($value <= 4094) {
          #Check Cisco's OID description for 1.3.6.1.4.1.9.9.68.1.5.1.1.1
          $vlan{$x} = $value;
          if (not scalar grep {$_ eq $value} @vlans) { #if no matches
            push @vlans, $value;
            warn "vlan: $value added" if $DEBUG;
          }
        }
      }, # End of subroutine
      100
    ); # End of map_table_4
 
    # print "Vlans:\n";
    # print "@vlans\n";
    # print "--\n";
 
  } # End of if this is one of a specific list of devices
  # We now have a list of vlans active on ports (in @vlans) and a note of which
  # vlan is on which port (in %vlan{<interface>})
 
  else { #Not a 2900 etc.
    warn "* Going into vlanIndex mode\n";
    $sws->map_table_4 (
      [$OID{'vlanIndex'}],
      sub {
        my($x,$value) = pretty(@_);
        # (not on this OID) $vlan{$x} = $value;
        push @vlans, $value;
        warn qq(vlan: $value) if $DEBUG;
      }, # End of subroutine
      100
    ); #End of map_table_4
  } # End of else - i.e. this is not a 2900 etc
 
  # We now have a list of all vlans present on the switch - this includes
  # those active on ports and those with no port on the vlan
 
  warn qq(Vlans seen: @vlans) if $DEBUG;
 
  # Note that either of the above options will have populated @vlans with
  # simply a list of vlan numbers.
 
  # print STDERR "Vlan for each interface: " . scalar keys(%vlan) . "\n";
  # foreach my $key (keys %vlan) {
  #   my $value = $vlan{$key};
  #   print STDERR " interface $key : vlan $value\n";
  # }
 
  # which ifNames are on the switch?
  my %name;
  warn "* Gather Interface Name Table from Switch $OID{'ifName'}\n";
  $sws->map_table_4 (
    [$OID{'ifName'}],
    sub {
      my($if,$name) = pretty(@_);
      warn qq(if: $if, name: $name) if $DEBUG;
      $name{$if}=$name;
    }, # End of subroutine
    100
  ); # End of map_table_4
 
  # Hash %name{interface number} gives the name of each numbered interface
 
 
  # What descriptions are on the interfaces - Cisco uses ifAlias for the desc
  my @intf_alias;
  warn "* Fetching any interface descriptions";
  $sws->map_table_4 (
    [$OID{'ifAlias'}],
    sub {
      my ($x, $value) = pretty (@_);
      warn qq(Interface "$x" has description "$value"\n) if $DEBUG;
      $intf_alias[$x] = $value;
    }, # End of subroutine
    100
  ); # End of map_table_4
 
 
  #Get the operational status of each port - i.e. Up or Down
  my @intf_oper_status;
  warn "* Fetching operational status of each interface\n";
  $sws->map_table_4 (
    [$OID{'ifOperStatus'}],
    sub {
      my ($x, $value) = pretty (@_);
      warn qq(Re. operational status I have "$x"=>"$name{$x}" and "$value") if $DEBUG;
      $intf_oper_status[$x] = $value == 2 ? 0 : 1; #0=>down, 1=>up
    }, # End of subroutine
    100
  ); # End of map_table_4
 
 
 
  # Get the physical addresses of the switch ports (so that they can be
  # excluded from the report)
  warn "* Fetching interface physical addresses\n";
  $sws->map_table_4 (
    [$OID{'ifPhysAddress'}],
    sub {
      my ($x, $value) = pretty (@_);
      # print "Length of value is " . length($value) . "\n";
      # print unpack "H*", $value;
      $value = unpack "H*", $value;
      # print "Value is $value\n";
      if ($value) {
        # print "Substr 0, 2 is " . (substr $value, 0, 2);
        # print "Substr 2, 2 is " . (substr $value, 2, 2);
        # print "Substr 4, 2 is " . (substr $value, 4, 2);
        # print "Substr 6, 2 is " . (substr $value, 6, 2);
        # print "Substr 8, 2 is " . (substr $value, 8, 2);
        # print "Substr 10, 2 is " . (substr $value, 10, 2);
 
        $value = join "-",
          (substr $value, 0, 2),
          (substr $value, 2, 2),
          (substr $value, 4, 2),
          (substr $value, 6, 2),
          (substr $value, 8, 2),
          (substr $value, 10, 2);
        # print "Value is now $value\n";
        # print "Found local mac $value (on interface $x, aka $name{$x})\n";
 
        # warn print qq( Mac on interface "$x" is "$value") if $DEBUG;
 
        $local_mac[$x] = $value; #The mac address for this interface is $value.
        warn "Added local_mac[$x] = $value" if $DEBUG;
      } # End of if there is a value - i.e. a mac address
      # (there won't be on a Null0 interface)
    }, # End of subroutine
    100
  ); # End of map_table_4
 
 
  warn qq(* Check VTP MIB to find which interfaces are trunks);
  my @trunk_interfaces;
  my @intf_istrunk; #Booleans
  $sws->map_table_4 (
    [$OID{'vlanTrunkPortDynamicStatus'}],
    sub {
      my ($x, $value) = pretty (@_);
      if ($value == 1) { #If a trunk
        push @trunk_interfaces, $x;
        $intf_istrunk[$x] = 1; #Flag this as a trunk
        warn qq( New trunk interface: $x) if $DEBUG > 2;
      }
    }, # End of subroutine
    100
  ); # End of map_table_4
 
 
  $sws->close();
 
 
 
 
  # get mac to ip from router
  my %ip; #To hold the IP to mac mappings
 
  my $ros = &snmp_session($opt{ro}, $opt{roco}, 161);
 
  warn "* Gather Arp Table from Router $OID{'ipNetToMediaPhysAddress'}\n";
  $ros->map_table_4 (
    [$OID{'ipNetToMediaPhysAddress'}],
    sub {
      my($ip,$mac) = pretty(@_);
      $mac = unpack 'H*', pack 'a*',$mac;
      $mac =~ s/../$&-/g;
      $mac =~ s/.$//;
      $ip =~ s/^.+?\.//;
      push @{$ip{$mac}}, $ip;
      warn qq(ip: $ip, mac: $mac) if $DEBUG > 5;
    }, # End of subroutine
    100
  ); # End of map_table_4
 
  $ros->close();
 
 
 
 
  # --- Start of kludge ---
 
  # NB Patched/kludged here to add arp entries from specific routers:
  # Was used while one site was in a state of transition from one router to
  # many for any given switch
 
  # Assumes same community string as specified on command line
 
  my @router_names = ($opt{ro}, ); #Add routers as needed
  my @router_names = (); #if extra routers are not wanted)
 
  foreach my $router_name (@router_names) {
 
    my $ros = &snmp_session($opt{ro}, $opt{roco}, 161);
 
    warn "* Gather Arp table from router $router_name";
    $ros->map_table_4 (
      [$OID{'ipNetToMediaPhysAddress'}],
      sub {
        my($ip,$mac) = pretty(@_);
        $mac = unpack 'H*', pack 'a*',$mac;
        $mac =~ s/../$&-/g;
        $mac =~ s/.$//;
        $ip =~ s/^.+?\.//;
        push @{$ip{$mac}}, $ip;
        warn qq(ip: $ip, mac: $mac) if $DEBUG > 5;
      }, # End of subroutine
     100
   ); # End of map_table_4
   $ros->close();
 
  } # end foreach router name
 
  # --- end of kludge ---
 
 
 
 
  warn qq(* Walk CAM table for each VLAN);
  my %if;
  my %port;
 
  my %maccount; #Count of macs per vlan and port
 
  warn "* Gather Mac to Port and Port to Intf table for all VLANS \
  community\@vlan $OID{'dot1dTpFdbPort'},
  $OID{'dot1dBasePortIfIndex'}\n";
  foreach my $vlan (@vlans){
    warn qq( Gathering mac and port data for vlan $vlan) if $DEBUG;
 
    # Catalyst 2900 and similar do not use com@vlan hack
    # Open FDB for this vlan only
    my $sws = &snmp_session($opt{sw}, $opt{swco} . '@' . $vlan, 161);
 
    warn qq( Reading all the mac addresses known in this vlan to find \
      the "port" each one is on) if $DEBUG;
    $sws->map_table_4 (
      [$OID{'dot1dTpFdbPort'}],
      sub {
        my($mac,$port) = pretty(@_);
        $mac = sprintf "%02x-%02x-%02x-%02x-%02x-%02x", (split /\./, $mac);
 
        warn qq(Mac "$mac" is on port "$port" for this vlan, "$vlan") if $DEBUG;
 
        ### next if $port == 0; #0 => not learned, maybe statically configured, ignore
 
        $maccount{$vlan}{$port} += 1;
 
        warn qq(Mac "$mac" is number $maccount{$vlan}{$port} on port "$port" \
          in vlan "$vlan") if $DEBUG;
 
        if ($port != 0) { # 0 => not learned, maybe statically configured, ignore
          $port{$vlan}{$mac} = $port;
          # warn qq( Have set port($vlan,$mac) = $port) if $DEBUG > 1;
          if ($DEBUG > 1) {print STDERR " Have set port($vlan,$mac) = $port\n"}
        }
      }, # End of subroutine
      100
    ); # End of map_table_4
 
    warn qq(For each "port" in the CAM table find the "interface" number used \
      in the rest of the MIB) if $DEBUG;
    $sws->map_table_4 (
      [$OID{'dot1dBasePortIfIndex'}],
      sub {
        my($port,$if) = pretty(@_);
        if ($port != 0) {
          $if{$vlan}{$port} = $if;
          warn qq( Have set interface($vlan,$port) = $if) if $DEBUG;
        }
      }, # End of subroutine
      100
    ); # End of map_table_4
 
    $sws->close();
  } # End of for each vlan
 
 
 
 
  warn "* Process each vlan, mac, ip and hostname";
  my %output;
  warn qq( Looking at these vlans: @vlans) if $DEBUG;
  vlan: foreach my $vlan (@vlans){
    warn " Processing each mac in vlan $vlan" if $DEBUG;
    mac: foreach my $mac (keys %{$port{$vlan}}) {
      warn qq( Processing mac $mac (in vlan $vlan) on port $port{$vlan}{$mac}") if $DEBUG;
      # print "$mac, $port{$vlan}{$mac} -- " if $DEBUG;
      # while ((my $k, my $v) = each (%{$port{$vlan}})) {
      #   warn "$k=$v\n";
      # }
      my $qmacs = $maccount{$vlan}{$port{$vlan}{$mac}};
      warn qq( No. of macs ($mac) on vlan $vlan port $port{$vlan}{$mac} is $qmacs) if $DEBUG;
      # if ($qmacs > 1) {
      #   next;
      # }
      my $bridge_port_num = $port{$vlan}{$mac};
      my $intf_num = $if{$vlan}{$bridge_port_num};
      my $intf_name = $name{$intf_num};
      my $local_mac = $local_mac[$intf_num]; # The mac address of this interface, if any.
      warn qq( Mac "$mac" is on port "$bridge_port_num" aka interface "$intf_num" \
        called "$intf_name"\n) if $DEBUG;
      if (scalar grep {$_ eq $if{$vlan}{$port{$vlan}{$mac}}} @trunk_interfaces) { #If a trunk port
        warn qq( is trunk, ignoring) if $DEBUG;
        next mac;
      }
      warn qq( Comparing $mac with mac of port $intf_num, $local_mac[$intf_num]) if $DEBUG;
      if ($mac eq $local_mac) {
        warn qq( a local mac so ignoring) if $DEBUG;;
        #### if (scalar grep {$_ eq $mac} @local_mac) { #If one of our own macs
        next mac;
      } else {
        warn qq( not the same so continuing) if $DEBUG;
      }
      # if ($qmacs > 1) {
      # my $name = $name{$if{$vlan}{$port{$vlan}{$mac}}};
      # my $truevlan = $vlan eq 'none' ? $vlan{$if{$vlan}{$port{$vlan}{$mac}}} : $vlan;
      # push @{$output{$name}}, sprintf "%s,%s", $truevlan, $qmacs;
      # next mac;
      # }
      # else {
      my @ip = $ip{$mac} ? @{$ip{$mac}} : ();
      my @host;
      foreach my $ip (@ip) {
        warn " - ip $ip\n" if $DEBUG > 3;
        my $host = gethostbyaddr(pack('C4',split(/\./,$ip)),AF_INET);
        warn qq(- for ip $ip got host $host) if $DEBUG > 2;
        my @host_split = split("\\.", $host, 2); #Separate out the hostname and domain parts
        # $host =~ s/\.ethz\.ch//;
        warn qq(- host splits into "$host_split[0]" and "$host_split[1]") if $DEBUG > 2;
        if ($host) {
          push @host, $host_split[0] . "," . $host_split[1]; #Host name and domain name
        } else {
          push @host, ($ip . ","); #IP in place of the host; a blank domain name
        }
        # push @host, ($host or $ip);
      } # End of for each IP
      if (scalar @host == 0) {
        push @host, ","; #Blank hostname and domain name
      }
      my $oper_status = ($intf_oper_status[$intf_num] ? "up" : "down");
      warn qq(- operational status of $intf_num is $oper_status) if $DEBUG > 2;
      my $oui = lc(join "", ((split /-/, $mac)[0..2])); #Lower case hex
      warn qq(- oui is $oui) if $DEBUG > 2;
      my $ouiname = $ouis{$oui} ? $ouis{$oui} : "";
      warn qq(- oui name is $ouiname) if $DEBUG > 2;
      my $name = $name{$if{$vlan}{$port{$vlan}{$mac}}};
      warn qq(- name is $name) if $DEBUG > 2;
      my $truevlan = $vlan eq 'none' ? $vlan{$if{$vlan}{$port{$vlan}{$mac}}} : $vlan;
      warn qq(- vlan is $truevlan) if $DEBUG;
      if ($intf_istrunk[$intf_num]) {
        my $truevlan = "trunk";
      }
      warn qq(- true vlan is $truevlan) if $DEBUG;
      my $quest = scalar @ip > 1 ? "(multi)":"";
      warn qq(- multi string is "$quest") if $DEBUG;
      my $intf_num_lz = sprintf("%08d", $intf_num); #Add leading zeros for sort
 
      # push @{$output{$intf_num_lz . "c"}},
      # sprintf qq("mac",%s,%s,"oui %6s",%s,%s,"%s","%s"),$truevlan,$mac,$oui,$ip[0],\
      # $quest,$host[0],$ouiname;
 
      push @{$output{$intf_num_lz . "m"}},
        sprintf qq($truevlan,$oper_status,"$intf_alias[$intf_num]","$mac",$quest,$ip[0],$host[0],"oui $oui","$ouiname");
      warn qq(--- written for $intf_num_lz) if $DEBUG > 2;
 
      ## push @{$output{$name}},
      # sprintf "%s,%s,\"oui %6s\",%s,%s,%s,\"%s\"",
      # $truevlan,$mac,$oui,$ip[0],$quest,$host[0],$ouiname;
      # } #if $qmacs
      warn qq(- end of mac) if $DEBUG > 2;
    } # foreach my $mac
    warn qq(- end of vlan) if $DEBUG > 2;
  } # foreach my $vlan
 
  warn qq(- highest interface number is $#intf_oper_status) if $DEBUG > 4;
 
 
  # for (my $i = 0; $i <= scalar $#intf_oper_status; $i++) { #Each interface
  foreach my $i (sort keys %name) {
    my $oper_status = ($intf_oper_status[$i] ? "up" : "down");
    warn qq( -> interface $i status $intf_oper_status[$i]) if $DEBUG > 4;
    my $intf_num_lz = sprintf("%08d", $i); # Interface number with leading zeros
    my $truevlan = $vlan{$i};
 
    print STDERR "Vlan for interface $i is $truevlan\n" if $DEBUG;
 
    print STDERR qq(testing whether interface $i is a trunk: "$intf_istrunk[$i]") if $DEBUG;
    if ($intf_istrunk[$i]) {
      print STDERR qq( Yes\n) if $DEBUG;
      $truevlan = "trunk";
    } else {
      print STDERR qq( No\n) if $DEBUG;
    }
    push @{$output{$intf_num_lz . "p"}}, qq($truevlan,$oper_status,"$intf_alias[$i]");
 
    # if ($intf_oper_status[$i]) {
    # push @{$output{$intf_num_lz . "a"}}, qq("conn","up","$intf_alias[$i]");
    ## push @{$output{$intf_num_lz . "a"}}, "up";
    # } else {
    # push @{$output{$intf_num_lz . "a"}}, qq("conn","down","$intf_alias[$i]");
    ## push @{$output{$intf_num_lz . "a"}}, "down";
    # } #if operational status
    # if ($intf_istrunk[$i]) {
    # push @{$output{$intf_num_lz . "b"}}, qq("extra","trunk");
    ## push @{$output{$intf_num_lz . "b"}}, "trunk";
    # }
  } # For each port or interface
 
 
 
 
  my @macs_printed; #Count of macs printed for each interface
  foreach my $ikey (sort keys %output) {
    warn qq( --- have key $ikey) if $DEBUG;
    my $inum = $ikey;
    my $record_type = chop $inum; #Drop the last character suffix
    my $short_inum = 0 + $inum;
    if ($record_type eq "m") {
      foreach my $line (@{$output{$ikey}}) {
        warn qq( --- have line "$line") if $DEBUG;
        if ($name{$short_inum} ne "") { #Filter out blank port names
          printf "%s,%s,\"interface %s\",%s\n", $opt{site}, $opt{sw}, $name{$short_inum}, $line;
          $macs_printed[$inum] += 1;
          warn qq(macs printed for line key $ikey is $macs_printed[$inum]) if $DEBUG;
        } #if $name...
      } #Foreach $line as one of the m records
    } #if record_type is m
    if ($record_type eq "p") {
      foreach my $line (@{$output{$ikey}}) {
        warn qq(Have $ikey p line $line) if $DEBUG;
        # my $line = $output{$ikey}[0]; #We assume only one p-record
        warn qq(now on p record, key $ikey. total macs printed for m records was \
          "$macs_printed[$inum]") if $DEBUG;
        if ($macs_printed[$inum] == 0) {
          printf "%s,%s,\"interface %s\",%s\n", $opt{site}, $opt{sw}, $name{$short_inum}, $line;
        } #if macs_printed was 0
      } #foreach line
    } #if record_type is p
  } #Foreach $ikey
 
}
 
main;
exit 0;
 
 
sub options () {
  my $opt = shift;
  GetOptions( $opt,
    'help|?',
    'man'
  ) or pod2usage(2);
  pod2usage(-verbose => 1) if $$opt{help} or scalar @ARGV < 2 or scalar @ARGV > 3;
  $opt->{ro} = shift @ARGV;
  $opt->{sw} = shift @ARGV;
 
  $opt->{site} = shift @ARGV;
  # print "Site is .$opt->{site}.\n";
 
  pod2usage(-exitstatus => 0, -verbose => 2) if $$opt{man};
 
  $opt->{sw} =~ /^(.+)@(.+?)$/;
  $opt->{sw} = $2;
  $opt->{swco} = $1;
  $opt->{ro} =~ /^(.+)@(.+?)$/;
  $opt->{ro} = $2;
  $opt->{roco} = $1;
}
 
sub pretty(@){
  my $index = shift;
  my @ret = ($index);
  foreach my $x (@_){
    push @ret, pretty_print($x);
  };
  return @ret;
}
 
sub snmp_session {
  my $sess = SNMPv2c_Session->open($_[0], $_[1], $_[2]);
  if ($sess) {
    ;
  } else {
    my $sess = SNMPv1_Session->open($_[0], $_[1], $_[2]);
  }
  if ($sess) {
    ;
  } else {
    die("Opening SNMP session with $_[0]");
  }
  $sess;
}
 
 __END__
 
 =head1 NAME
 
 cammer - list switch ports with associated IP-addresses
 
 =head1 SYNOPSIS
 
 cammer [--help|--man] community@router community@switch
 
 
 =head1 DESCRIPTION
 
 B<Cammer> is a script which polls a switch and a router in order to produce
 a list of machines attached (and currently online) at each port of the
 switch.
 
 =head1 COPYRIGHT
 
 Copyright (c) 2000 ETH Zurich, All rights reserved.
 
 =head1 LICENSE
 
 This script 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 library 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 library; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 =head1 AUTHOR
 
 Tobias Oetiker E<lt>oetiker@ee.ethz.chE<gt>
 
 =cut