http://qs321.pair.com?node_id=60928
Category: Networking Code
Author/Contact Info ybiC
Description: Starting with a seed device, discover neighboring Cisco switches/routers.   If run repeatedly, feeding prior output back as input, can generate complete list of Cisco switches/routers in your network.   I use it like that to create input files for (code)) Cisco Pass Mass - IOS (deprecated by node 123464).

Comments and critique are very much welcomed.

Thanks to tilly, fastolfe, japhy, geektron, Petruchio, mkmcconn, boo_radley and chromatic for their suggestions and advice.
 

#!/usr/bin/perl -w

# cdp-n.pl
# pod at tail

use strict;
use Net::Telnet::Cisco;
use Term::ReadKey;
use Tie::IxHash;


### Begin Config Parameters ###
my %parm = (
    tntimeout => 10,
    pwtimeout => 60,
    errmode   => 'return',
    );
my %file = (
    in      => 'cdp-n.in',
    tmp0    => 'cdp-n.tmp0',
    tmp1    => 'cdp-n.tmp1',
    tmp2    => 'cdp-n.tmp2',
    tmp3    => 'cdp-n.tmp3',
    tmp4    => 'cdp-n.tmp4',
    uniqout => 'cdp-n.uniq',
    );
my @tmpfiles = qw(
    $file{tmp0}
    $file{tmp1}
    $file{tmp2}
    $file{tmp3}
    $file{tmp4}
    );
my @commands = (
    'term leng 0',        # IOS
    'set leng 0',         # CatOS 
    'sho cdp n de',       # distinguish 'details' from 'duplex'
    );
my @keeplines = (         # lines of text to be kept
    'scrpne',             # Corporate switches
    'cff',                # old Frozen switches at Campus 5
    'pdl',                # old Frozen switches at Campus 6
    );
my @stripchars = (        # specific text to be stripped from what's l
+eft
    '\.\w.*',             # domain name (if present) through end-of-li
+ne
    '^.*\(',              # characters and paren before devicename
    '\).*$',              # paren after devicename through end-of-line
    'device-id:\s',       # preceeds some switches, dunno why
    'device id:\s',       # preceeds some switches, dunno why
    '^.*>.*$',            # command prompts
    '^\s+',               # leading spaces
    );
my @striplines = (        # strip all lines containing this text
    '-MSFC',
    '-msfc',
    'RSM',
    'rsm',
    'Password',
    'password',
    'unintialized value',
    );
### End Config Parameters ###


my ($pass, %seen);
Usage() unless ((@ARGV) or (-s $file{in} && -T_));
open (TMP1, ">$file{tmp1}") or die "Error opening $file{tmp1} WO: $!";
close (TMP1)                or die "Error closing $file{tmp1}: $1";
# $file{tmp1} must be present to prevent errors if first device unreac
+hable


print "\n",
    "  Prompting for password\n",
    "    (*not* echoed to screen nor written to disk)\n\n",
    '    Enter password:';
ReadMode('noecho');
eval {
    local $SIG{ALRM} = sub { die "ALARUM" };
    alarm("$parm{pwtimeout}");
    chomp($pass = <STDIN>);
    alarm(0);
    };
if ($@ =~ /ALARUM/) {
    print "\n\n",
        "  Sorry - you waited too long before entering a password.\n",
        "  Try again, if you want.\n\n";
    ReadMode(0);
    exit;
    }
ReadMode(0);


unless (@ARGV) {
    print "  using $file{in} for input\n";
    @ARGV = $file{in};
    chomp (@ARGV = <>);
    }


