#!/usr/bin/perl -wT # # Copyright (c) 2000-2015 University of Utah and the Flux Group. # # {{{EMULAB-LICENSE # # This file is part of the Emulab network testbed software. # # This file is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at # your option) any later version. # # This file 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 Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this file. If not, see . # # }}} # # # This is a stub library to provide a few things that libtestbed on # boss provides. # package libtestbed; use Exporter; @ISA = "Exporter"; @EXPORT = qw( SENDMAIL TB_BOSSNODE TB_EVENTSERVER TBScriptLock TBScriptUnlock TBSCRIPTLOCK_OKAY TBSCRIPTLOCK_TIMEDOUT TBSCRIPTLOCK_IGNORE TBSCRIPTLOCK_FAILED TBSCRIPTLOCK_GLOBALWAIT TBSCRIPTLOCK_SHAREDLOCK TBSCRIPTLOCK_NONBLOCKING TBSCRIPTLOCK_WOULDBLOCK TBSCRIPTLOCK_INTERRUPTED TBSCRIPTLOCK_INTERRUPTIBLE TBTimeStamp TBTimeStampWithDate TBBackGround ReOpenLog ); # Must come after package declaration! use English; # For locking below use Fcntl ':flock'; use IO::Handle; use Time::HiRes qw(gettimeofday); use POSIX qw(:signal_h); # # Turn off line buffering on output # $| = 1; # Load up the paths. Done like this in case init code is needed. BEGIN { if (! -e "/etc/emulab/paths.pm") { die("Yikes! Could not require /etc/emulab/paths.pm!\n"); } require "/etc/emulab/paths.pm"; import emulabpaths; } # Need this. use libtmcc; sub SENDMAILWith($$$$;$$@); sub SENDMAIL($$$;$$@) { my($To, $Subject, $Message, $From, $Headers, @Files) = @_; SENDMAILWith("/usr/sbin/sendmail -i -t", $To, $Subject, $Message, $From, $Headers, @Files); } sub SENDMAILWith($$$$;$$@) { my($Command, $To, $Subject, $Message, $From, $Headers, @Files) = @_; my $tag = uc($MAILTAG); # # Untaint the path locally. Note that using a "local" fails on older perl! # my $SAVE_PATH = $ENV{'PATH'}; $ENV{'PATH'} = "/bin:/usr/bin"; delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; if (! open(MAIL, "| $Command")) { print STDERR "SENDMAIL: Could not start sendmail: $!\n"; goto bad; } # # Sendmail will figure this out if not given. # if (defined($From) && $From) { print MAIL "From: $From\n"; } if (defined($Headers) && length($Headers) > 0) { print MAIL "$Headers\n"; } print MAIL "X-NetBed: $SCRIPTNAME\n"; if (defined($To)) { print MAIL "To: $To\n"; } print MAIL "Subject: $tag: $Subject\n"; print MAIL "\n"; print MAIL "$Message\n"; print MAIL "\n"; if (@Files) { foreach my $file ( @Files ) { if (defined($file) && open(IN, "$file")) { print MAIL "\n--------- $file --------\n"; while () { print MAIL "$_"; } close(IN); } } } print MAIL "\n"; if (! close(MAIL)) { print STDERR "SENDMAIL: Could not finish sendmail: $!\n"; goto bad; } $ENV{'PATH'} = $SAVE_PATH; return 1; bad: $ENV{'PATH'} = $SAVE_PATH; return 0; } # # Put ourselves into the background, directing output to the log file. # The caller provides the logfile name, which should have been created # with mktemp, just to be safe. Returns the usual return of fork. # # usage int TBBackGround(char *filename). # sub TBBackGround($) { my ($logname) = @_; my $mypid = fork(); if ($mypid) { return $mypid; } select(undef, undef, undef, 0.2); # # We have to disconnect from the caller by redirecting both STDIN and # STDOUT away from the pipe. Otherwise the caller (the web server) will # continue to wait even though the parent has exited. # open(STDIN, "< /dev/null") or die("opening /dev/null for STDIN: $!"); ReOpenLog($logname); # # Create a new session to ensure we are clear of any process group # POSIX::setsid() or die("setsid failed: $!"); return 0; } # # As for newsyslog. Call this on signal. newsyslog will have renamed the # the original file already. # sub ReOpenLog($) { my ($logname) = @_; # Note different taint check (allow /). if ($logname =~ /^([-\@\w.\/]+)$/) { $logname = $1; } else { die "Bad data in logfile name: $logname"; } open(STDERR, ">> $logname") or die("opening $logname for STDERR: $!"); open(STDOUT, ">> $logname") or die("opening $logname for STDOUT: $!"); # # Turn off line buffering on output # STDOUT->autoflush(1); STDERR->autoflush(1); return 0; } # # Return a timestamp. We don't care about day/date/year. Just the time mam. # # TBTimeStamp() # sub TBTimeStamp() { my ($seconds, $microseconds) = gettimeofday(); return POSIX::strftime("%H:%M:%S", localtime($seconds)) . "." . sprintf("%06d", $microseconds); } sub TBTimeStampWithDate() { my ($seconds, $microseconds) = gettimeofday(); return POSIX::strftime("%m/%d/20%y %H:%M:%S", localtime($seconds)) . "." . sprintf("%06d", $microseconds); } # # Return name of the bossnode. # sub TB_BOSSNODE() { return tmccbossname(); } # # Return name of the event server. # sub TB_EVENTSERVER() { # duplicate behavior of tmcc bossinfo function my @searchdirs = ( "/etc/testbed","/etc/emulab","/etc/rc.d/testbed", "/usr/local/etc/testbed","/usr/local/etc/emulab" ); my $bossnode = TB_BOSSNODE(); my $eventserver = ''; foreach my $d (@searchdirs) { if (-e "$d/eventserver" && !(-z "$d/eventserver")) { $eventserver = `cat $d/eventserver`; last; } } if ($eventserver eq '') { my @ds = split(/\./,$bossnode,2); if (scalar(@ds) == 2) { # XXX event-server hardcode $eventserver = "event-server.$ds[1]"; } } if ($eventserver eq '') { $eventserver = "event-server"; } return $eventserver; } # # Serialize an operation (script). # my $lockname; my $lockhandle; # Return Values. sub TBSCRIPTLOCK_OKAY() { 0; } sub TBSCRIPTLOCK_TIMEDOUT() { 1; } sub TBSCRIPTLOCK_IGNORE() { 2; } sub TBSCRIPTLOCK_WOULDBLOCK() { 4; } sub TBSCRIPTLOCK_INTERRUPTED() { 8; } sub TBSCRIPTLOCK_FAILED() { -1; } sub TBSCRIPTLOCK_GLOBALWAIT() { 0x01; } sub TBSCRIPTLOCK_SHAREDLOCK() { 0x10; } sub TBSCRIPTLOCK_NONBLOCKING() { 0x20; } sub TBSCRIPTLOCK_INTERRUPTIBLE(){ 0x40; } # # There are two kinds of serialization. # # * Usual Kind: Each party just waits the lock. # * Other Kind: Only the first party really needs to run; the others just # need to wait. For example; exports_setup operates globally, # so there is no reason to run it more then once. We just # need to make sure that everyone waits for the one that is # running to finish. Use the global option for this. # sub TBScriptLock($;$$$) { my ($token, $flags, $waittime, $lockhandle_ref) = @_; local *LOCK; my $global = 0; my $shared = 0; my $interruptible = 0; if (!defined($waittime)) { $waittime = 30; } elsif ($waittime == 0) { $waittime = 99999999; } $global = 1 if (defined($flags) && ($flags & TBSCRIPTLOCK_GLOBALWAIT())); $shared = 1 if (defined($flags) && ($flags & TBSCRIPTLOCK_SHAREDLOCK())); $interruptible = 1 if (defined($flags) && ($flags & TBSCRIPTLOCK_INTERRUPTIBLE())); $lockname = "/var/tmp/testbed_${token}_lockfile"; my $oldmask = umask(0000); if (! open(LOCK, ">>$lockname")) { print STDERR "Could not open $lockname!\n"; umask($oldmask); return TBSCRIPTLOCK_FAILED(); } umask($oldmask); my $checkforinterrupt = sub { my $sigset = POSIX::SigSet->new; sigpending($sigset); # XXX Why isn't SIGRTMIN and SIGRTMAX defined in the POSIX module. for (my $i = 1; $i < 50; $i++) { if ($sigset->ismember($i)) { print "checkForInterrupt: Signal $i is pending\n"; return 1; } } return 0; }; if (! $global) { # # A plain old lock. # my $tries = 0; my $ltype = ($shared ? LOCK_SH : LOCK_EX); while (flock(LOCK, $ltype|LOCK_NB) == 0) { return TBSCRIPTLOCK_WOULDBLOCK() if (defined($flags) && ($flags & TBSCRIPTLOCK_NONBLOCKING())); print "Another $token is in progress (${tries}s). Waiting ...\n" if (($tries++ % 60) == 0); $waittime--; if ($waittime == 0) { print STDERR "Could not get the lock after a long time!\n"; return TBSCRIPTLOCK_TIMEDOUT(); } sleep(1); if ($interruptible && &$checkforinterrupt()) { print STDERR "ScriptLock interrupted by signal!\n"; return TBSCRIPTLOCK_INTERRUPTED(); } } # Okay, got the lock. Save the handle. We need it below. if (defined($lockhandle_ref)) { $$lockhandle_ref = *LOCK; } else { $lockhandle = *LOCK; } return TBSCRIPTLOCK_OKAY(); } # # Okay, a global lock. # # If we don't get it the first time, we wait for: # 1) The lock to become free, in which case we do our thing # 2) The time on the lock to change, in which case we wait for that # process to finish, and then we are done since there is no # reason to duplicate what the just finished process did. # if (flock(LOCK, LOCK_EX|LOCK_NB) == 0) { my $oldlocktime = (stat(LOCK))[9]; my $gotlock = 0; while (1) { print "Another $token in progress. Waiting a moment ...\n"; if (flock(LOCK, LOCK_EX|LOCK_NB) != 0) { # OK, got the lock $gotlock = 1; last; } my $locktime = (stat(LOCK))[9]; if ($locktime != $oldlocktime) { $oldlocktime = $locktime; last; } $waittime--; if ($waittime <= 0) { print STDERR "Could not get the lock after a long time!\n"; return TBSCRIPTLOCK_TIMEDOUT(); } sleep(1); if ($interruptible && &$checkforinterrupt()) { print STDERR "ScriptLock interrupted by signal!\n"; return TBSCRIPTLOCK_INTERRUPTED(); } } my $count = 0; # # If we did not get the lock, wait for the process that did to finish. # if (!$gotlock) { while (1) { if ((stat(LOCK))[9] != $oldlocktime) { return TBSCRIPTLOCK_IGNORE(); } if (flock(LOCK, LOCK_EX|LOCK_NB) != 0) { close(LOCK); return TBSCRIPTLOCK_IGNORE(); } $waittime--; if ($waittime <= 0) { print STDERR "Process with the lock did not finish after ". "a long time!\n"; return TBSCRIPTLOCK_TIMEDOUT(); } sleep(1); if ($interruptible && &$checkforinterrupt()) { print STDERR "ScriptLock interrupted by signal!\n"; return TBSCRIPTLOCK_INTERRUPTED(); } } } } # # Perl-style touch(1) # my $now = time; utime $now, $now, $lockname; if (defined($lockhandle_ref)) { $$lockhandle_ref = *LOCK; } else { $lockhandle = *LOCK; } return TBSCRIPTLOCK_OKAY(); } # # Unlock; Just need to close the file (releasing the lock). # sub TBScriptUnlock(;$) { my ($lockhandle_arg) = @_; if (defined($lockhandle_arg)) { flock($lockhandle_arg, LOCK_UN); close($lockhandle_arg); } elsif (defined($lockhandle)) { flock($lockhandle, LOCK_UN); close($lockhandle); undef($lockhandle); } } 1;