# # Routines for reading, cacheing, searching, and saving bridge table from # network equipment. Works with 3Com SuperStack II 3900/9300s and CoreBuilder 3500s, # and other equipment which supports the dot1dTp group from BRIDGE-MIB. # # $Id: BridgeTable.pm,v 1.35 2002/04/11 13:45:01 trockij Exp $ # # Copyright (C) 1998 Jim Trocki # # 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 # package A3Com::BridgeTable; use strict; use vars qw($VERSION); use A3Com::Base; use Expect; use SNMP; use Data::Dumper; $VERSION = "0.05"; sub _convert_bridgetable_3com; sub _convert_bridgetable_fdry; sub new { my $proto = shift; my %vars = @_; my $class = ref($proto) || $proto; my $self = {}; $self->{"PORTS"} = {}; $self->{"MACS"} = {}; $self->{"HIST_START"} = undef; $self->{"HIGHEST_PORT"} = undef; $self->{"ERROR"} = ""; $self->{"SWITCH"} = ""; $self->{"READ_METHOD"} = \&snmp_read; $self->{"LOGIN"} = "read"; $self->{"PASSWORD"} = ""; $self->{"READ_COMMUNITY"} = "public"; $self->{"WRITE_COMMUNITY"} = "public"; $self->{"ENABLE"} = ""; $self->{"LOG"} = 0; $self->{"CACHE"} = 1; $self->{"GLOBALCACHE"} = 1; $self->{"CACHETIME"} = $A3Com::Base::DEFAULT_CACHETIME; $self->{"FILE"} = ""; $self->{"TYPE"} = ""; $self->{"GLOBALCACHEDIR"} = "/usr/local/share/a3com"; if (defined $ENV{"HOME"}) { $self->{CACHEDIR} = $ENV{"HOME"} . "/.a3com"; } else { $self->{CACHEDIR} = ".a3com"; } my %c = &A3Com::Base::_read_conf ($self, $ENV{"A3COM_CONF"}); for (keys %c) { $self->{$_} = $c{$_}; } if (defined $ENV{"A3COM_GLOBALCACHEDIR"}) { $self->{"GLOBALCACHEDIR"} = $ENV{"A3COM_GLOBALCACHEDIR"}; } if (defined $ENV{"A3COM_CACHEDIR"}) { $self->{"CACHEDIR"} = $ENV{"A3COM_CACHEDIR"}; } $self->{_LOADED} = 0; for my $k (keys %vars) { $self->{$k} = $vars{$k} if ($vars{$k} ne ""); } bless ($self, $class); return $self; } sub cachedir { my $self = shift; if (@_) { $self->{CACHEDIR} = shift } return $self->{CACHEDIR}; } sub globalcachedir { my $self = shift; if (@_) { $self->{"GLOBALCACHEDIR"} = shift } return $self->{"GLOBALCACHEDIR"}; } sub cachetime { my $self = shift; if (@_) { $self->{CACHETIME} = shift } return $self->{CACHETIME}; } sub switch { my $self = shift; if (@_) { $self->{SWITCH} = shift } return $self->{SWITCH}; } sub error { my $self = shift; if (@_) { $self->{ERROR} = shift } return $self->{ERROR}; } sub log { my $self = shift; if (@_) { $self->{LOG} = shift } return $self->{LOG}; } sub cache { my $self = shift; if (@_) { $self->{CACHE} = shift } return $self->{CACHE}; } sub hist_start { my $self = shift; if (@_) { $self->{"HIST_START"} = shift } return $self->{"HIST_START"}; } sub globalcache { my $self = shift; if (@_) { $self->{"GLOBALCACHE"} = shift } return $self->{"GLOBALCACHE"}; } sub port { my $self = shift; my $portnum = shift; $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); if (!defined $self->{"PORTS"}->{$portnum}) { $self->{"ERROR"} = "port $portnum not found"; return undef; } return @{$self->{"PORTS"}->{$portnum}}; } sub ports { my $self = shift; $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); return keys %{$self->{"PORTS"}}; } sub mac { my $self = shift; my $macaddr = A3Com::Base::_mac_normalize (shift); $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); if (!defined $self->{MACS}->{$macaddr}) { $self->{"ERROR"} = "mac $macaddr not found"; return undef; } return @{$self->{"MACS"}->{$macaddr}}; } sub macs { my $self = shift; $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); return keys %{$self->{"MACS"}}; } sub btable { my $self = shift; $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); return %{$self->{"PORTS"}}; } # # write bridge table # sub file_write { my $self = shift; my $file = shift; my ($port); $self->{"ERROR"} = ""; if (!defined $file) { my $cd; if ($self->{"GLOBALCACHE"}) { $cd = "GLOBALCACHEDIR"; } else { $cd = "CACHEDIR"; } if (! -d $self->{$cd}) { $self->{"ERROR"} = "$cd is not a directory"; return undef; } $file = "$self->{$cd}/$self->{SWITCH}.bt"; } if (!open (O, ">$file")) { $self->{"ERROR"} = "$!"; return undef; } print O "#\n# bridge table for $self->{SWITCH} at " . time . "\n#\n"; foreach $port (sort {$a <=> $b} keys %{$self->{"PORTS"}}) { print O "$port @{$self->{PORTS}->{$port}}\n"; } close (O); } # # read bridge table # sub file_read { my $self = shift; my $file = shift; my ($port, $mac, @macs); $self->{"ERROR"} = ""; if (!defined $file) { if (! -d $self->{"CACHEDIR"}) { $self->{"ERROR"} = "CACHEDIR is not a directory"; return undef; } $file = "$self->{CACHEDIR}/$self->{SWITCH}.bt"; } if (!open (I, $file)) { $self->{"ERROR"} = "$!"; return undef; } $self->{"PORTS"} = {}; $self->{"MACS"} = {}; while () { chomp; next if (/^\s*#/ || /^\s*$/); ($port, @macs) = split; @{$self->{"PORTS"}->{$port}} = @macs; for (@macs) { push @{$self->{"MACS"}->{$_}}, $port; } } close (I); } # # returns 2-element array, anonymous refs to # porthash and machash. # # If first element is undef, then a failure ocurred, # and error text is in second element. # sub telnet_read_3com { my $self = shift; my ($ADDRS, $s, @n); $s = Expect->spawn ("telnet $self->{SWITCH}"); if (!$s) { $self->{"ERROR"} = "could not telnet to $self->{SWITCH}"; return undef; } $s->log_stdout($self->{"LOG"}); # # log in # if (!$s->expect (15, ("Select access level (read, write, administer): "))) { $self->{"ERROR"} = "did not get login prompt in time"; return undef; } $s->clear_accum; print $s "$self->{LOGIN}\r"; if (!$s->expect (8, ("Password:"))) { $self->{"ERROR"} = "did not get password prompt"; return undef; } $s->clear_accum; print $s "$self->{PASSWORD}\r"; @n = $s->expect (8, "Incorrect password", "Select menu option: "); if ($n[0] == 1) { $s->hard_close(); $self->{"ERROR"} = "incorrect password for $self->{SWITCH}"; return undef; } elsif (!defined $n[0] && $n[1] eq "1:TIMEOUT") { $s->hard_close(); $self->{"ERROR"} = "timeout logging in to $self->{SWITCH}, it says {$n[3]}"; return undef; } $s->clear_accum; print $s "bridge port addr list\r"; @n = $s->expect (10, "Enter VLAN interface indexes", "Select bridge ports"); $s->clear_accum; if (!defined ($n[0])) { $s->hard_close(); $self->{"ERROR"} = "error ($n[1]) during VLAN/bridge port interface prompt $self->{SWITCH}\n$n[3]"; return undef; } elsif ($n[0] == 1) { print $s "all\r"; @n = $s->expect (45, "Select bridge ports"); $s->clear_accum; if (!defined ($n[0])) { $s->hard_close(); $self->{"ERROR"} = "error ($n[1]) during bridge ports prompt $self->{SWITCH}\n$n[3]"; return undef; } } print $s "all\r"; @n = $s->expect (120, "Select menu option:"); $ADDRS = $n[3]; $s->clear_accum; # # log out politely # print $s "logout\r"; if ($s->expect (8, "Exiting") != 1) { $s->hard_close(); $self->{"ERROR"} = "did not get logout confirmation"; return undef; } $s->hard_close(); _convert_bridgetable_3com $self, $ADDRS; } sub telnet_read_fdry { my $self = shift; my ($ADDRS, $s, @n); $s = Expect->spawn ("telnet $self->{SWITCH}"); if (!$s) { $self->{"ERROR"} = "could not telnet to $self->{SWITCH}"; return undef; } $s->log_stdout($self->{"LOG"}); # # log in # if (!$s->expect (15, ("Please Enter Login Name : "))) { $s->hard_close(); $self->{"ERROR"} = "did not get login prompt in time"; return undef; } $s->clear_accum; print $s "$self->{LOGIN}\r"; if (!$s->expect (8, ("Please Enter Password:"))) { $s->hard_close(); $self->{"ERROR"} = "did not get password prompt"; return undef; } $s->clear_accum; print $s "$self->{PASSWORD}\r"; @n = $s->expect (8, "User login failure", "telnet@"); if ($n[0] == 1) { $s->hard_close(); $self->{"ERROR"} = "incorrect password for $self->{SWITCH}"; return undef; } elsif (!defined $n[0] && $n[1] eq "1:TIMEOUT") { $s->hard_close(); $self->{"ERROR"} = "timeout logging in to $self->{SWITCH}, it says {$n[3]}"; return undef; } $s->clear_accum; print $s "enable\r"; if (!$s->expect (15, ("Password:"))) { $s->hard_close(); $self->{"ERROR"} = "did not get login prompt in time"; return undef; } $s->clear_accum; print $s "$self->{ENABLE}\r"; @n = $s->expect (8, "Error - incorrect password", "#"); if ($n[0] == 1) { $s->hard_close(); $self->{"ERROR"} = "incorrect enable password for $self->{SWITCH}"; return undef; } elsif (!defined $n[0] && $n[1] eq "1:TIMEOUT") { $s->hard_close(); $self->{"ERROR"} = "timeout logging in to $self->{SWITCH}, it says {$n[3]}"; return undef; } $s->clear_accum; print $s "skip-page\r"; if (!$s->expect (8, "#")) { $s->hard_close(); $self->{"ERROR"} = "did not get prompt after skip-page in time"; return undef; } $s->clear_accum; print $s "sh mac\r"; @n = $s->expect (120, "#"); if ($n[1] ne undef) { $s->hard_close(); $self->{"ERROR"} = "error waiting for prompt after sh mac ($n[1])"; return undef; } $s->clear_accum; $ADDRS = $n[3]; # # log out politely # print $s "exit\r"; print $s "exit\r"; if (0) { if ($s->expect (8, "Connection closed") != 1) { $s->hard_close(); $self->{"ERROR"} = "did not get logout confirmation"; return undef; } } $s->hard_close(); _convert_bridgetable_fdry $self, $ADDRS; } sub _convert_bridgetable_fdry { my $self = shift; my $ADDRS = shift; foreach my $line (split (/\r\n|\n/, $ADDRS)) { next if ($line !~ /^([0-9a-f]{4})\.([0-9a-z]{4})\.([0-9a-z]{4})\s+(\S+)/); my $mac = "$1$2$3"; my $port = $4; $mac =~ s/^(..)(..)(..)(..)(..)(..)/$1:$2:$3:$4:$5:$6/; $mac = A3Com::Base::_mac_normalize ($mac); # # convert the slot/port to ifindex value # my ($b, $p) = ($port =~ /(\d+)\/(\d+)/); my $port = ($b - 1) * 32 + $p; push @{$self->{"PORTS"}->{$port}}, $mac; push @{$self->{"MACS"}->{$mac}}, $port; } } sub _convert_bridgetable_3com { my $self = shift; my $ADDRS = shift; my ($l); my ($vlan, $port, $mac); $vlan = 0; while ($ADDRS =~ /([^\n\r]+\n\r)/sg) { $l = $1; $l =~ s/[\r\n]*$//; if ($l =~ /Addresses for port (\d+)/) { $port = $1; next; } elsif ($l =~ /There are no addresses for port (\d+)/) { $port = $1; @{$self->{"PORTS"}->{$port}} = (); } elsif ($l =~ /Addresses for.*interface\s+(\d+),\s+port\s+(\d+)/i) { ($vlan, $port) = ($1, $2); next; } elsif ($l =~ /There are no addresses for.*interface\s+(\d+),\s+port\s+(\d+)/i) { ($vlan, $port) = ($1, $2); @{$self->{"PORTS"}->{$port}} = (); next; } elsif ($l =~ /^\s*(\w\w-\w\w-\w\w-\w\w-\w\w-\w\w)/) { $mac = $1; $mac =~ s/-/:/g; $mac = A3Com::Base::_mac_normalize ($mac); push @{$self->{"PORTS"}->{$port}}, $mac; push @{$self->{"MACS"}->{$mac}}, $port; next; } } } sub snmp_read { my $self = shift; if ($self->{"SWITCH"} eq "") { $self->{"ERROR"} eq "no switch supplied"; return undef; } $ENV{"MIBS"} = "+A3COM-SWITCHING-SYSTEMS-MIB:BRIDGE-MIB"; my $s = new SNMP::Session ( DestHost => $self->{"SWITCH"}, Community => ($self->{"READ_COMMUNITY"} or "public"), UseEnums => 1, Timeout => 30_000_000, ); if (!defined $s) { $self->{"ERROR"} = "could not create session"; return undef; } my $sys = ""; my $sysver = ""; # # get system ID # if (1) { my $sysoid = $s->get("sysObjectID.0"); print STDERR "sysoid = [$sysoid]\n"; if (!defined $sysoid) { $self->{"ERROR"} = "could not retrieve sysObjectID"; return undef; } # # foundry # if ($sysoid =~ /^\.1\.3\.6\.1\.4\.1\.1991\.1\./) { print STDERR "sys = fdry\n"; $sys = "fdry"; # # get the software image version via # snAgImgVer # $sysver = $s->get(".1.3.6.1.4.1.1991.1.1.2.1.11.0"); print STDERR "sysver = $sysver\n"; } # # 3com # if ($sysoid =~ /^\.1\.3\.6\.1\.4\.1\.43\.1\.16\./) { $sys = "3com"; } } # # 3com bridge table # if ($self->{"TYPE"} eq "3com") { # # get revision via a3ComSysSystemSoftwareRevision.0 # my $rev = $s->get(["a3ComSysSystemSoftwareRevision", 0]); if (!defined $rev) { $self->{"ERROR"} = "error retrieving switch version:" . $s->{"ErrorStr"}; return undef; } if (unpack ("C", $rev) < 2) { return $self->telnet_read_3com; } my $row = new SNMP::VarList ( ["a3ComSysBridgeVlanPortAddressVlanIndex"], ["a3ComSysBridgeVlanPortAddressPortIndex"], ["a3ComSysBridgeVlanPortAddressIndex"], ["a3ComSysBridgeVlanPortAddressRemoteAddress"], ["a3ComSysBridgeVlanPortAddressType"], ["a3ComSysBridgeVlanPortAddressIsStatic"], ["a3ComSysBridgeVlanPortAddressStaticPort"], ["a3ComSysBridgeVlanPortAddressAge"], ); while ($s->getnext ($row)) { last if ($row->[0]->[0] ne "a3ComSysBridgeVlanPortAddressVlanIndex"); last if ($s->{"ErrorStr"} ne ""); $row->[3]->[2] = A3Com::Base::_mac_normalize ( sprintf "%02x:%02x:%02x:%02x:%02x:%02x", unpack ("C6", $row->[3]->[2]) ); push @{$self->{PORTS}->{$row->[1]->[2]}}, $row->[3]->[2]; push @{$self->{MACS}->{$row->[3]->[2]}}, $row->[1]->[2]; } } # # BRIDGE-MIB # elsif ($self->{"TYPE"} eq "BRIDGE-MIB" || ($sys eq "fdry" && $sysver =~ /^07\.1/) || ($self->{"TYPE"} eq "" && ($sys eq "fdry" && $sysver !~ /^06\./))) { print STDERR "using bridge mib\n"; my $row = new SNMP::VarList ( ["dot1dTpFdbAddress"], ["dot1dTpFdbPort"], ["dot1dTpFdbStatus"], ); if (!defined $row) { $self->{"ERROR"} = $s->{"ErrorStr"}; return undef; } while ($s->getnext ($row)) { last if ($row->[0]->[0] ne "dot1dTpFdbAddress"); last if ($s->{"ErrorStr"} ne ""); $row->[0]->[2] = A3Com::Base::_mac_normalize ( sprintf "%02x:%02x:%02x:%02x:%02x:%02x", unpack ("C6", $row->[0]->[2]) ); push @{$self->{PORTS}->{$row->[1]->[2]}}, $row->[0]->[2]; push @{$self->{MACS}->{$row->[0]->[2]}}, $row->[1]->[2]; } } # # foundry without bridge-mib # elsif ($self->{"TYPE"} eq "FDRY" || ($sys eq "fdry" && $sysver =~ /^06\./)) { print STDERR "type is fdry\n"; return $self->telnet_read_fdry; } if ($s->{"ErrorStr"} ne "") { $self->{"ERROR"} = $s->{"ErrorStr"}; return undef; } } sub _load_cache { my $self = shift; if (!$self->{"_LOADED"}) { if ($self->{"GLOBALCACHE"}) { return undef if (!defined (A3Com::Base::_load_global ($self, ".bt"))); } else { return undef if (!defined (A3Com::Base::_load ($self, ".bt"))); } $self->{"_LOADED"} = 1; } } # # load bridge table history structure from a file # # returns ("port1" => {"mac1" => time1, "mac2" => time2,}, "port2" => ...) # sub load_history { my $self = shift; my $file = shift; my %a; $self->{"ERROR"} = ""; if (!defined $file) { if (! -d $self->{"GLOBALCACHEDIR"}) { $self->{"ERROR"} = "GLOBALCACHEDIR is not a directory"; return undef; } $file = "$self->{GLOBALCACHEDIR}/$self->{SWITCH}.bthist"; } return () if (! -f $file); if (!open (I, $file)) { $self->{"ERROR"} = "$!"; return undef; } my $port = ""; my $l; while (defined ($l = )) { next if ($l =~ /^\s*#/ || $l =~ /^\s*$/); if ($l =~ /^\s* hist_start \s* = \s* (\d+)/ix) { $self->{"HIST_START"} = $1; next; } if ($l =~ /^(\d+)$/) { $port = $1; next; } if ($port eq "") { # error next; } $l =~ /^\s+(([0-9a-f]{1,2}:){5}[0-9a-f]{1,2})\s+(\d+)\s*$/i; my ($m, $t) = ($1, $3); if ($t eq "" || $m eq "") { # error next; } $m = A3Com::Base::_mac_normalize ($m); $a{$port}->{$m} = $t; } close (I); return %a; } # # save bridge table history # # $c = current history # $file(optional) = filename # sub save_history { my $self = shift; my $c = shift; my $file = shift; $self->{"ERROR"} = ""; if (!defined $file) { if (! -d $self->{"GLOBALCACHEDIR"}) { $self->{"ERROR"} = "GLOBALCACHEDIR is not a directory"; return undef; } $file = "$self->{GLOBALCACHEDIR}/$self->{SWITCH}.bthist"; } if (!open (O, ">$file")) { $self->{"ERROR"} = "$!"; return undef; } my $t = time; print O "#\n# bridge table history for $self->{SWITCH} at $t\n#\n"; if (defined $self->{"HIST_START"}) { print O "hist_start = $self->{HIST_START}\n"; } foreach my $port (sort {$a <=> $b} keys %{$c}) { print O "$port\n"; foreach my $m (sort {$c->{$port}->{$a} <=> $c->{$port}->{$b}} keys %{$c->{$port}}) { print O " $m $c->{$port}->{$m}\n"; } } close (O); } # # Merge a current bridge table into a historic bridge table # # %a = $self->btable; # $c = \%a; # $h = history # $t = time # # returns merged history as # ("port1" => { "mac1" => time1, "mac2" => time2, }, "port2" => {}, ...) # sub merge_current_with_history { my $self = shift; my $c = shift; my $h = shift; my $t = shift; my %n = %{$h}; foreach my $port (keys %{$c}) { foreach my $m (@{$c->{$port}}) { if (!defined $n{$port}) { $n{$port}->{$m} = $t; } elsif (!defined ($n{$port}->{$m})) { $n{$port}->{$m} = $t; } elsif ($n{$port}->{$m} < $t) { $n{$port}->{$m} = $t; } } } return %n; } =head1 NAME A3Com::BridgeTable - Methods for collecting bridge tables from 3Com or other network gear which provides such information =head1 SYNOPSIS use A3Com::BridgeTable; my $bt = new A3Com::BridgeTable ( "SWITCH" => "name", "READ_COMMUNITY" => "public", "CACHE" => 0, ); my %btable = $bt->btable; if ($bt->error ne "") { die "error: " . $bt->error . "\n"; } foreachy my $port (keys %btable) { print "$port\n"; foreach my $mac (@{$btable{$port}}) { print " $mac\n"; } } =head1 DESCRIPTION This code used to support only 3com gear, but it has been extended to provide functionality for devices which support standards-based SNMP agents. The methods provided by this module operate on bridge tables from network devices. The tables may be extracted from the devices via SNMP and the BRIDGE-MIB's dot1dTpFdbTable, or by telnetting to the device and reading the bridge tables from the interactive interface. The telnet method is supported for 3com SuperStack II 3900/9300 (software <3.0) and Foundry FastIron and Bigiron routers which are running the 06.* firmware. The type of device and firmware is detected by first sampling the value of sysObjectID.0, then determining the vendor and further way of obtaining the firmware version. The default is to get the bridge table via SNMP and dot1dTpFdbTable. A global and local cacheing mechanism is supported so that each read of the bridge table does not need to talk to a bridge. The idea is to have a set of tools which run periodically (via cron or some other mechanism) which keep the caches populated and fresh, while other tools use the caches. =head1 METHODS =over 4 =item new my $bt = new A3Com::BridgeTable ( "SWITCH" => "switch hostname or ip address", "LOGIN" => "login name", "PASSWORD" => "password", "READ_COMMUNITY" => "snmp read community", "WRITE_COMMUNITY" => "snmp write community", "ENABLE" => "enable password", "GLOBALCACHEDIR" => "path to global cache", "CACHEDIR" => "path to personal cache", "LOG" => 0 or 1, "CACHE" => 0 or 1, "GLOBALCACHE" => 0 or 1, ); The default for GLOBALCACHEDIR is "/usr/local/share/a3com". This may be overridden by the environment variable "A3COM_GLOBALCACHEDIR" or by assigning a value to GLOBALCACHEDIR when the object is created. CACHEDIR defaults to $HOME/.a3com if HOME is defined, or to the environment variable A3COM_CACHEDIR if it is defined, otherwise it is ".a3com" in the working directory. When an object is created, global configuration file is read. This file is the A3COM_CONF environment variable, /etc/a3com.conf, or /usr/local/etc/a3com.conf, in that order. The format of this file is "name = value". Blank lines and lines beginning with # are skipped. These assignments are made in the same way as passing the arguments to the new method. For example, the config file may look like this: LOGIN=read READ_COMMUNITY=public GLOBALCACHEDIR=/var/lib/a3comcache =item cachedir ($path) Sets cache dir if $path is given, otherwise returns it. =item globalcachedir ($pat) Sets global cache dir if $path is given, otherwise returns it. =item cachetime ($secs) Sets cache time if $path is given, otherwise returns it. =item switch Sets switch name if $path is given, otherwise returns it. =item error Returns the error string of the last method call, or "" if there was no error. =item log (0 or 1) Sets or unsets logging. =item hist_start Returns the first history entry. =item globalcache (0 or 1) Enables the global cache (1) or disables it (0). =item port ($num) Returns a list of MAC addresses which have been learned or hard-coded for $port, or undef on error. =item ports Returns a hash indexed by port where each entry contains a ref to an array of the MAC addresses for that port. Returns undef on error. %h = ports; foreach my $port (keys %h) { print "$port @{$h{$port}}\n"; } =item mac ($mac_addr) Returns list of ports which have learned the source address $mac_addr, or undef if not found or error. =item macs Returns a list of MAC addresses which have been learned by the switch, or undef on error. =item btable Returns a hash indexed by port number which is the entire bridge table. Each entry in the hash is a ref to a list of MAC addresses learned on that port. =item file_write ($file) Write a bridge table to a file. If the filename is not supplied, then it attempts to write a file named "$SWITCH.bt" in the $CACHEDIR directory. Returns undef on error. =item file_read ($file) Reads a bridge table from a file. If the filename is not supplied, it attempts to read a file named "$SWITCH.bt" in the $CACHEDIR directory. Returns undef on error. =item load_history ($filename) Loads bridge table history from $filename and returns the hash on success, or undef on error. The hash looks like this: ( "port1" => { "mac1" => time1, "mac2" => time2, ... }, "port2" => { "mac1" => time1, "mac2" => time2, ... }, ... ) =item save_history ($histref, $filename) Saves bridge table history $histref to $filename and return undef on error. Writes a file which looks like this: hist_start = time port1 mac1 time mac2 time mac3 time port2 mac1 time mac2 time ... =item merge_current_with_history ($current, $history, $time) =back =head1 SEE ALSO =head1 HISTORY =cut