print "\n  Starting CDP neighbor check, shouldn't take long...\n";
for my $cisco (@ARGV) {
    if (my $cs=Net::Telnet::Cisco->new(
            host      => $cisco,
            timeout   => $parm{tntimeout},
            errmode   => $parm{errmode},
            input_log => $file{tmp0},
            )
        )
            {
            $cs->login('',$pass);              # vty login
            print '    ', $cs->last_prompt, ' ';       # print to cons
+ole
                for(@commands) {
                    my @output = $cs->cmd($_);
                    }
            print $cs->last_prompt, "\n";      # print to console
            $cs->close;                        # exit session

            open (TMP0, "<$file{tmp0}")
                or die "Error opening $file{tmp0} RO: $!";
            open (TMP1, ">>$file{tmp1}")
                or die "Error opening $file{tmp1} for append: $!";
            while (<TMP0>) { print TMP1 $_; }
            close (TMP0)                 or die "Error closing $file{t
+mp0}: $!";
            close (TMP1)                 or die "Error closing $file{t
+mp1}: $!";
    } else { warn "Error connecting to $cisco\n"; } # if telnet connec
+t fail
  }
unlink ($file{tmp0})                     or die "Error unlinking $file
+{tmp0}: $!";
print "  Finished CDP neighbor check.\n";


print "  Extracting lines of interest...\n";
open (TMP1, "<$file{tmp1}") or die "Error opening $file{tmp1} RO: $!";
open (TMP2, ">$file{tmp2}") or die "Error opening $file{tmp2} RW: $!";
    while (<TMP1>) {
        foreach my $keep(@keeplines) {
            if (/$keep/i) {
                print TMP2 $_;
                }
            }
        }
close (TMP1)                 or die "Error closing $file{tmp1}: $!";
unlink ($file{tmp1})         or die "Error unlinking $file{tmp1}: $!";
close (TMP2)                 or die "Error closing $file{tmp2}: $!";


print "  Stripping unwanted characters...\n";
open (TMP2, "<$file{tmp2}") or die "Error opening $file{tmp2} RO: $!";
open (TMP3, ">$file{tmp3}") or die "Error opening $file{tmp3} RW: $!";
    while (<TMP2>) {
        foreach my $strip(@stripchars) {
            s/$strip//gi;
            }
        tr/A-Z/a-z/;
        print TMP3 $_;
        }
close (TMP2)                 or die "Error closing $file{tmp2}: $!";
unlink ($file{tmp2})         or die "Error unlinking $file{tmp2}: $!";
close (TMP3)                 or die "Error closing $file{tmp3}: $!";


print "  Stripping unwanted lines...\n";
open (TMP3, "<$file{tmp3}")  or die "Error opening $file{tmp3} RO: $!"
+;
open (TMP4, ">$file{tmp4}")  or die "Error opening $file{tmp4} RW: $!"
+;
    while (<TMP3>) {
        foreach my $strip(@striplines) {
            s/^.*$strip.*//gi;
            }
        s/^\s+$//gi;
        print TMP4 $_;
        }
close (TMP3)                 or die "Error closing $file{tmp3}: $!";
unlink ($file{tmp3})         or die "Error unlinking$file{tmp3}: $!";
close (TMP4)                 or die "Error closing $file{tmp4}: $!";


print "  Stripping duplicate lines...\n";
open (TMP4, "<$file{tmp4}")    or die "Error opening $file{tmp4} RO: $
+!";
open (UNIQ, ">$file{uniqout}") or die "Error opening $file{uniqout} RW
+: $!";
    foreach my $item (<TMP4>) {
        ++$seen{$item};           # parse $file{in} into a hash
        }                         # and remove duplicate keys
    print UNIQ sort keys %seen;   # sort keys and save to $outfile
close TMP4                   or die "Cannot close $file{tmp4}: $!";
unlink ($file{tmp4})         or die "Error unlinking$file{tmp4}: $!";
close UNIQ                   or die "Cannot close $file{uniqout}: $!";


print 
    "  Finished parsing results.\n",
    "  Output at $file{uniqout}\n\n\a";


