[Netpass-devel] NetPass/bin appstarter.pl,1.3,1.4 macscan.pl,1.10,1.11 portmover.pl,1.5,1.6 proc_cou
Brought to you by:
jeffmurphy
|
From: jeff m. <jef...@us...> - 2005-08-03 02:44:48
|
Update of /cvsroot/netpass/NetPass/bin In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv19022/bin Modified Files: appstarter.pl macscan.pl portmover.pl proc_counter.pl resetport.pl Log Message: some bug fixes, resetport.pl re-write, appstarter completion, install/initd tweaks Index: appstarter.pl =================================================================== RCS file: /cvsroot/netpass/NetPass/bin/appstarter.pl,v retrieving revision 1.3 retrieving revision 1.4 diff -u -d -r1.3 -r1.4 --- appstarter.pl 12 Apr 2005 14:18:11 -0000 1.3 +++ appstarter.pl 3 Aug 2005 02:44:38 -0000 1.4 @@ -82,9 +82,10 @@ netpass /etc/init.d/netpass npcfgd /opt/netpass/bin/npcfgd.pl npstatusd /opt/netpass/bin/npstatusd.pl - npsnortd /opt/netpass/bin/npsnortd.pl - unquar-all /opt/netpass/bin/unquar-all.pl - quar-all /opt/netpass/bin/quar-all.pl + unquarall /opt/netpass/bin/bulk_moveport.pl -N 0.0.0.0/0 -a unquarantine + quarall /opt/netpass/bin/bulk_moveport.pl -N 0.0.0.0/0 -a quarantine + reload_nessus_plugins /opt/netpass/bin/update_nessus_plugins.sh + reload_snort_plugins /opt/netpass/bin/update_snort_plugins.sh Items in the /etc/init.d directory must accept the "stop" "start" "restart" and "status" command line parameter and must do the appropriate thing. @@ -125,27 +126,15 @@ use lib '/opt/netpass/lib'; use FileHandle; use Pod::Usage; +use Data::Dumper; + +use POSIX qw(:sys_wait_h setsid setuid setgid); use RUNONCE; use NetPass::LOG qw(_log _cont); my $myName = "appstarter"; -NetPass::LOG::init [ $myName, 'local0' ]; #*STDOUT; - -my $otherPid = RUNONCE::alreadyRunning($myName); - -if(defined($otherPid) && $otherPid) { - _log "ERROR", "i'm already running. pid=$otherPid\n"; - die "ERR: another copy of this script is already running pid=$otherPid"; -} - -require NetPass; -require NetPass::Config; - -$SIG{'ALRM'} = \&alarmHandler; - - my %opts; getopts('c:U:qnDh?', \%opts); pod2usage(2) if exists $opts{'h'} || exists $opts{'?'}; @@ -160,6 +149,32 @@ daemonize($myName, "/var/run/netpass"); } +if ($D) { + NetPass::LOG::init *STDOUT; +} else { + NetPass::LOG::init [ $myName, 'local0' ]; #*STDOUT; +} + +my $otherPid = RUNONCE::alreadyRunning($myName); + +if(defined($otherPid) && $otherPid) { + _log "ERROR", "i'm already running. pid=$otherPid\n"; + die "ERR: another copy of this script is already running pid=$otherPid"; +} + +require NetPass; +require NetPass::Config; + +sub REAPER { + my $child; + while (($child = waitpid(-1,WNOHANG)) > 0) { + } + $SIG{'CHLD'} = \&REAPER; +} + +$SIG{'ALRM'} = \&alarmHandler; +$SIG{'CHLD'} = \&REAPER; # just incase they fail to disassociate + print "new NP\n" if $D; my $np = new NetPass(-cstr => exists $opts{'c'} ? $opts{'c'} : undef, @@ -170,29 +185,124 @@ die "failed to connect to NetPass: $np" unless (ref($np) eq "NetPass"); while (1) { - _log "DEBUG", "wakeup: processing worklist\n" if $D; + _log ("DEBUG", "wakeup: processing worklist\n") if $D; RUNONCE::handleConnection(); - _log "DEBUG", "sleeping for 10 seconds.\n" if $D; - print scalar localtime(time()), " sleeping...\n" if $D; + my $x = $np->db->getAppAction(); + if (ref($x) ne "ARRAY") { + _log("ERROR", "getAppAction failed: $x\n"); + } else { + _log("DEBUG", "Worklist: ". Dumper($x). "\n"); - select(undef, undef, undef, 10.0); + foreach my $row (@$x) { + if ($row->[2] eq "start") { + if (isRunning($row->[1])) { + _log("WARNING", $row->[1]. " is already running, so wont start another copy.\n"); + # behavior is to ack the duplicate.XXX + } else { + start($row); + } + } + elsif ($row->[2] eq "stop") { + if (!isRunning($row->[1])) { + _log("WARNING", $row->[1]. " is not running, so cant stop.\n"); + } else { + stop($row) unless !isRunning($row->[1]); + } + } + } + } + + _log ("DEBUG", "sleeping for 10 seconds.\n") if $D; + sleep(10); +} + +sub isRunning { + my $cn = shift; + + _log("DEBUG", "isRunning $cn\n") if $D; + + my @pids = (); + if ($cn =~ /^([u]{0,1}[n]{0,1})quarall$/) { + use Proc::ProcessTable; + my $pt = new Proc::ProcessTable; + my $un = $1; + foreach my $pte (@{$pt->table}) { + push @pids, $pte->pid + if ($pte->cmndline =~ /^reset:\s${un}quarantine/); + } + _log("DEBUG", "isRunning looking for $cn found: ".join(',',@pids)."\n") if $D; + return @pids; + } + _log("DEBUG", "shouldnt be here\n"); } +sub start { + my $row = shift; + my ($rowid, $cmd, $junk, $as) = @$row; -exit 0; + if ($cmd eq "quarall") { + runAs("/opt/netpass/bin/bulk_moveport.pl -N 0.0.0.0/0 -a quarantine", $as); + } + elsif ($cmd eq "unquarall") { + runAs("/opt/netpass/bin/bulk_moveport.pl -N 0.0.0.0/0 -a unquarantine", $as); + } +} + +sub stop { + my $cmd = shift; + if ($cmd eq "quarall") { + # search for "reset: quarantine" + } + elsif ($cmd eq "unquarall") { + # search for "reset: unquarantine" + } +} + +sub runAs { + my $cmd = shift; + my $as = shift; + $as ||= "netpass"; + my ($uid,$gid) = (getpwnam($as))[2,3]; + if (!defined($uid)) { + _log("ERROR", "no such user $as\n"); + return; + } + unless ($cmd) { + _log("ERROR", "cmd empty\n"); + return; + } + + _log("DEBUG", qq{exec'ing as $as cmd "$cmd"\n}) if $D; + my $child = fork; + return if ($child); # parent + + open STDIN, '/dev/null'; + open STDOUT, '>/dev/null'; + setsid; + if (setgid($gid)) { + _log("ERROR", "child $$ failed to setgid($gid) $!\n"); + exit 0; + } + if (setuid($uid)) { + _log("ERROR", "child $$ failed to setuid($uid) $!\n"); + exit 0; + } + exec($cmd); + _log("ERROR", "child $$ failed to exec($cmd) $!\n"); + exit 0; +} +exit 0; # borrowed from mailgraph.pl sub daemonize { - use POSIX 'setsid'; - my ($myname, $pidDir) = (shift, shift); chdir $pidDir or die "$myname: can't chdir to $pidDir: $!"; -w $pidDir or die "$myname: can't write to $pidDir\n"; Index: macscan.pl =================================================================== RCS file: /cvsroot/netpass/NetPass/bin/macscan.pl,v retrieving revision 1.10 retrieving revision 1.11 diff -u -d -r1.10 -r1.11 --- macscan.pl 27 Apr 2005 03:54:06 -0000 1.10 +++ macscan.pl 3 Aug 2005 02:44:38 -0000 1.11 @@ -243,12 +243,14 @@ -network => $nw); if ($macscan == 0) { - _log("INFO", "macscan is disabled for this port: $switch/$p ($nw)\n"); + # too verbose + #_log("INFO", "macscan is disabled for this port: $switch/$p ($nw)\n"); next; } if ($multi_mac ne "ALL_OK") { - _log("INFO", "multi_mac is $multi_mac for this port: $switch/$p ($nw)\n"); + # too verbose + #_log("INFO", "multi_mac is $multi_mac for this port: $switch/$p ($nw)\n"); next; } Index: proc_counter.pl =================================================================== RCS file: /cvsroot/netpass/NetPass/bin/proc_counter.pl,v retrieving revision 1.4 retrieving revision 1.5 diff -u -d -r1.4 -r1.5 --- proc_counter.pl 12 Apr 2005 15:24:08 -0000 1.4 +++ proc_counter.pl 3 Aug 2005 02:44:38 -0000 1.5 @@ -69,7 +69,7 @@ my $insert = "INSERT INTO stats_procs (serverid, dt, proc, count) VALUES (?,NOW(),?,?)"; $np->db->{'dbh'}->do($delete); -my $sth = $np->dbh->{'dbh'}->prepare($insert); +my $sth = $np->db->{'dbh'}->prepare($insert); my $t = new Proc::ProcessTable; Index: resetport.pl =================================================================== RCS file: /cvsroot/netpass/NetPass/bin/resetport.pl,v retrieving revision 1.14 retrieving revision 1.15 diff -u -d -r1.14 -r1.15 --- resetport.pl 2 Jun 2005 19:59:08 -0000 1.14 +++ resetport.pl 3 Aug 2005 02:44:38 -0000 1.15 @@ -14,11 +14,12 @@ =head1 SYNOPSIS - resetport.pl [-c cstr] [-U user/pass] [-nqDh?] <traplog> + resetport.pl [-c cstr] [-U user/pass] [-t thread-queue-size] [-nqDh?] <traplog> -n "not really" -q be quiet. exit status only. -D enable debugging -c db connect string + -t thread queue size -U user/pass db user[/pass] =head1 OPTIONS @@ -54,6 +55,16 @@ C<OPTIONS="-Lf /opt/netpass/log/snmptraps.log -p /var/run/snmptrapd.pid -F '%#04.4y-%#02.2m-%02.2l %#02.2h:%#02.2j:%#02.2k TRAP %N;%w;%q;%A;%v\n' "> +=item B<-t thead-queue-size> + +A number denoting how many switches to delegate to each thread for monitoring. +The default is 20. If you have 100 switches in your NetPass configuration, +5 threads will be spawned. Each thread will handle incoming link up/down +processing. + +Each thread requires a connection to the database, so don't set this number +too low or you'll needless use DB resources. + =back =head1 DESCRIPTION @@ -90,9 +101,15 @@ use File::Tail; use threads; use threads::shared; +use Data::Dumper; use RUNONCE; +BEGIN { + use Config; + $Config{useithreads} or die "Recompile Perl with threads to run this program."; +} + my $otherPid = RUNONCE::alreadyRunning('resetport'); require NetPass; @@ -105,7 +122,7 @@ my %opts; -getopts('vnqDc:U:h?', \%opts); +getopts('vnqDt:c:U:h?', \%opts); pod2usage(1) if $#ARGV != 0; pod2usage(2) if exists $opts{'h'} || exists $opts{'?'}; @@ -154,53 +171,408 @@ # a mac on the port. my $unq = {}; +my $quar = {}; my $threads = {}; -my $myself = threads->self; +my $me = threads->self; + +my $ps = exists $opts{'t'} ? $opts{'t'} : 20; +my $threadPool = {}; +my $swThrAffin = {}; + +_log("DEBUG", "creating $ps threads\n"); + +for (my $i = 0 ; $i < $ps ; $i++) { + my %thrq : shared; + $thrq{'q'} = &share({}); + $thrq{'u'} = &share({}); + $thrq{'workLoad'} = 0; + share($thrq{'workLoad'}); + my $thr = new threads(\&thread_entry, \%thrq); + my $tid = $thr->tid; + $threadPool->{$tid} = {}; + $threadPool->{$tid}->{thro} = $thr; + $threadPool->{$tid}->{thrq} = \%thrq; +} + +_log("DEBUG", "parent entering endless loop\n"); while (1) { + _log("DEBUG", $me->tid." parent awake. checking log file.\n"); my @lines = (); while ($fh->predict == 0) { push @lines, $fh->read; } RUNONCE::handleConnection(); - processLines($np, $unq, \@lines); + processLines($np, $unq, $quar, \@lines); - foreach my $switch (keys %$unq) { - if (!defined($threads->{$switch}) || - !$myself->object($threads->{$switch}->tid)) { - # a thread doesnt exist for this switch - $threads->{$switch} = threads->create(\&procUQ, $switch); - _log("INFO", "spawning thread to handle $switch\n"); + my $alreadyDidThisSwitch = {}; + + foreach my $switch (keys %$unq, keys %$quar) { + next if (exists $alreadyDidThisSwitch->{$switch}); + $alreadyDidThisSwitch->{$switch} = 1; + + _log("DEBUG", $me->tid." processing work read from log file\n"); + my $tid; + + # if this switch isnt being handled by an existing + # thread, find a thread to handle it + + if (! exists($swThrAffin->{$switch}) ) { + # find a thread to assign it to + $tid = findThread($threadPool); + _log("DEBUG", $me->tid." findThread says to assign $switch to ".$tid."\n"); + $swThrAffin->{$switch} = $tid; + } else { + $tid = $swThrAffin->{$switch}; + _log("DEBUG", $me->tid." assigning work for $switch to $tid\n"); + } + + # add the work to the thread's queue + + _log("DEBUG", $me->tid." adding work (".join(',',@{$unq->{$switch}}).") to ".$tid."'s U queue\n") + if exists($unq->{$switch}); + _log("DEBUG", $me->tid." adding work (".join(',',@{$quar->{$switch}}).") to ".$tid."'s Q queue\n") + if exists($quar->{$switch}); + + { + lock(%{$threadPool->{$tid}->{'thrq'}}); + if (! exists $threadPool->{$tid}->{'thrq'}->{'u'}->{$switch}) { + $threadPool->{$tid}->{'thrq'}->{'u'}->{$switch} = &share([]); + } + if (! exists $threadPool->{$tid}->{'thrq'}->{'q'}->{$switch}) { + $threadPool->{$tid}->{'thrq'}->{'q'}->{$switch} = &share([]); + } + if ( exists $unq->{$switch} ) { + push @{$threadPool->{$tid}->{'thrq'}->{'u'}->{$switch}}, @{$unq->{$switch}} + if ($#{$unq->{$switch}} > -1); + delete $unq->{$switch}; + } + if ( exists $quar->{$switch} ) { + push @{$threadPool->{$tid}->{'thrq'}->{'q'}->{$switch}}, @{$quar->{$switch}} + if ($#{$quar->{$switch}} > -1); + delete $quar->{$switch}; + } } } - $myself->yield; - sleep(10); + $me->yield; + _log("DEBUG", $me->tid." parent done assigning work. sleeping.\n"); + sleep(11); } exit 0; =head1 PROGRAMMERS DOC +=head2 Overview + +This application is split into N threads. The main thread watches the +snmptrap.log file and parses new lines that appear in that file. If the +log entry indicates a link down trap, the main thread handles logging +the quarantine request to the database. + +If the log entry indicates a link up trap, the main thread will pass +that information (the switch and port) to a worker thread. It's the +job of the worker thread to watch the switch port for the appearance +of a MAC address, evaluate it and possibly unquarantine the port based +upon what the results of the MAC evaluation are. + +=head2 findThread( ) + +Search through the available threads and select one to handle the (new) +switch. Right now, this is fairly simple, just add it to the thread that +has the least switches. In the future, we might want to track switch +time and avoid threads that are bogged down with slow switches. + +=cut + +sub findThread { + my $tp = shift; + + my %qLens; + my $firstSeen; + + foreach my $tid (keys %$tp) { + lock($tp->{$tid}->{thrq}); + $qLens{$tid} = $tp->{$tid}->{'thrq'}->{'workLoad'}; + $firstSeen = $tid unless $firstSeen; + } + + my $assignToMe = ''; + my $min = ''; + + foreach my $tid (keys %qLens) { + if ( ($min eq '') || ($qLens{$tid} < $min ) ) { + $assignToMe = $tid; + $min = $qLens{$tid}; + } + } + + return $assignToMe || $firstSeen; +} + +=head2 removeFromQCheck($privQ, $privQT, $publicU, $switch) + +Given the public unquarantine (linkup) queue and the private +quarantine (linkdown) queue, private quarantine (linkdown) time +list and a switch: + +if a port on the pub queue is also on the priv queue, remove +it from the priv queue and remove it from the priv queue time. + +=cut + +sub removeFromQCheck { + my $priv = shift; + my $privT = shift; + my $pub = shift; + my $sw = shift; + + return unless ( (ref($priv) eq "HASH") && + (ref($pub) eq "HASH") && + (ref($privT) eq "ARRAY") && + (exists $priv->{$sw}) && + (exists $pub->{$sw}) ); + + # strip the ports from the priv queue + + foreach my $port (@{$pub->{$sw}}) { + @{$priv->{$sw}} = grep !/^$port$/, @{$priv->{$sw}}; + } + + # remove any times that no longer have associated ports + + for (my $port = 1 ; $port <= $#{$privT->{$sw}} ; $port++) { + $privT->{$sw}->[$port] = undef + if (! grep /^$port$/, @{$priv->{$sw}}); + } +} + +=head2 removeFromUCheck($privU, $publicQ, $switch) + +Given the public quarantine (linkdown) queue and the private +unquarantine (linkup) queue and a switch: + +if a port on the pub queue is also on the priv queue, remove +it from the priv queue. + +=cut + +sub removeFromUCheck { + my $priv = shift; + my $pub = shift; + my $sw = shift; + + return unless ( (ref($priv) eq "HASH") && + (ref($pub) eq "HASH") && + (exists $priv->{$sw}) && + (exists $pub->{$sw}) ); + + # strip the ports from the priv queue + + foreach my $port (@{$pub->{$sw}}) { + @{$priv->{$sw}} = grep !/^$port$/, @{$priv->{$sw}}; + } +} + +=head2 thread_entry( ) + +This is the entry point for the worker threads. This routine sits +in an endless loop watching for new work to be placed on the +queue. When it sees new work, it moves it from the queue to a +private queue. It then periodically calls procUQ() to process +the work. procUQ() will remove the work from the private queue +when it is finished, or leave it on the private queue if it +was not able to process the work. + +=cut + +sub thread_entry { + my $self = threads->self; + my $thrq = shift; + + my $pq = { + 'q' => {} , + 'u' => {} , + 'qt' => {} + }; + + print $self->tid(), " connecting to DB\n" if $opts{'D'}; + + my ($dbuser, $dbpass) = exists $opts{'U'} ? split('/', $opts{'U'}) : (undef, undef); + + my $np = new NetPass(-cstr => exists $opts{'c'} ? $opts{'c'} : undef, + -dbuser => $dbuser, -dbpass => $dbpass, + -debug => exists $opts{'D'} ? 0 : 0, #XXX + -quiet => exists $opts{'q'} ? 1 : 0); + + + if (ref($np) ne "NetPass") { + _log("ERROR", "failed to connect to NetPass: $np\n"); + return; + } + + + while(1) { + my $didWork = 0; + { + lock($thrq); + + my $wl = workLoad($pq); + $thrq->{'workLoad'} = $wl; + + #print $self->tid, " wakeup wl=$wl\n"; + + # move work to the private queues, deleting it from + # the public queue. if the port is not already on + # the linkdown queue, record the current time (and associate + # it with the port) so we can implement the linkflap + # tolerance feature. + + # the ports coming are guaranteed (by 'processLines') to be + # unique. iow, you wont see the same port on both the + # linkup and linkdown queues. so... + + # if the port is on the private linkup queue, and we see + # it on the public (newly detected) linkdown queue, then + # remove it from the private linkup queue as link is now + # down and we dont want to continue to process it as + # if link is up. a port may persist on the linkup queue + # for a while if unquar-on-linkup is enabled and no mac + # has appeared on the port. + + # also, if the port is on the private linkdown queue, and + # we see it on the public (newly detected) linkup queue then + # remove it from the private linkdown queue. because link + # is now up and we no longer want to process it for the + # linkdown event. ports may persist on the linkdown queue + # for a while if linkflap tolerance is enabled. + + my $alreadyDidThisSwitch = {}; + + foreach my $sw (keys %{$thrq->{'q'}}, keys %{$thrq->{'u'}}) { + next if (exists $alreadyDidThisSwitch->{$sw}); + $alreadyDidThisSwitch->{$sw} = 1; + + if (! exists $pq->{'q'}->{$sw}) { + $pq->{'q'}->{$sw} = &share([]); + $pq->{'qt'}->{$sw} = &share([]); + $pq->{'u'}->{$sw} = &share([]); + } + + #print $self->tid, " sw=$sw moving u..\n"; + + # run thru the new unquarantine ports (linkup ports) + # and see if any of them are on the private + # linkdown queue. if they are, remove them from the + # priv linkdown queue (quar 'q' queue) + + removeFromQCheck($pq->{'q'}, $pq->{'qt'}, + $thrq->{'u'}, $sw); + + # run thru the new quarantine ports (linkdown ports) + # and see if any of them are on the private + # linkup queue. if they are, remove them from the + # priv linkup queue (unquar 'u' queue) + + removeFromUCheck($pq->{'u'}, $thrq->{'q'}, $sw); + + # push the port onto the unquarantine work queue + # for this switch and then uniq that queue to remove + # duplicates. empty the public queue. + + push @{$pq->{'u'}->{$sw}}, @{$thrq->{'u'}->{$sw}}; + $pq->{'u'}->{$sw} = uniq($pq->{'u'}->{$sw}); + $thrq->{'u'}->{$sw} = &share([]); + + # push the port onto the quarantine work queue + # for this switch. if the port wasn't already on + # the queue, record the current time so we can + # to the linkflap tolerance feature. empty the + # public queue. + + #print $self->tid, " sw=$sw moving q..\n"; + + ($pq->{'q'}->{$sw}, + $pq->{'qt'}->{$sw}) + = linkflap_starttime_calculation($pq->{'q'}->{$sw}, + $pq->{'qt'}->{$sw}, + $thrq->{'q'}->{$sw}); + + $pq->{'q'}->{$sw} = uniq($pq->{'q'}->{$sw}); + $thrq->{'q'}->{$sw} = &share([]); + $pq = procUQ($pq, $np); + } + + } + sleep(10) unless $didWork; + } +} + +sub linkflap_starttime_calculation { + my $priv = shift; # private queue (arrayref) + my $ptl = shift; # port time list (arrayref) + my $pub = shift; # public queue (arrayref) + + return undef unless (ref($priv) eq "ARRAY"); + return undef unless (ref($ptl) eq "ARRAY"); + return undef unless (ref($pub) eq "ARRAY"); + + foreach my $port (@{$pub}) { + if (grep (/^$port$/, @{$priv})) { + # port is already on the list + } else { + # record the time at which we added + # this port to the queue + $ptl->[$port] = time(); + push @{$priv}, $port; + } + } + + return ($priv, $ptl); +} + + +sub uniq { + my $ar = shift; + return unless (ref($ar) eq "ARRAY"); + my %h = map { $_ => $_ } @{$ar}; + $ar = [ sort keys %h ]; + return $ar; +} + +sub workLoad { + my $pq = shift; + my $wl = 0; + if (ref($pq) eq "HASH") { + foreach my $sw (keys %{$pq->{'u'}}) { + $wl += @{$pq->{'u'}->{$sw}}; + } + foreach my $sw (keys %{$pq->{'q'}}) { + $wl += @{$pq->{'q'}->{$sw}}; + } + } + return $wl; +} + =head2 processLines(\@lines) This routine will take an array ref containing lines read from the file -and will parse them. For lines that show linkdown, we will immediately -quarantine the port. For lines that show linkup, we'll stash the -switch/port into a work-list ($unq) if unquar-on-linkup is set. +and will parse them. For lines that show linkdown, we will place them +on the work queue to be delegated to a thread for handling. + +For lines that show linkup, we'll place them on the work queue +if unquar-on-linkup is set. Periodically, that list will be processed by another routine. =cut sub processLines { - my ($np, $unq) = (shift, shift); + my ($np, $unq, $quar) = (shift, shift, shift); my $lines = shift; - # reload this value to be sure we have the current one - my $unq_on_linkup = $np->cfg->policy(-key => 'UNQUAR_ON_LINKUP') || "0"; - while (defined(my $l = shift @{$lines})) { chomp $l; @@ -234,66 +606,33 @@ next; } - _log("DEBUG", "checking if resetport is enabled...\n") if exists $opts{'D'}; + _log("DEBUG", "$switch/$port checking if resetport is enabled...\n") if exists $opts{'D'}; if (resetPortEnabled($np, $switch, $port) == 0) { - _log("DEBUG", "reset port is disabled for $switch $port. skipping.\n"); + _log("DEBUG", "$switch/$port reset port is disabled for $switch $port. skipping.\n"); next; } - _log("DEBUG", "yes it is...\n") if exists $opts{'D'}; - - _log("DEBUG", "ttype=$ttype sw=$switch port=$port\n") if exists $opts{'D'}; - - if ($ttype == 2) { # LINKDOWN - _log("INFO", "LINKDOWN quarantine $switch / $port\n"); + _log("DEBUG", "$switch/$port yes, reserport is enabled and ttype=$ttype\n") if exists $opts{'D'}; - # if the link is down, and this port is on our linkup worklist, - # remove it. + if ($ttype == 2) { # LINKDOWN + _log("INFO", "$switch/$port LINKDOWN\n"); - if (exists $unq->{$switch}) { - { - my @pl; - lock($unq->{$switch}); - while (my $p = shift @{$unq->{$switch}}) { - next if ($p =~ /^$port$/); - push @pl, $p; - } - push @{$unq->{$switch}}, @pl; - } - } + # if the port is already on the linkup queue, remove it from that + # queue (because this event - linkdown - occurred at a later time) - if (exists $opts{'n'}) { - _log("DEBUG", " not really!\n") if exists $opts{'D'}; - _log("INFO", "-n flag given. not really doing it.\n"); - } else { - $np->db->requestMovePort(-switch => $switch, -port => $port, - -vlan => 'quarantine', - -by => 'resetport.pl') || - _log("ERROR", $np->db->error()); - _log ("DEBUG", " backfrom dbh->requestMovePort\n") - if exists $opts{'D'}; - } + @{$unq->{$switch}} = grep !/^$port$/, @{$unq->{$switch}} if (exists ($unq->{$switch})); + $quar->{$switch} = [] if (!exists($quar->{$switch})); + push @{$quar->{$switch}}, $port; } - elsif (($ttype == 3) && ($unq_on_linkup ne "0")) { # LINKUP - _log("INFO", "LINKUP (maybe) unquarantine $switch / $port\n"); + elsif ($ttype == 3) { # LINKUP + _log("INFO", "$switch/$port LINKUP\n"); - # just record the switch, port. we process quar-on-linkup in a separate - # routine + # if the port is already on the linkdown queue, remove it from that + # queue (because this event - linkdown - occurred at a later time - if (exists($unq->{$switch})) { - { - lock($unq->{$switch}); - if (!grep {/^$port/} @{$unq->{$switch}}) { - push @{$unq->{$switch}}, $port; - } - } - } else { - $unq->{$switch} = &share([]); - { - lock($unq->{$switch}); - push @{$unq->{$switch}}, $port; - } - } + @{$quar->{$switch}} = grep !/^$port$/, @{$quar->{$switch}} if (exists ($quar->{$switch})); + $unq->{$switch} = [] if (!exists($unq->{$switch})); + push @{$unq->{$switch}}, $port; } } } @@ -309,169 +648,265 @@ show any attached macs) will be left on the list and reviewed again the next time we are called. -=cut - -sub procUQ { - my $switch = shift; +A port will be reviewed for a maximum of 1 hour. If we don't see a MAC +appear in that time, we stop looking. +=cut - print "thread connecting to DB\n" if $opts{'D'}; +# - my ($dbuser, $dbpass) = exists $opts{'U'} ? split('/', $opts{'U'}) : (undef, undef); +sub procUQ { + my $pq = shift; + my $np = shift; - my $np = new NetPass(-cstr => exists $opts{'c'} ? $opts{'c'} : undef, - -dbuser => $dbuser, -dbpass => $dbpass, - -debug => exists $opts{'D'} ? 1 : 0, - -quiet => exists $opts{'q'} ? 1 : 0); - - # reload this value to be sure we have the current one - my $unq_on_linkup = $np->cfg->policy(-key => 'UNQUAR_ON_LINKUP') || "0"; + my $self = threads->self; - if (ref($np) ne "NetPass") { - _log("ERROR", "failed to connect to NetPass: $np\n"); - threads->join; - return 1; - } + # process unquarantine (linkup) events - my $cn = ($np->cfg->getCommunities($switch))[1]; + my $switches = uniq [ keys %{$pq->{'u'}}, keys %{$pq->{'q'}} ]; - while (1) { - my @failed = (); + foreach my $switch (@$switches) { + my $cn = ($np->cfg->getCommunities($switch))[1]; - { # start lock - lock($unq->{$switch}); + my $failed = {}; - goto endlock if ($#{$unq->{$switch}} < 0); + next if (!exists($pq->{'u'}->{$switch}) && !exists($pq->{'q'}->{$switch})); + next if (($#{$pq->{'u'}->{$switch}} == -1) && ($#{$pq->{'q'}->{$switch}} == -1)); my $snmp = new SNMP::Device('hostname' => $switch, 'snmp_community' => $cn); my ($mp, $pm) = $snmp->get_mac_port_table(); - foreach my $port (@{$unq->{$switch}}) { - # figure out what macs are on this port - - _log("DEBUG", "link up $switch $port and unq_lu is $unq_on_linkup\n"); - - print "fetch maclist ($unq_on_linkup)\n" if exists $opts{'D'}; - my $macList = $pm->{$port}; - if (!defined($macList)) { - _log ("ERROR", "we want to unquar on linkup, but $switch doesnt have mac information available for port $port yet!\n"); - push @failed, $port; - next; - } - - print "macList=".join(',', @$macList)."\n" if exists $opts{'D'}; - - if ($unq_on_linkup eq "1") { - print "unq=ON findRegMac\n" if exists $opts{'D'}; + if (exists ($pq->{'u'}->{$switch})) { + foreach my $port (@{$pq->{'u'}->{$switch}}) { + + # if the port is on the 'q' queue, remove it from that queue since + # link is now, apparently, up. + + if (exists ($pq->{'q'}->{$switch})) { + _log("DEBUG", $self->tid(). " $switch $port possibly removing from 'q'\n"); + @{$pq->{'q'}->{$switch}} = grep !/^$port$/, @{$pq->{'q'}->{$switch}}; + if (exists ($pq->{'qt'}->{$switch})) { + $pq->{'qt'}->{$switch}->[$port] = undef; + } + } + + my $unq_on_linkup = $np->cfg->policy(-key => 'UNQUAR_ON_LINKUP') || "0"; + my $rppt = $np->cfg->policy(-key => 'RESETPORT_PORT_POLL_TIME') || 0; - # in order to move the port to unquarantine - # we just need to call validateMac on the first - # registered mac address we found. + # if possible, we'll resolve the switch/port to a specific network and the + # look to see if the above policy settings are over-ridden at the network or + # netgroup level. - my ($regMac, $regMacStatus) = findRegMac($np, $macList); - if (!defined($regMac)) { - _log ("WARNING", "no macs registered on $switch $port. leaving in quarantine.\n"); - } else { - _log("DEBUG", "regMac $regMac $regMacStatus\n") if exists $opts{'D'}; + my $curNw = $np->cfg->getMatchingNetwork(-switch => $switch, -port => $port); + if ($curNw =~ /^\d/) { + _log("DEBUG", $self->tid(). " sw=$switch po=$port nw=$curNw\n"); + $unq_on_linkup = $np->cfg->policy(-key => 'UNQUAR_ON_LINKUP', -network => $curNw); + $rppt = $np->cfg->policy(-key => 'RESETPORT_PORT_POLL_TIME', -network => $curNw); + } + + # figure out what macs are on this port + + _log("DEBUG", $self->tid. " link up $switch $port and unq_lu=$unq_on_linkup rppt=$rppt\n"); + + print $self->tid. " fetch maclist\n" if exists $opts{'D'}; + + if (!exists ($failed->{$switch})) { + $failed->{$switch} = []; + $failed->{$switch."PT"} = []; + } + + my $macList = $pm->{$port}; + if (!defined($macList)) { + _log ("ERROR", $self->tid(). + " we want to unquar on linkup, but $switch doesnt have mac information available for port $port yet!\n"); + push @{$failed->{$switch}}, $port; + $failed->{$switch."PT"}->[$port] = time(); #XXX + next; + } + + print "macList=".join(',', @$macList)."\n" if exists $opts{'D'}; + + if ($unq_on_linkup eq "1") { + print $self->tid(), " unq=ON findRegMac\n" if exists $opts{'D'}; - _log ("DEBUG", "found a registered mac ($regMac) on $switch $port\n"); - # if we are alone on this port, and are UNQUAR - # then unquarantine us + # in order to move the port to unquarantine + # we just need to call validateMac on the first + # registered mac address we found. - if ($#{$macList} == 0) { - _log ("DEBUG", "$regMac is alone on $switch $port. status is $regMacStatus\n"); - if ($regMacStatus =~ /UNQUAR$/) { - _log ("DEBUG", "$regMac unquarantine $switch $port\n"); - if(exists $opts{'n'}) { - _log("DEBUG", "not really!\n"); - } else { - $np->db->requestMovePort(-switch => $switch, -port => $port, - -vlan => 'unquarantine', -by => 'resetport.pl') || - push @failed, $port; - } - } else { - _log ("DEBUG", "$regMac leave quar $switch $port\n"); - } + my ($regMac, $regMacStatus) = findRegMac($np, $macList); + if (!defined($regMac)) { + _log ("WARNING", $self->tid(). " no macs registered on $switch $port. leaving in quarantine.\n"); } else { - # if we are not alone, then enforceMultiMacPolicy - # and do whatever it says to do (quar or unquar) + _log("DEBUG", $self->tid(). " regMac $regMac $regMacStatus\n") if exists $opts{'D'}; - _log ("DEBUG", "$switch $port has more than one mac on it. enforceMultiMacPolicy\n"); + _log ("DEBUG", $self->tid(). " found a registered mac ($regMac) on $switch $port\n"); + # if we are alone on this port, and are UNQUAR + # then unquarantine us - my ($_rv, $_sw, $_po) = $np->enforceMultiMacPolicy($regMac, '', $regMacStatus, - $switch, $port, - undef, {$port => $macList}); - if ($_rv =~ /UNQUAR$/) { - _log ("DEBUG", "$switch $port multiMac said to unquarantine the port.\n"); - if (exists $opts{'n'}) { - _log("DEBUG", "not really!\n"); + if ($#{$macList} == 0) { + _log ("DEBUG", $self->tid(). " $regMac is alone on $switch $port. status is $regMacStatus\n"); + if ($regMacStatus =~ /UNQUAR$/) { + _log ("DEBUG", $self->tid(). " $regMac unquarantine $switch $port\n"); + if(exists $opts{'n'}) { + _log("DEBUG", $self->tid(). " not really!\n"); + } else { + $np->db->requestMovePort(-switch => $switch, -port => $port, + -vlan => 'unquarantine', -by => 'resetport.pl') || + push @{$failed->{$switch}}, $port; + } } else { - $np->db->requestMovePort(-switch => $switch, -port => $port, - -vlan => 'unquarantine', -by => 'resetport.pl') || - push @failed, $port; + _log ("DEBUG", $self->tid(). " $regMac leave quar $switch $port\n"); } } else { - _log ("DEBUG", "$switch $port multiMac said to quarantine the port.\n"); + # if we are not alone, then enforceMultiMacPolicy + # and do whatever it says to do (quar or unquar) + + _log ("DEBUG", $self->tid(). " $switch $port has more than one mac on it. enforceMultiMacPolicy\n"); + + my ($_rv, $_sw, $_po) = $np->enforceMultiMacPolicy($regMac, '', $regMacStatus, + $switch, $port, + undef, {$port => $macList}); + if ($_rv =~ /UNQUAR$/) { + _log ("DEBUG", $self->tid(). " $switch $port multiMac said to unquarantine the port.\n"); + if (exists $opts{'n'}) { + _log("DEBUG", "not really!\n"); + } else { + $np->db->requestMovePort(-switch => $switch, -port => $port, + -vlan => 'unquarantine', -by => 'resetport.pl') || + push @{$failed->{$switch}}, $port; + } + } else { + _log ("DEBUG", $self->tid()." $switch $port multiMac said to quarantine the port.\n"); + } + } } + } + elsif($unq_on_linkup =~ /^ITDEPENDS$/) { + # "ITDEPENDS" means that in order to unquarantine this port + # the following must be true: + # + # if MULTI_MAC is ALL_OK then + # all of the clients on this port must be tagged as uqlinkup="yes" + # AND they all must be registered and P/UNQUAR. UQLinkUp_itDepends() + # does this in a single query. + # else + # XXX we're not going to implement the other MULTI_MAC cases yet + # endif + my $numOK = $np->db->UQLinkUp_itDependsCheck($macList); + my $mmpol = $np->cfg->policy(-key => 'MULTI_MAC'); + + if ( ($numOK == ($#$macList+1)) && ($mmpol eq "ALL_OK") ) { + _log ("DEBUG", $self->tid()." $switch $port 'itdepends' set. everything looks good. unquar port. ", + "numOK=$numOK numMacs=".($#$macList+1)." mmpol=$mmpol\n"); + if (exists $opts{'n'}) { + _log("DEBUG", $self->tid(). " not really!\n"); + } else { + $np->db->requestMovePort(-switch => $switch, + -port => $port, + -vlan => 'unquarantine', + -by => 'resetport.pl') || + push @{$failed->{$switch}}, $port; + } + } else { + _log ("DEBUG", $self->tid()." $switch $port 'itdepends' set. somethings not right. quar port. ", + "numOK=$numOK numMacs=".($#$macList+1)." mmpol=$mmpol maclist=(", + join(',', @$macList), + ")\n"); + } } - } - elsif($unq_on_linkup =~ /^ITDEPENDS$/) { - # "ITDEPENDS" means that in order to unquarantine this port - # the following must be true: - # - # if MULTI_MAC is ALL_OK then - # all of the clients on this port must be tagged as uqlinkup="yes" - # AND they all must be registered and P/UNQUAR. UQLinkUp_itDepends() - # does this in a single query. - # else - # XXX we're not going to implement the other MULTI_MAC cases yet - # endif - - my $numOK = $np->db->UQLinkUp_itDependsCheck($macList); - my $mmpol = $np->cfg->policy(-key => 'MULTI_MAC'); + } + + if (exists $pq->{'q'}->{$switch}) { + foreach my $port (@{$pq->{'q'}->{$switch}}) { + my $unq_on_linkup = $np->cfg->policy(-key => 'UNQUAR_ON_LINKUP') || "0"; + my $rppt = $np->cfg->policy(-key => 'RESETPORT_PORT_POLL_TIME') || 0; + my $lftol = $np->cfg->policy(-key => 'LINKFLAP_TOLERANCE') || 0; + + # if possible, we'll resolve the switch/port to a specific network and the + # look to see if the above policy settings are over-ridden at the network or + # netgroup level. + + my $curNw = $np->cfg->getMatchingNetwork(-switch => $switch, -port => $port); + if ($curNw =~ /^\d/) { + _log("DEBUG", $self->tid(). " sw=$switch po=$port nw=$curNw\n"); + $unq_on_linkup = $np->cfg->policy(-key => 'UNQUAR_ON_LINKUP', -network => $curNw); + $rppt = $np->cfg->policy(-key => 'RESETPORT_PORT_POLL_TIME', -network => $curNw); + $lftol = $np->cfg->policy(-key => 'LINKFLAP_TOLERANCE', -network => $curNw) || 0; + } - if ( ($numOK == ($#$macList+1)) && ($mmpol eq "ALL_OK") ) { - _log ("DEBUG", "$switch $port 'itdepends' set. everything looks good. unquar port. ", - "numOK=$numOK numMacs=".($#$macList+1)." mmpol=$mmpol\n"); - if (exists $opts{'n'}) { - _log("DEBUG", "not really!\n"); + _log("DEBUG", $self->tid. " link down $switch $port and unq_lu=$unq_on_linkup rppt=$rppt\n"); + + # if we have a link flap tolerance time set, dont do anything until the time has + # expired. if link comes back up on the port before the time expires, the port + # will be removed from the 'q' queue by the linkup code above. if the timer + # expires, quarantine the port. + + if ($rppt) { + if ($pq->{'qt'}->{$switch}->[$port]) { + # if we are on the 'u' list then link is up and we'll be + # removed from the 'u' list by the linkup code above. + + # if the timer has expired, tho, we should quarantine this port. + # otherwise leave the port on the 'q' list + + if ( time() - $pq->{'qt'}->{$switch}->[$port] > $lftol ) { + + $np->db->requestMovePort(-switch => $switch, -port => $port, + -vlan => 'quarantine', + -by => 'resetport.pl') || + _log("ERROR", $np->db->error()); + _log ("DEBUG", $self->tid()." quarantined $switch $port because rppt expired\n") + if exists $opts{'D'}; + + # remove the port from the linkdown queue since we've processed it + + @{$pq->{'q'}->{$switch}} = grep /!$port$/, @{$pq->{'q'}->{$switch}}; + $pq->{'qt'}->{$switch}->[$port] = undef; + } else { + _log ("DEBUG", $self->tid(). + " $switch $port has linkdown, but is recent, we'll wait a while: ". + (time() - $pq->{'qt'}->{$switch}->[$port])." secs old (of $lftol max)\n"); + } + } else { + _log ("DEBUG", $self->tid()." $switch $port has no first-seen time, but should.\n"); + $pq->{'qt'}->{$switch}->[$port] = time(); + } } else { - $np->db->requestMovePort(-switch => $switch, - -port => $port, - -vlan => 'unquarantine', + # rppt is not set (or set to zero) so immediate quarantine the port + + $np->db->requestMovePort(-switch => $switch, -port => $port, + -vlan => 'quarantine', -by => 'resetport.pl') || - push @failed, $port; - } - } else { - _log ("DEBUG", "$switch $port 'itdepends' set. somethings not right. quar port. ", - "numOK=$numOK numMacs=".($#$macList+1)." mmpol=$mmpol maclist=(", - join(',', @$macList), - ")\n"); + _log("ERROR", $np->db->error()); + _log ("DEBUG", $self->tid()." immediately quarantined $switch $port because rppt=0\n") + if exists $opts{'D'}; + + # remove the port from the linkdown queue since we've processed it + + @{$pq->{'q'}->{$switch}} = grep /!$port$/, @{$pq->{'q'}->{$switch}}; + $pq->{'qt'}->{$switch}->[$port] = undef; + } } } - } - # shift off all the ports we have worked on - while (shift @{$unq->{$switch}}) {} - # push on the ones that have failed so we can take care of - # them next time around. - push @{$unq->{$switch}}, @failed; - -endlock: - } # end lock - threads->yield; - sleep(5); - } # end while + # save the ports that have failed so we can take care of + # them next time around. since this is our private queue, + # no need to share it. + + if (exists $failed->{$switch}) { + @{$pq->{'u'}->{$switch}} = @{$failed->{$switch}}; + } else { + $pq->{'u'}->{$switch} = []; + } + } - # we shouldn't be leaving the while loop but if we are - # make sure we join up with the parent thread + } # end foreach - threads->join; - return 1; + return $pq; } Index: portmover.pl =================================================================== RCS file: /cvsroot/netpass/NetPass/bin/portmover.pl,v retrieving revision 1.5 retrieving revision 1.6 diff -u -d -r1.5 -r1.6 --- portmover.pl 27 Apr 2005 03:54:06 -0000 1.5 +++ portmover.pl 3 Aug 2005 02:44:38 -0000 1.6 @@ -16,6 +16,7 @@ moveport.pl [-c cstr] [-U user/pass] [-n] [-q] [-D] -c cstr db connect string -U user/pass db user[/pass] + -t thread queue size -n "not really" -q be quiet. exit status only. -D enable debugging @@ -47,6 +48,18 @@ Enable debugging output. This flag causes this script to run in the foreground. Otherwise, this script will detach and run in the background. +=item B<-t thead-queue-size> + +A number denoting how many switches to delegate to each thread for processing. +The default is 20. If you have 100 switches in your NetPass configuration, +5 threads will be spawned. Each thread will deal with moving ports back +and forth on the switches that are assigned to it. + +Each thread requires a connection to the database, so don't set this number +too low or you'll needless use DB resources. If you make the number too +high, then a slow switch or a large number of ports to be moved could +slow things down for many people. + =back =head1 DESCRIPTION |