Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

host-switchport mapper

by colakong (Initiate)
on Dec 07, 2007 at 18:59 UTC ( [id://655718]=sourcecode: print w/replies, xml ) Need Help??
Category: Networking Code
Author/Contact Info Cedric Nelson cedric.nelson@gmail.com
Description: A utility for describing where hosts are physically connected. Queries Cisco switches for CAM tables, queries hosts for MAC information using wmic (available on WinXP/W2K3), compares CAM/MAC information for host-to-switch-and-port-and-vlan mapping.
#host-mapper
#version 0.022
#Cedric Nelson, November 2007
#cedric.nelson@gmail.com

use WWW::Mechanize;
use Getopt::Std;
use DBI;
use Crypt::SSLeay;

my ($flags,
    $dirty,
    @switches,
    @hosts,
    @nodes,
    $default_username,
    $default_password,
    $username,
    $password,
    $protocol,
    $port,
    $url,
    $realm,
    $database_file,
    $DSN,
    $dbh,
    $input_camdb,
    $input_macdb,
    $input_db_file,
    $output_file,
    @unaccounted_for);


#Set variables
$url = '/level/15/exec/-/show/mac-address-table/CR';
$realm = 'level_15_access';
#Example path:
#$database_file = '\\\\server\\share\\SysInfo.mdb';
$database_file = '';
$DSN = "driver=Microsoft Access Driver (*.mdb);dbq=$database_file";
$default_username = '';
$default_password = '';


#Parse invokation options.
if (@ARGV) {
  $flags = analyze_options(@ARGV);
  
  foreach my $flag (keys %{$flags}) {
    if ($flag !~ /^-([seupmcidqh])$/) {
      print "Unrecognized option: $flag\n\n";
      $dirty = 1;
      show_help();
    }
  }
  
  unless ($dirty == 1) {
    
    if (${$flags}{'-s'}) {
      foreach my $item (@{${$flags}{'-s'}}) {
        if (-e $item) {push @switches, parse_files_macro($item);}
        else {push @switches, $item;}
      }
    }
    
    if (${$flags}{'-e'}) {$port = '443';$protocol = 'https';}
    else {$port = '80';$protocol = 'http';}
    
    if (${$flags}{'-u'}) {$username = @{${$flags}{'-u'}}[0];}
    if (${$flags}{'-p'}) {$password = @{${$flags}{'-p'}}[0];} 
    unless (${$flags}{'-u'}) {$username = $default_username;}
    unless (${$flags}{'-p'}) {$password = $default_password;}
    
    if (${$flags}{'-m'}) {
      foreach my $item (@{${$flags}{'-m'}}) {
        if (-e $item) {push @nodes, parse_files_macro($item);}
        else {push @nodes, $item;}
      }
    }
    
    if (${$flags}{'-c'}) {$input_camdb = @{${$flags}{'-c'}}[0];}
    if (${$flags}{'-i'}) {$input_macdb = @{${$flags}{'-i'}}[0];}
    if (${$flags}{'-d'}) {$input_db_file = @{${$flags}{'-d'}}[0];}
    
    if (${$flags}{'-q'}) {
      foreach my $item (@{${$flags}{'-q'}}) {
        if (-e $item) {push @hosts, parse_files_macro($item);}
        else {push @hosts, $item;}
      }
    }
    unless (${$flags}{'-m'}) {@nodes = @hosts;}
      
    
    ###Actions based on flags###
    
    #Search for a host entry
    if (${$flags}{'-q'}) {
      
      #Search an existing database
      if (${$flags}{'-d'}) {
        my $mapped_macs_ref = load_mapdb($input_db_file);
        search_macdb($mapped_macs_ref, \&print_mac_entry, @hosts);
        
        print "Hosts unaccounted for:\n";
        foreach my $host (@unaccounted_for) {print "$host\n";}
      }
      
      #Search using combinations of other resources
      else {
        my $mapped_macs_ref = map_macro();
        search_macdb($mapped_macs_ref, \&print_mac_entry, @hosts);
        save_mapdb($mapped_macs_ref);
        
        print "Hosts unaccounted for:\n";
        foreach my $host (@unaccounted_for) {print "$host\n";}
      }      
    }
    
    #Create a host-map database
    elsif ((${$flags}{'-s'} || ${$flags}{'-c'}) && (${$flags}{'-m'} ||
+ ${$flags}{'-i'})) {
      my $mapped_macs_ref = map_macro();
      save_mapdb($mapped_macs_ref);
    }
    
    #Create a cam-database
    elsif (${$flags}{'-s'} || ${$flags}{'-c'}) {
      my $cam_ref = cam_macro();
      save_camdb($cam_ref);
    }
    
    #Create a mac-database
    elsif (${$flags}{'-m'} || ${$flags}{'-i'}) {
      my $mac_ref = mac_macro();
      save_macdb($mac_ref);
    }
    
    elsif (${$flags}{'-h'}) {show_help();}
   
   else {print "I don't understand this combination of options. Exitin
+g...\n";} 
  }
}

#Or display help if there are none.
else {show_help();}


### Macros Section ###

#Parse Files Macro
#Make an array of items by reading them from file(s)
sub parse_files_macro {
  my (@files) = @_;
  my (@items);
  
  foreach my $file (@files) {
    open FH, ($file) or warn "Can't open $file for parsing. Skipping i
+t.\n";
    while (<FH>) {
      chomp($_);
      push @items, $_;
    }
    close FH;
  }
  return @items;
}

#CAM Table Macro
#1. Build trunkport-hash from switches
#2. Get cam-hash from file or from switches
sub cam_macro {
  my ($cam_ref);
  
  #Get cam-hash  
  if (${$flags}{'-c'}) {$cam_ref = load_camdb($input_camdb);}
  elsif (${$flags}{'-s'}) {
    
    trunk_macro();
    
    foreach my $ip (@switches) {
      my $dirty_output = get_data($ip, $port, $realm, $username, $pass
+word, $url);
      $cam_ref = parse_cam($dirty_output);
    }
  }
  else {die "In order to proceed I need a CAM table specified.\n";}
  return ($cam_ref);
}

#MAC Table Macro
#1. Get mac-hash from a file, host(s), or from default database
sub mac_macro {
  my ($mac_ref);
  
  #Get mac-hash
  if (${$flags}{'-i'}) {$mac_ref = load_macdb($input_macdb);}
  elsif (${$flags}{'-m'}) {
    $mac_ref = get_macs(@nodes);
  }
  elsif (-e $database_file) {$mac_ref = get_mac_db();}
  elsif (${$flags}{'-q'}) {
    $mac_ref = get_macs(@nodes);
  }
  else {die "In order to proceed I need a host-MAC table specified.\n"
+;}
  return ($mac_ref);
}

#Trunk Macro
#1. Get trunkport-hash from switches
sub trunk_macro {
  my ($trunk_ref);
  
  my $url = '/level/15/exec/-/show/cdp/neighbors/CR';
  
  foreach my $ip (@switches) {
    my $dirty_output = get_data($ip, $port, $realm, $username, $passwo
+rd, $url);
    $trunk_ref = parse_trunks($dirty_output);
  }
  return ($trunk_ref);
} #End trunk_macro

#Map Macro
#1. Get cam-hash
#2. Get mac-hash
#4. Map MACs
sub map_macro {
  my ($cam_ref,
      $mac_ref,
      $mapped_macs_ref);
  
  $cam_ref = cam_macro();
  $mac_ref = mac_macro();
  
  #Map MACs
  $mapped_macs_ref = map_macs($cam_ref, $mac_ref);
  return ($cam_ref, $mac_ref, $mapped_macs_ref);
} #End map_macro


### Subroutine Section ###

#Anonymous code block for scoping %options, @list properly.
{
  my %options;
  my @list;
  #Defines relationship of invokation arguments as a hash.
  sub analyze_options {
    my ($element);
    ($element, @list) = @_;
    #Add new options to the hash.
    unless ($options{$element}) {$options{$element} = [];}
    #Parse the list of arguments.
    while (@list) {
      my $item = shift(@list);
      #If the element is an option, parse it with remaining arguments.
      if ($item =~ /^-/) {analyze_options($item, @list);}
      #If the element is an argument, it belongs to the preceding opti
+on.
      else {push @{$options{$element}}, $item;}
    }
    #Return a reference to your hash of options.
    return \%options;
  } #End analyze_options
} #End anonymous code block.

#Grabs html data from a switch, and passes it to a function.
sub get_data {
  my ($ip, $port, $realm, $username, $password, $url, $function) = @_;
  my $spider;
  
  $spider = WWW::Mechanize->new(autocheck => 1);
  $spider->credentials("$ip:$port", $realm, $username, $password);
  $spider->get("$protocol://$ip$url");

  if (defined($function)) {$function->($spider->content());}  
  else {return $spider->content();}
} #End get_data

#Anonymous code block for get_macs
{
  my %macs;
  #Retrieve MAC information from host(s)
  sub get_macs {
     my (@hosts) = @_;
     
    unless (@hosts) {@hosts = 'localhost';}
     
    foreach my $host (@hosts) {
      my ($command,
          $output,
          $mac);
          
      $command = "wmic /node:'$host' nic where NetConnectionID='Local 
+Area Connection' get MACAddress";
      @output = `$command`;
      
      #Collect all MAC addresses from output
      foreach my $entry (@output) {
        if ($entry =~ /([a-f0-9]{2}:){5}[a-f0-9]{2}/i) {
          $mac = $&;
          $mac =~ s/://g;
          $mac =~ s/([a-z0-9]{4})/$1\./gi;
          $mac =~ s/\.$//;
          unless ($macs{$mac}) {$macs{$mac} = $host;}
        }
      }      
    }
    return \%macs;
  } #End get_macs
}#End anonymous code block

#Anonymous code block
{
  my %cam;
  #Parse html data for MAC information
  #Note that parse_cam is dependant on data created from parse_trunks
  sub parse_cam {
    my ($dirty_output, $function) = @_;
    my ($output,
        @lines,
        $hostname);
    
    #Parse data for hostname
    if ($dirty_output =~ m{<BODY><H1>(.*)</H1><PRE>}) {$hostname = $1;
+}
    else {$hostname = 'unknown_device';}
    
    #Cut out un-neeeded output
    if ($dirty_output =~ m{----    -----------       --------    -----
+(.*)Total Mac Addresses for this criterion}s) {
      $output = $1;
    }
    else {print "No match found for MACs regex.\n";}
    
    #Parse data for MACs
    @lines = split /\n/, $output;
    shift @lines;
    
    #Format MACs and re-assign to hash
    #Table format: Switch,VLAN,MAC,Type,Port
    #Hash format: MAC => [Switch, Port, VLAN]
    foreach my $line (@lines) {
      $line =~ s/^ +//;
      $line =~ s/( )+/,/g;
      $line = "$hostname," . $line;
      my ($switch, $vlan, $mac, $type, $port)    = split /,/, $line;
      $port =~ tr/\r\n//d;
      
      if (is_host_port($hostname, $port)) {
        unless ($cam{$mac}) {$cam{$mac} = [$switch, $port, $vlan];}
      }
    }
    if (defined($function)) {$function->(\%cam);}
    else {return \%cam;}
  } #End parse_cam
} #End anonymous code block

#Anonymous code block
{
  my %switch_trunks;
  #Parse html data for trunk-port information
  #This is dependant on cdp being enabled
  sub parse_trunks {
    my ($dirty_output, $function) = @_;
    my ($hostname,
        $output,
        @lines);
    
    if ($dirty_output =~ m{<BODY><H1>(.*)</H1><PRE>}) {$hostname = $1;
+}
    else {$hostname = 'unknown_device';}
    
    #Isolate CDP information
    if ($dirty_output =~ m{Port ID(.*)</DL>}s) {$output = $1;}
    else {print "No match found for CDP regex.\n";}
    
    #Parse Trunk-port information
    @lines = split /\n/, $output;
    foreach (@lines) {
      #Isolate trunk-port entries with the format:
      #<interface> <number>/<number> or <interface> <number>
      if ($_ =~ /(\w+ \d+\/\d+|\w+ \d+)/) {
        my $match = $1;
        #Format trunk-port to match mac-address-table output
        #You may need to add to this, depending on what modules
        #your switches are using.
        $match =~ s/Gig /Gi/;
        
        #Add any trunk-ports to an array for this switch
        unless ($switch_trunks{$hostname}) {$switch_trunks{$hostname} 
+= [];}
        push @{$switch_trunks{$hostname}}, $match;
      }
    }
    if (defined($function)) {$function->(\%switch_trunks);}
    else {return \%switch_trunks;}
  } #End parse_trunks
  
  #Determine if a specified host is directly connected to switch
  sub is_host_port {
    my ($switch, $switch_port) = @_;
    my $is_host_port = 'maybe';
    
    foreach my $trunk_port (@{$switch_trunks{$switch}}) {
      if ($switch_port eq $trunk_port) {$is_host_port = 'no';}
    }
    if ($is_host_port eq 'no') {return 0;}
    else {return 1;}
  } #End is_access_port
} #End anonymous code block

#Anonymous block for get_mac_db
{
  my %macs;
  #Read the default host-mac db
    sub get_mac_db {
        $dbh = DBI->connect("dbi:ODBC:$DSN", '','') or die "$DBI::errs
+tr\n";
        my $sql = "SELECT ComputerName, MACAddress FROM tblSysInfo";
      my $sth = $dbh->prepare($sql);
      $sth->execute();
      my @row;
      while (@row = $sth->fetchrow_array) {
        my ($hostname, $macaddress) = @row;
        
        #Format MACs to conform to cisco's style
        if ($macaddress =~ /([a-z0-9]{2}:){5}[a-z0-9]{2}/i) {
          $macaddress =~ s/://g;
          $macaddress =~ s/([a-z0-9]{4})/$1\./gi;
          $macaddress =~ s/\.$//;
        }        
        $macs{$macaddress} = $hostname;
      }
        return \%macs;
    } #End get_mac_db
} #End anonymous code block.

#Save mac database to csv file
sub save_mapdb {
  my ($output_hash) = @_;
  
  unless ($output_file) {$output_file = 'hostmap-database.csv';}
  open OUTPUT, ">$output_file" or die "Can't open $output_file for wri
+ting.\n";

  print OUTPUT "Hostname,Switch,Port,VLAN,MAC\n";
  while (my ($mac, $arrayref) = each %$output_hash) {
    my (@row) = (@$arrayref[0], @$arrayref[1], @$arrayref[2], @$arrayr
+ef[3], $mac);

    $line = join(",", @row);
    print OUTPUT ($line, "\n");
  }
  
  close OUTPUT;
} #End save_mapdb

#Anonymous block for load_mapdb
{
  my %mapped_mac_info;
  #Load a host-map from a file
  sub load_mapdb {
    my ($input_db) = @_;
    
    if (-e $input_db) {
      open INPUT, "<$input_db" or die "Can't open $input_db.\n";
      while (<INPUT>) {
        chomp($_);
        my ($line) = $_;
        my (@entry) = split /,/, $_;
        my ($hostname, $switch, $port, $vlan, $mac) = @entry;
        
        unless ($mac eq 'MAC') {
          $mapped_mac_info{$mac} = [$hostname, $switch, $port, $vlan];
        }       
      }
      close INPUT;
      return \%mapped_mac_info;
    }
    else {print "I can't open $input_db.\n";} 
  } #End load_mapdb
} #End anonymous code block


#Anonymous block for load_camdb
{
  my %cam_db;
  #Load a cam database from a file
  sub load_camdb {
    my ($input_db) = @_;
    
    if (-e $input_db) {
      open INPUT, "<$input_db" or die "Can't open $input_db.\n";
      while (<INPUT>) {
        chomp($_);
        my ($switch, $port, $vlan, $mac) = split /,/, $_;
        unless ($mac eq 'MAC') {
          $cam_db{$mac} = [$switch, $port, $vlan, $mac];
        }
      }
      close INPUT;
      return \%cam_db;
    }
    else {print "I can't open $input_db.\n";}
  } #End load_camdb
} #End anonymous code block

sub save_camdb {
  my ($cam_ref) = @_;
  
  open OUTPUT, ">cam-database.csv" or die "Can't open cam-database.csv
+ for writing.\n";
  print OUTPUT "Switch,Port,VLAN,MAC\n";
  while (my ($mac, $arrayref) = each %$cam_ref) {
    my (@row) = (@$arrayref[0],@$arrayref[1],@$arrayref[2],$mac);
    print OUTPUT (join(',', @row), "\n");
  }
  close OUTPUT;
} #End save_camdb

sub save_macdb {
  my ($mac_ref) = @_;
  
  open OUTPUT, ">mac-database.csv" or die "Can't open mac-database.csv
+ for writing.\n";
  print OUTPUT "Hostname, MAC\n";
  while (my ($mac, $hostname) = each %$mac_ref) {
    my (@row) = ($hostname, $mac);
    print OUTPUT (join(',', @row), "\n");
  }
  close OUTPUT;
} #End save_macdb

#Anonymous block for load_macdb
{
  my %mac_db;
  #Load a mac database from a file
  sub load_macdb {
    my ($input_db) = @_;
    
    if (-e $input_db) {
      open INPUT, "<$input_db" or die "Can't open $input_db.\n";
      while (<INPUT>) {
        chomp($_);
        my ($hostname, $mac) = split /,/, $_;
        unless ($mac eq 'MAC') {
          $mac_db{$mac} = $hostname;
        }
      }
      close INPUT;
      return \%mac_db;
    }
    else {print "I can't open $input_db.\n";}
  } #End load_macdb
} #End anonymous code block

#Search a macdb for a host, and take action for matches.
sub search_macdb {
  my ($macdb_ref, $action_ref, @hosts) = @_;
  
  foreach $host (@hosts) {
    my ($match_found) = 0;
    while (my ($host_mac, $cam_array_ref) = each %$macdb_ref) {
      my ($macdb_host) = @$cam_array_ref[0];
      if ($host =~ /$macdb_host/i) {
        my ($switch, $port, $vlan) = (@$cam_array_ref[1], @$cam_array_
+ref[2], @$cam_array_ref[3]);
        $action_ref->($host, $switch, $port, $vlan);
        $match_found = 1;
      }
    }
    if ($match_found == 0) {
      print "No entry for $host found.\n\n";
      push @unaccounted_for, $host;
    }
  }
} #End search_macdb

#Prints a mac entry.
sub print_mac_entry {
  my ($host, $switch, $port, $vlan) = @_;
  print "Hostname: $host\nSwitch: $switch\nPort: $port\nVLAN: $vlan\n\
+n";
} #End print_mac_entry

#Anonymous block for map_macs
{
  my %mapped_mac_info;
  #Maps hostnames to ports using the MAC address
  sub map_macs {
    my ($switch_mac_hash, $host_mac_hash) = @_;

    #There should be a more efficient way to find MAC matches
    while (my ($switch_mac, $cam_array_ref) = each %$switch_mac_hash) 
+{
      while (my ($host_mac, $hostname) = each %$host_mac_hash) {
        if ($switch_mac =~ /$host_mac/i) {
          my ($switch, $port, $vlan) = (@$cam_array_ref[0], @$cam_arra
+y_ref[1], @$cam_array_ref[2]);
          unless ($mapped_mac_info{$host_mac}) {
            $mapped_mac_info{$host_mac} = [$hostname, $switch, $port, 
+$vlan];
          }
        }
      }
    }

    return \%mapped_mac_info;
  } #End map_macs
} #End anonymous code block.

sub show_help {
  my ($error) = @_;
  if ($error) {print "$error\n\n";}
  
  my ($help) = <<END;
host-mapper
Version 0.022
Cedric Nelson, 2007

A utility for describing where hosts are physically connected.
    
    
    Usage:  host-mapper <options>
    
    
    Options:

    -s <sw | file>...         Query switch(es) for CAM table(s).
                              (Used alone, -s will save a cam-database
+)
                              
    -e                        Establish encrypted connection to switch
+(es).
    -u <...>                  Username for connection to switch(es).
    -p <...>                  Password for connection to switch(es).

    
    -m <host | file>...       Query host(s) for MAC address(es).
                              (Used alone, -m will save a mac-database
+)

    (Using -s & -m alone will save a host-map database.)
    
    
    -c <file>                 Use cam-database from file.
                              (A substitute for -s)
                              
    -i <file>                 Use mac-database from file.
                              (A substitute for -m)
                              
    -d <file>                 Use host-map database from file.
                              (A substitute for -s and -m)

    
    -q <host | file>...       Query host-map for specified host(s).
                              (Used with -s & -m, or -d)
    
    -h                        Display help.
END
  print "$help\n";
} #End show_help

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: sourcecode [id://655718]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others cooling their heels in the Monastery: (2)
As of 2024-04-19 01:39 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found