######################################################################
+#####
sub Usage {
    print <<EOF;

 D'oh!  You didn't specify any target device(s) on command-line or at 
+$file{in}.
 If you want to try again, use the following syntax:

 cdp-n.pl switch1 switch2 switch3
    will query the 3 named (or numbered) switches for CDP neighbors
 cdp-n.pl
    with no arguments will read $file{in} for list of switches to quer
+y.
    $file{in} would be a text file that looks like this:
        switch1
        switch2
        switch3

    ASCII text file
    One IP address or DNS name per line
    FQDN if targets in different DNS domain
    No leading/trailing spaces
    No blank lines
 
 First run this program against a core switch, then recurse
 down, using output as next input.  Review and edit .in files between
 recursions, and specify different outfile for each iteration also.
 Remember to add each recursion back into final results, then run
 uniq.pl to sort and extract unique entries.

    Net::Telnet::Cisco $Net::Telnet::Cisco::VERSION
    Net::Telnet        $Net::Telnet::VERSION
    Term::Readkey      $Term::ReadKey::VERSION
    Tie::IxHash        $Tie::IxHash::VERSION
    Perl               $]
    OS                 $^O
    Program            $0
 
EOF
    exit;                   # exit instead of die, so no error to cons
+ole
    }
######################################################################
+#####
sub Pause {
    print "<enter> to continue, ctrl-C to quit.\n";
    (<STDIN>);
    }
######################################################################
+#####


=head1 Name

 cdp-n.pl

=head1 Summary

 Generate comprehensive list of Cisco LAN switches installed.

=head1 Usage

 cdp-n.pl switch1 switch2 switch3
    will query the 3 named (or numbered) switches for CDP neighbors
 cdp-n.pl
    with no arguments will read $file{in} for list of switches to quer
+y.
    $file{in} would be a text file that looks like this:
        switch1
        switch2
        switch3

    ASCII text file
    One IP address or DNS name per line
    FQDN if targets in different DNS domain
    No leading/trailing spaces
    No blank lines
 
 First run this program against a core switch, then recurse
 down, using output as next input.  Review and edit .in files between
 recursions, and move outfile for each iteration also.  Remember to
 add each recursion back into final results, then run  uniq.pl to
 sort and extract unique entries from combined outfile.


=head1 Tested

 with:    Perl 5.00503 on Debian 2.2r2 "Espy"
 against: Catalyst 2916/24, 5500, 

=head1 Updated

 2001-04-25   14:15
   Add password timeout
   Simplify foreach $scalar(@array) to 'for(@array)' and '$_'
   Hashimafy file scalars
   Unsubify non-redundant code
   Mixed-case subroutines
   Eliminate global vars with my(,) instead of use vars qw()
   Reformat to 75 chars/line max
 2001-03-29
   Eliminate unnecessary quotes
   Change double to single quotes for strings
   @keep case-insensitive
 2000-09-20
   Initial working code

=head1 ToDos

 Use File::Slurp to append log (avoid race condition)
 Use File::Temp instead of multiple temp files

=head1 Author

Don [ybiC] Royer

=head1 Credits

 Thanks to tilly, fastolfe, japhy, geektron and chromatic for
   suggestions and critique,
 and to tilly, mkmcconn and boo_radley for
   cat filename | sed -e 's/$/.subdomain.dom/' > newfilename
 to fully-qualify hostnames in resulting outfiles so they can be fed
   back in as infiles.

=cut
Replies are listed 'Best First'.
Re: Cisco Neighbors
by fingers (Acolyte) on May 16, 2001 at 21:56 UTC
    I put together a similar script that works using SNMP and automatically polls the IPs it gets back from the SNMP queries.

    Thanks for giving me the idea to write it, and answering questions for me.

    cdppoll.pl
      I am getting the following error.

      181: $seen{$item}; # parse $file{in} into a hash
      Useless use of hash element in void context at c:\temp\dir39FB.tmp\cdp poll1.pl line 181.

      67: Usage() unless ((@ARGV) or (-s $file{in} && -T_));
      Undefined subroutine &main::Usage called at c:\temp\dir39FB.tmp\cdp poll1.pl lin e 67.
      Press any key to continue . . .
      Any suggestion?