#!/usr/bin/perl -w
#
# SwitchMap.pl - generate web pages that describe Cisco switches
#
# This program's version number is in file Constants.pm.
#
# AUTHOR
#
# Pete Siemsen, siemsen@ucar.edu, 303-497-1810
#
# AVAILABILITY
#
# The current version should always be available at
# http://sourceforge.net/projects/switchmap/
#
#-------------------------------------------------------------------------
# Copyright 2008 University Corporation for Atmospheric Research
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.
#
# For more information, please contact
# Pete Siemsen, siemsen@ucar.edu
#--------------------------------------------------------------------------
#
# This script outputs HTML files and text files that describe Cisco
# Ethernet switches. The files show information about each switch
# port, including how long the port has been idle and if possible,
# what machines are connected to each port. This script can use
# information from a machine running HP Network Node Manager, or
# information from local configuration files.
#
# The basic algorithm is:
#
# Every hour, a cron job runs the ScanSwitch.pl script, which checks
# every switch to see which ports are active. For each switch,
# ScanSwitch.pl uses SNMP to download the operational status of each
# port on the switch. ScanSwitch.pl stores the state of each port in
# "idlesince" files, for later use by this script.
#
# Every hour, another cron job runs GetArp.pl, a script that gets the
# ARP caches from the routers and updates MacList, a file that provides
# MAC-address to IP-address information for later use by this script.
#
# Every day, a cron job runs this script, which integrates the data in
# the idlesince files, possibly data from HP NNM, possibly data from
# the MacList file, and more SNMP data from the switches themselves,
# and outputs HTML and text files.
#
# These are the various files that this script needs, reads or writes:
#
# 1. SwitchMap.pl (this script), which is run in a cron job every day.
# 2. ScanSwitch.pl, a Perl script which is run in a cron job every hour.
# 3. GetArp.pl, a Perl script which is run in a cron job every hour.
# 4. MacList, a text file containing IP addresses for every MAC
# address, as collected by GetArp.pl.
# 5. OuiCodes.txt, a text file containing the manufacturer codes that
# are found in MAC addresses. OuiCodes.txt is read by this script.
# It is generated by UpdateOuiCodes.pl.
# 6. UpdateOuiCodes.pl, a Perl script that generates the OuiCodes.txt
# file. See comments in the script for details.
# 7. "idlesince" files, one for each switch. These files live in
# the "idlesince" subdirectory, are updated every hour by
# ScanSwitch.pl, and read by SwitchMap.pl. These files contain, on
# each line, a port name and Unix timestamp of the most recent time
# that the port was found to be idle, or 0 if the port was active the
# last time ScanSwitch.pl was run. Thus, the port has been "idle
# since" the given time.
# 8. HTML files in the "switches", "unused" and "vlans" subdirectories.
# These files are generated by SwitchMap.pl. These are the Web
# pages that are the point of all this.
# 9. Text files named "*.map" in the "text" sub directory, one for each
# switch. These files are generated by SwitchMap.pl. These are
# simple text versions of the "switches" HTML files. They are needed
# to support the search function, and are nice to have when you want
# to grep for something.
#
# BUGS
#
# . the string "port is active, but no packets have been seen recently"
# appears on too many ports. Like, several ports on l3-gw-1.frgp.net
# show it when they clearly have seen packets.
#
# . Suns with multiple network interfaces use the same MAC address
# on all the interfaces. This causes incorrect entries in some IP
# address columns. Dunno how to fix this.
#
# . Sometimes, a 6509 will report an ifName table that is missing an
# entry. Other SNMP tables will refer to the ifIndex for the entry,
# causing SwitchMap to generate "Warning: no interface name for SNMP
# ifIndex <n> on <SwitchName>, skipping <Mac>" messages. Rebooting
# the switch makes the problem go away. I opened Cisco TAC case
# 602418175, but the problem switch was rebooted before I could
# supply enough information to Cisco, so I asked them to close the
# case. Two different 6509s did it. Both were running 7.6(12).
# This bug existed as of 2005-11-10, and is still seen on some
# of our switches as of 2007-09-06.
#
# TODO
#
# . Consider the SNMP-BridgeQuery module (Google for it)
#
# . Rewrite FindOffice.pl so it parses the HTML files instead of the
# .map files. Then relax the 20-character limit on Port Labels.
# This is needed because IOS systems can have "description" fields
# that are longer than 20 characters, so there's no reason other
# than the .map files to retain the limit. Remember to truncate
# the Port Labels as they are written to the .map files, to preserve
# the formatting of the .map files.
#
# . Add "SNMP agent has been up for xxx" to the Model/Contact/Location
# information at the top of switch web pages.
#
# . An easy one: as we read manufacturer names from OuiCodes file,
# convert any spaces into . This will make entries in the
# column be lined up with entries in other columns.
#
# . In ModuleList.pm, modules are represented by a set of hashes, each
# of which uses the same keys (module number). At one time, this
# seemed cool because GetSnmpTable can read right into these arrays.
# Now that I've added support for getting module data from the Entity
# MIB, it would be cleaner to represent modules as an array of module
# objects. This would be more natural, and would get rid of the
# $NbrModules variables, and I could initialize the fields in the
# objects to 'unknown', which would be cleaner than the tests I do
# now.
#
# . David Mitchell asked
# "is it possible to indicate the auto negotiation status on the
# port lists? I assume you are grabbing the duplex out of portDuplex
# in the Cisco StackMIB? If so, grab the previous column
# portAdminSpeed and if it is a 1 or 2, prepend 'a-' onto the
# displayed duplex. Or something like that. Basically speed and
# duplex cannot have their negotiation status changed
# independently. The portAdminSpeed value indicates whether both
# of them are auto negotiate or fixed."
#
# . Lance Vermillion noticed that the Spare Ports pages count
# trunk ports, which is wrong - trunk ports are not contenders
# for use as spare ports.
#
# . For etherchannels, David Mitchell wants to see MACs on both ports
# somehow. The key is that the MAC addresses are hooked to the
# *parent* port, not the children.
#
# . Show port errors. There isn't room to add more columns to the
# existing web pages, so make a new set of web pages under "ports"
# for "error" ports, which are ports that have errors that exceed
# some threshold. Columns in these new pages might include
#
# FCS
# runts
# giants
# last change counter/bouncing
# 100Mbps but not full duplex
# admin auto but not full
# not admin auto
# input traffic but no output traffic
# output traffic but no input traffic
#
# Note that some errors are not shown by the "show ports"
# command - there are more errors shown by the "show counters"
# command.
#
# For some of these, I'll have to put new data into the idlesince
# files to track state. Perhaps I should put a single "error" link
# on each row of the main web pages - if the link exists, there's
# something wrong with the port. If I do this, maybe empty
# "comment" fields shouldn't be indicated with color on the main
# pages any more - it should be in the "error" pages.
#
# . When SwitchMap reads the MacList file, it does a DNS lookup on
# every one of the IP addresses in the file. If a DNS server is
# down, these DNS lookups can time out on each lookup, causing
# SwitchMap to appear to hang. Dunno how to fix this one.
#
# . See the comments about doDNS in MacIpTables.pm. There's gotta be
# a better way.
#
# . Make the switch names be links to/from the switch web pages, and
# make links from those pages back to these Web pages.
#
# . Make links to/from the NOC pages
#
# . Make links from the each switch page to it's corresponding
# unusedbyswitch page.
#
# . In the header text, show the switch and the uptime for the switch
#
# . Peter Silva <Peter.Silva@pt.ibm.com> requested that the portlist
# code be able to get data from the host running OpenView via telnet
# instead of ssh.
#
# . In the spare port lists:
# . Change the Spare Ports web page so that it shows how many
# ports are spare on each VLAN on each switch. Then mark each
# place where this number is 0 in red. This is just an idea,
# and may not be practical. If I do this, see if it makes
# sense to identify the spare ports on each VLAN.
#
# . Start using ifLastOperChange, and stop doing ScanSwitch.pl? Would
# this work when switches reboot?
#
# . use a database to store: MACs, idlesince times, SNMPv2 capability.
#
use strict;
#use Data::Dumper;
use Getopt::Std;
use Log::Log4perl qw(get_logger :levels);
use File::Spec;
use FindBin;
use lib $FindBin::Bin;
use ThisSite;
use Constants;
use CiscoConstants;
use SnmpCommunities;
use Switch;
use Vlan;
use MacIpTables;
use PetesUtils; # InitializeLogging
#use SwitchUtils;
use Stats; # WriteSwitchStats
use WriteNcarFiles;
use WriteModulesFile;
use WritePortsDirectory;
use WriteSwitchesDirectory;
use WriteTextDirectory;
use WriteGigePerVlansDirectory;
use WriteVlansDirectory;
sub version { $Constants::VERSION; }
sub Usage () {
my $MyName = PetesUtils::ThisScriptName();
die <<WARNING;
Usage: SwitchMap.pl [-c] [-d n] [-v] [switchname]
This program creates text and HTML files representing one
or more Cisco Ethernet switches. For each switch,
information about each module and port is generated.
-c Function as a cgi script, requires the
switchname argument
-d n Debugging level from 0 to $Constants::MAX_DEBUGGING_MESSAGE_LEVELS,
default is 0
-i n Informational level from 0 to $Constants::MAX_INFORMATIONAL_MESSAGE_LEVELS,
default is 0
-f Write log message to a file named
$MyName.log
-v Display the version and exit
switchname The name of a switch. The name must be
composed of only lowercase letters,
digits, dashes and periods.
If no switch name is given, all switches
are processed. In this case, the list
of switches is retrieved from HP Network
Node Manager or the hard-coded list in
ThisSite.pm.
WARNING
}
#
# Get the switch name from the command line. If there is no
# switch name, then do all switches.
#
sub ParseCommandLineAndInitializeLogging ($$) {
my $CgiRef = shift;
my $SwitchNameRef = shift;
my %options;
if (getopts('cd:fi:sv', \%options) == 0) {
Usage();
}
$$CgiRef = (exists $options{'c'});
my $opt_d = 0;
my $opt_i = 0;
my $opt_s = 0;
if (exists $options{'d'}) {
$opt_d = $options{'d'};
Usage unless $opt_d =~ /^\d+$/ and $opt_d >= 0 and $opt_d <= $Constants::MAX_DEBUGGING_MESSAGE_LEVELS;
} elsif (exists $options{'i'}) {
$opt_i = $options{'i'};
Usage unless $opt_i =~ /^\d+$/ and $opt_i >= 0 and $opt_i <= $Constants::MAX_DEBUGGING_MESSAGE_LEVELS;
}
if (exists $options{'v'}) {
my $version = version();
die "SwitchMap version $version\n";
}
my $LogToFile = 0;
if (exists $options{'f'}) {
$LogToFile = 1;
}
if ($#ARGV == -1) { # if no arguments
if ($$CgiRef) {
die "-c option requires that a switchname be suppiled, exiting\n";
}
} elsif ($#ARGV == 0) { # if there's one argument
$$SwitchNameRef = $ARGV[0]; # must be a switch name
} else { # else, too many arguments
Usage();
}
PetesUtils::InitializeLogging($LogToFile, $opt_i, $opt_d, $Constants::MAX_DEBUGGING_MESSAGE_LEVELS);
}
sub WriteSearchHelpFile () {
my $logger = get_logger('log2');
my $SearchHelpFileName = File::Spec->catfile($ThisSite::DestinationDirectory, $Constants::SearchHelpFile);
$logger->debug("called, writing $SearchHelpFileName");
$logger->info("writing $SearchHelpFileName");
open SEARCHHELPFILE, ">$SearchHelpFileName" or do {
$logger->fatal("Couldn't open $SearchHelpFileName for writing, $!");
exit;
};
print SEARCHHELPFILE SwitchUtils::HtmlHeader("Help searching the Cisco port lists", 0);
print SEARCHHELPFILE <<SBODY;
You can use this to get answers to questions like
<ul>
<li>What ports are active in a given office?
<li>Where is a given MAC address?
<li>What switch/port is the machine named fileserver connected to?
</ul>
$ThisSite::ExtraHelpText
<p>
Case is not significant in the search.
SBODY
print SEARCHHELPFILE SwitchUtils::HtmlTrailer;
close SEARCHHELPFILE;
SwitchUtils::AllowAllToReadFile $SearchHelpFileName;
$logger->debug("returning");
}
sub WriteMainIndexFile () {
my $logger = get_logger('log2');
my $IndexFileName = File::Spec->catfile($ThisSite::DestinationDirectory, 'index.html');
$logger->debug("called, writing main $IndexFileName");
$logger->info("writing $IndexFileName");
open INDEXFILE, ">$IndexFileName" or do {
$logger->fatal("Couldn't open $IndexFileName for writing, $!");
exit;
};
print INDEXFILE SwitchUtils::HtmlHeader("Ethernet Switch Port Lists", 0);
print INDEXFILE <<IDX1;
Information about switches is available in various forms. You can
<ul>
<li>
<a href="SearchPortlists.html">Search the portlist web pages</a>
for various text strings
</li>
IDX1
if ($ThisSite::HasFinder) {
print INDEXFILE <<IDX2;
<li>
<a href="SearchNetwork.html">Search the network itself</a>
for an address
</li>
IDX2
}
print INDEXFILE <<IDX3;
<li>Browse the portlist web pages:
<ul>
<li><a href = "switches/">Switches</a></li>
<li><a href = "$Constants::ModulesBySwitchFile">Modules</a></li>
<li><a href = "ports/">Ports</a></li>
<li><a href = "vlans/">VLANs</a></li>
<li><a href = "$Constants::SwitchStatsFile">Statistics</a></li>
IDX3
print INDEXFILE <<IDX4;
</ul>
</li>
</ul>
IDX4
print INDEXFILE SwitchUtils::HtmlTrailer;
close INDEXFILE;
SwitchUtils::AllowAllToReadFile $IndexFileName;
$logger->debug("returning");
} # sub WriteMainIndexFile
#
# Given a list of switch names, return a list of switch objects.
# In other words, create a switch object for each switch, and
# populate the object with real data by doing SNMP to the switch.
#
sub CreateSwitches ($) {
my $SwitchNames = shift;
my $logger = get_logger('log2');
$logger->debug("called");
my @Switches;
foreach my $SwitchName (@$SwitchNames) {
$logger->info("getting data from $SwitchName");
my $Switch = new Switch $SwitchName;
if ($Switch->PopulateSwitch()) {
push @Switches, $Switch; # save the object
}
}
$logger->debug("returning");
return @Switches;
}
#
# Go through all the ports in all the switches, to create $VlansRef,
# a hash of Vlan objects. The keys of the hash serve as a list of
# all VLANs.
#
sub CreateVlans ($$) {
my $SwitchesRef = shift; # passed in array of Switch objects
my $VlansRef = shift; # hash of Vlan objects, filled by this subroutine
my $logger = get_logger('log2');
$logger->debug("called");
foreach my $Switch (@$SwitchesRef) {
my $SwitchName = $Switch->GetName;
foreach my $PortName (keys %{$Switch->{Ports}}) {
my $Port = $Switch->{Ports}{$PortName};
if (exists $Port->{VlanNbr}) {
my $VlanNbr = $Port->{VlanNbr};
my $Vlan;
if (exists $$VlansRef{$VlanNbr}) {
$Vlan = $$VlansRef{$VlanNbr};
} else {
$Vlan = new Vlan $VlanNbr;
$$VlansRef{$VlanNbr} = $Vlan;
}
$Vlan->{Switches}{$SwitchName} = $Switch;
$Vlan->{NbrPorts}++;
$Vlan->{NbrUnusedPorts}++ if $Port->{Unused};
}
}
}
$logger->debug("returning");
}
#
# Main. ======================================================================
#
my $Cgi;
my $SwitchName = '';
ParseCommandLineAndInitializeLogging(\$Cgi, \$SwitchName);
my $logger = get_logger('log1');
$logger->debug("SwitchMap version $Constants::VERSION starting...");
CiscoConstants::initialize(); # read Cisco MIBs and initialize chassis and module types
SnmpCommunities::initialize(); # read SNMP community strings
MacIpTables::initialize(1); # read MacList file or OpenView file
my @SwitchNames;
if ($SwitchName) { # if there is a single switch name on the command line
@SwitchNames = ( $SwitchName );
} else {
@SwitchNames = MacIpTables::getAllSwitchNames();
}
$logger->info("getting data from switches ...");
my @Switches = CreateSwitches(\@SwitchNames);
if ($#Switches == -1) {
$logger->fatal("no switches processed, dying");
exit;
}
my %Vlans; # a hash of Vlan objects indexed by Vlan number
CreateVlans(\@Switches, \%Vlans); # fill the hash of vlan objects
$logger->info("creating output files...");
WriteMainIndexFile();
WriteSwitchesDirectory::WriteSwitchesFiles(\@Switches);
WriteTextDirectory::WriteSwitchTextFiles(\@Switches);
WriteVlansDirectory::WriteVlansDirectory(\@Switches);
WritePortsDirectory::WritePortsDirectory(\@Switches, \%Vlans);
if ($ThisSite::DnsDomain eq '.ucar.edu') { # only if we're at NCAR
WriteNcarFiles::WriteNcarFiles(\@Switches);
}
WriteSearchHelpFile();
WriteModulesFile::WriteModulesFile(\@Switches);
Stats::WriteStatisticsFile(\@Switches);
$logger->info("exiting normally...");