diff --git a/configure b/configure
index d4a4436e807ae0aa1704a0f1929fcd2a90a63640..2f23a414ecfe37945277a38fbf07d5cd236c4b74 100755
--- a/configure
+++ b/configure
@@ -1542,6 +1542,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
 	tbsetup/plab/plabdiscover tbsetup/plab/etc/netbed_files/GNUmakefile \
 	tbsetup/ipassign/GNUmakefile tbsetup/ipassign/src/GNUmakefile \
 	tbsetup/ipassign/ipassign_wrapper tbsetup/assign_prepass \
+	tbsetup/panic tbsetup/webpanic \
 	tip/GNUmakefile \
 	tmcd/GNUmakefile tmcd/tmcd.restart \
 	tmcd/common/GNUmakefile tmcd/common/config/GNUmakefile \
diff --git a/configure.in b/configure.in
index 69a3a087ceb25dd3212dfc59a649b883f996f4be..899e6f5e2e768ef37a12a66c42ae809fe62a2306 100755
--- a/configure.in
+++ b/configure.in
@@ -572,6 +572,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
 	tbsetup/plab/plabdiscover tbsetup/plab/etc/netbed_files/GNUmakefile \
 	tbsetup/ipassign/GNUmakefile tbsetup/ipassign/src/GNUmakefile \
 	tbsetup/ipassign/ipassign_wrapper tbsetup/assign_prepass \
+	tbsetup/panic tbsetup/webpanic \
 	tip/GNUmakefile \
 	tmcd/GNUmakefile tmcd/tmcd.restart \
 	tmcd/common/GNUmakefile tmcd/common/config/GNUmakefile \
diff --git a/db/libdb.pm.in b/db/libdb.pm.in
index 104dfd32b9135180d647ef8c14c227111594f44c..a5bd8e1601c46f7fa467baf5de3cb55b22646ded 100644
--- a/db/libdb.pm.in
+++ b/db/libdb.pm.in
@@ -67,7 +67,7 @@ use vars qw(@ISA @EXPORT);
 	 DBLIMIT_NSFILESIZE NODERELOADPENDING_EID
 
 	 EXPTSTATE_NEW EXPTSTATE_PRERUN EXPTSTATE_SWAPPED EXPTSTATE_SWAPPING
-	 EXPTSTATE_ACTIVATING EXPTSTATE_ACTIVE
+	 EXPTSTATE_ACTIVATING EXPTSTATE_ACTIVE EXPTSTATE_PANICED
 	 EXPTSTATE_TERMINATING EXPTSTATE_TERMINATED EXPTSTATE_QUEUED
 	 EXPTSTATE_MODIFY_PARSE EXPTSTATE_MODIFY_REPARSE EXPTSTATE_MODIFY_RESWAP
 	 EXPTSTATE_RESTARTING
@@ -200,6 +200,8 @@ use vars qw(@ISA @EXPORT);
 	 TBExptMinMaxNodes TBExptSecurityLevel TBExptIDX
 	 TBDB_SECLEVEL_GREEN  TBDB_SECLEVEL_YELLOW
 	 TBDB_SECLEVEL_ORANGE TBDB_SECLEVEL_RED
+
+	 TBExptSetPanicBit TBExptGetPanicBit TBExptClearPanicBit
 	 );
 
 # Must come after package declaration!
@@ -336,6 +338,7 @@ sub EXPTSTATE_QUEUED()		{ "queued"; }
 sub EXPTSTATE_SWAPPING()	{ "swapping"; }
 sub EXPTSTATE_ACTIVATING()	{ "activating"; }
 sub EXPTSTATE_ACTIVE()		{ "active"; }
+sub EXPTSTATE_PANICED()		{ "paniced"; }
 sub EXPTSTATE_TERMINATING()	{ "terminating"; }
 sub EXPTSTATE_TERMINATED()	{ "ended"; }
 sub EXPTSTATE_MODIFY_PARSE()	{ "modify_parse"; }
@@ -3776,6 +3779,46 @@ sub TBNodeFirewall ($$$) {
     return 1;
 }
 
+#
+# Set the paniced bit for an experiment.
+#
+sub TBExptSetPanicBit($$) {
+    my ($pid, $eid) = @_;
+
+    return DBQueryWarn("update experiments set ".
+		       "    paniced=1,panic_date=now() ".
+		       "where pid='$pid' and eid='$eid'");
+}
+
+#
+# Clear the panic bit.
+# 
+sub TBExptClearPanicBit($$) {
+    my ($pid, $eid) = @_;
+
+    return DBQueryWarn("update experiments set ".
+		       "    paniced=0,panic_date=NULL ".
+		       "where pid='$pid' and eid='$eid'");
+}
+
+#
+# Get the value of the paniced bit. 
+# 
+sub TBExptGetPanicBit($$$) {
+    my ($pid, $eid, $panicp) = @_;
+
+    my $query_result =
+	DBQueryWarn("select paniced,panic_date from experiments ".
+		    "where pid='$pid' and eid='$eid'");
+    if (!$query_result || $query_result->num_rows == 0) {
+	return 0;
+    }
+    my @row = $query_result->fetchrow_array();
+    $$panicp = $row[0];
+
+    return 1;
+}
+
 #
 # Issue a DB query. Argument is a string. Returns the actual query object, so
 # it is up to the caller to test it. I would not for one moment view this
diff --git a/tbsetup/GNUmakefile.in b/tbsetup/GNUmakefile.in
index 0b7c37905122820042075ec70eee926c9f175ceb..4085da3be34e66a582792688d15d75fcba77f403 100644
--- a/tbsetup/GNUmakefile.in
+++ b/tbsetup/GNUmakefile.in
@@ -28,14 +28,14 @@ SBIN_STUFF	= resetvlans console_setup.proxy sched_reload named_setup \
 		  exports_setup.proxy vnode_setup eventsys_start \
 		  sfskey_update sfskey_update.proxy rmuser idleswap \
 		  newnode_reboot savelogs.proxy eventsys.proxy \
-		  elabinelab snmpit.proxy 
+		  elabinelab snmpit.proxy panic
 
 CTRLBIN_STUFF   = console_setup.proxy exports_setup.proxy sfskey_update.proxy \
 		  savelogs.proxy eventsys.proxy
 
 LIBEXEC_STUFF	= rmproj wanlinksolve wanlinkinfo \
 		  os_setup mkexpdir console_setup webnscheck webreport \
-		  webendexp webbatchexp \
+		  webendexp webbatchexp webpanic \
 		  assign_wrapper assign_prepass ptopgen webnodeupdate \
 		  webdelay_config \
 		  webrmgroup webswapexp webnodecontrol \
diff --git a/tbsetup/panic.in b/tbsetup/panic.in
new file mode 100755
index 0000000000000000000000000000000000000000..7fbd72a27768bc0a1dd1fac4eb8c7fd77d6dd794
--- /dev/null
+++ b/tbsetup/panic.in
@@ -0,0 +1,283 @@
+#!/usr/bin/perl -wT
+
+#
+# EMULAB-COPYRIGHT
+# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# All rights reserved.
+#
+
+use English;
+use Getopt::Std;
+use POSIX qw(isatty setsid);
+
+#
+# Press the panic button. Also invoked from web interface.
+#
+sub usage()
+{
+    print(STDERR
+	  "Usage: panicbutton [-r] <pid> <eid>\n".
+	  "switches and arguments:\n".
+	  "-r       - Reset panic state (admin people only)\n".
+	  "<pid>    - The project the experiment belongs to\n".
+	  "<eid>    - The experiment name (id)\n");
+    exit(-1);
+}
+my $optlist = "r";
+my $reset   = 0;
+
+sub Fatal($);
+
+#
+# Exit codes are important; they tell the web page what has happened so
+# it can say something useful to the user. Fatal errors are mostly done
+# with die(), but expected errors use this routine. At some point we will
+# use the DB to communicate the actual error.
+#
+# $status < 0 - Fatal error. Something went wrong we did not expect.
+# $status = 0 - Termination is proceeding in the background. Notified later.
+# $status > 0 - Expected error. User not allowed for some reason. 
+# 
+sub ExitWithStatus($$)
+{
+    my ($status, $message) = @_;
+    
+    if ($status < 0) {
+	die("*** $0:\n".
+	    "    $message\n");
+    }
+    else {
+	print STDERR "$message\n";
+    }
+    exit($status);
+}
+
+#
+# Configure variables
+#
+my $TB     = "@prefix@";
+my $TBOPS  = "@TBOPSEMAIL@";
+
+#
+# Testbed Support libraries
+#
+use lib "@prefix@/lib";
+use libdb;
+use libtestbed;
+
+# Be careful not to exit on transient error; 0 means infinite retry.
+$libdb::DBQUERY_MAXTRIES = 0;
+
+my $snmpit    = "$TB/bin/snmpit";
+my $dbuid;
+my $user_name;
+my $user_email;
+    
+#
+# Untaint the path
+# 
+$ENV{'PATH'} = '/bin:/usr/bin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+
+#
+# Turn off line buffering on output
+#
+$| = 1;
+
+#
+# Parse command arguments. Once we return from getopts, all that should
+# left are the required arguments.
+#
+%options = ();
+if (! getopts($optlist, \%options)) {
+    usage();
+}
+if (@ARGV != 2) {
+    usage();
+}
+my $pid   = $ARGV[0];
+my $eid   = $ARGV[1];
+if (defined($options{"r"})) {
+    $reset = 1;
+}
+
+#
+# Untaint the arguments.
+#
+if ($pid =~ /^([-\w\.]+)$/) {
+    $pid = $1;
+}
+else {
+    die("Tainted argument $pid!\n");
+}
+if ($eid =~ /^([-\w\.]+)$/) {
+    $eid = $1;
+}
+else {
+    die("Tainted argument $eid!\n");
+}
+
+#
+# See if the experiment is firewalled. Error if not.
+#
+my $firewall;
+my $firewalled = TBExptFirewall($pid, $eid, \$firewall);
+
+if (!$firewalled) {
+    die("*** $0:\n".
+	"    Experiment $pid/$eid is not firewalled!\n");
+}
+
+#
+# Verify user and get his DB uid.
+#
+if (! UNIX2DBUID($UID, \$dbuid)) {
+    die("*** $0:\n".
+	"    You do not exist in the Emulab Database.\n");
+}
+
+#
+# Get email info for user.
+#
+if (! UserDBInfo($dbuid, \$user_name, \$user_email)) {
+    die("*** $0:\n".
+	"    Cannot determine your name and email address.\n");
+}
+
+#
+# Verify that this person is allowed to press the panic button.
+# Note that any script down the line has to do an admin check also. 
+#
+if ($UID && !TBAdmin($UID) &&
+    !TBExptAccessCheck($dbuid, $pid, $eid, TB_EXPT_MODIFY)) {
+    die("*** $0:\n".
+	"    You do not have permission to end this experiment!\n");
+}
+
+#
+# We have to protect against trying to end an experiment that is currently
+# in the process of being terminated. We use a "wrapper" state (actually
+# a timestamp so we can say when termination was requested) since
+# terminating consists of a couple of different experiment states down inside
+# the tb scripts. 
+#
+DBQueryFatal("lock tables experiments write");
+
+$query_result =
+    DBQueryFatal("SELECT * FROM experiments WHERE eid='$eid' and pid='$pid'");
+
+if (! $query_result->numrows) {
+    die("*** $0:\n".
+	"    No such experiment $pid/$eid exists!\n");
+}
+my %hashrow = $query_result->fetchhash();
+my $expt_head_login = $hashrow{'expt_head_uid'};
+my $estate          = $hashrow{'state'};
+
+#
+# Called from user (via web interface).
+#
+if ($reset) {
+    ExitWithStatus(1, "Experiment $pid/$eid is not paniced!\n")
+	if ($estate ne EXPTSTATE_PANICED);
+}
+else {
+    ExitWithStatus(1, "Experiment $pid/$eid is not active!\n")
+	if (! ($estate eq EXPTSTATE_ACTIVE ||
+	       $estate eq EXPTSTATE_ACTIVATING ||
+	       $estate eq EXPTSTATE_SWAPPING));
+}
+
+#
+# Change experiment state and lock it.
+# 
+TBLockExp($pid, $eid, ($reset ? EXPTSTATE_ACTIVE : EXPTSTATE_PANICED));
+DBQueryFatal("unlock tables");
+
+#
+# XXX - At this point a failure is going to leave things in an
+# inconsistent state. Be sure to call fatal() only since we are
+# going into the background, and we have to send email since no
+# one is going to see printed error messages (output goes into the
+# log file, which will be sent along in the email). 
+#
+
+#
+# Get email address of the experiment head, which may be different than
+# the person who is actually terminating the experiment, since its polite
+# to let the original creator know whats going on. 
+#
+my $expt_head_name;
+my $expt_head_email;
+
+if (! UserDBInfo($expt_head_login, \$expt_head_name, \$expt_head_email)) {
+    print "*** WARNING: ".
+	  "Could not determine name/email for $expt_head_login.\n";
+    $expt_head_name  = "TBOPS";
+    $expt_head_email = $TBOPS;
+}
+
+$query_result = 
+    DBQueryFatal("select card1 from wires ".
+		 "where node_id1='$firewall' AND type='Control'");
+
+if (!$query_result->numrows) {
+    fatal("Could not determine firewall port for $pid/$eid!");
+}
+my ($port) = $query_result->fetchrow_array();
+
+#
+# Call snmpit.
+#
+if ($reset) {
+    system("$snmpit -e ${firewall}:${port}");
+    if ($?) {
+	fatal("snmpit exited with $?!");
+    }
+    TBExptClearPanicBit($pid, $eid);
+    print "Panic situation has been cleared!\n";
+}
+else {
+    system("$snmpit -d ${firewall}:${port}");
+    if ($?) {
+	fatal("snmpit exited with $?!");
+    }
+    TBExptSetPanicBit($pid, $eid);
+    print "Panic Button has been pressed!\n";
+}
+TBUnLockExp($pid, $eid);
+
+#
+# Send email notification to user *and* to tbops.
+#
+SENDMAIL("$user_name <$user_email>",
+	 "Panic Button ". ($reset ? "Cleared" : "Pressed") .
+	    " for Experiment $pid/$eid",
+	 "$dbuid has " . ($reset ? "cleared" : "pressed") .
+	   " the panic button for experiment $pid/$eid",
+	 "$user_name <$user_email>",
+	 "Cc:  $expt_head_name <$expt_head_email>\n".
+	 "Bcc: $TBOPS");
+
+exit 0;
+
+sub fatal($)
+{
+    my($mesg) = $_[0];
+    
+    #
+    # Send a message to the testbed list. Append the logfile.
+    #
+    SENDMAIL("$user_name <$user_email>",
+	     "Panic Button Failure for Experiment $pid/$eid",
+	     "$dbuid ". ($reset ? "cleared" : "pressed") .
+	       " the panic button for experiment $pid/$eid,\n".
+	     "BUT there was a failure!\n\n".
+	     "$mesg\n",
+	     "$user_name <$user_email>",
+	     "Cc:  $expt_head_name <$expt_head_email>\n".
+	     "Bcc: $TBOPS");
+
+    die("*** $0:\n".
+	"    $mesg\n");
+}
diff --git a/tbsetup/swapexp.in b/tbsetup/swapexp.in
index e97c4031557d7d430541fde6cc2189a3f5820a1a..b5dcd1fa853114134b5b9ed1c295fd0b0c425a3e 100644
--- a/tbsetup/swapexp.in
+++ b/tbsetup/swapexp.in
@@ -107,6 +107,7 @@ my @row;
 my $action;
 my $nextswapstate;
 my $termswapstate;
+my $isadmin  = 0;
 
 #
 # Untaint the path
@@ -270,12 +271,13 @@ if (! UserDBInfo($dbuid, \$user_name, \$user_email)) {
     die("*** $0:\n".
 	"    Cannot determine your name and email address.\n");
 }
+$isadmin = TBAdmin($UID);
 
 #
 # Verify that this person can muck with the experiment.
 # Note that any script down the line has to do an admin check also. 
 #
-if ($UID && !TBAdmin($UID) &&
+if ($UID && !$isadmin &&
     !TBExptAccessCheck($dbuid, $pid, $eid, TB_EXPT_DESTROY)) {
     die("*** $0:\n".
 	"    You do not have permission to swap or modify this experiment!\n");
@@ -470,6 +472,17 @@ else {
 		       "or modify the experiment.\n")
 	    if ($canceled);
 
+  	#
+ 	# Cannot swapmod an active elabinelab experiment, yet.
+ 	# 
+ 	ExitWithStatus(1,
+ 		       "Experiment $pid/$eid is an active ElabInElab.\n".
+ 		       "You cannot modify this type of experiment while it\n".
+ 		       "is swapped in. We hope to support this soon.\n")
+ 	    if ($inout eq "modify" &&
+ 		($elabinelab || defined($elabinelab_eid)) &&
+ 		$estate ne EXPTSTATE_SWAPPED());
+
 	#
 	# Check the state for the various operations.
 	#
@@ -484,12 +497,25 @@ else {
 	      };
 	      /^out$/i && do {
 		  if ($estate ne EXPTSTATE_ACTIVE() &&
+ 		      $estate ne EXPTSTATE_PANICED() &&
 		      $estate ne EXPTSTATE_ACTIVATING()) {
 		      ExitWithStatus(1,
 				     "Experiment $pid/$eid is not swapped in ".
 				     "or activating!\n");
 		  }
 		  
+ 		  #
+ 		  # Must be an admin person to swap out an experiment that
+ 		  # has had its panic button pressed.
+ 		  #
+ 		  if ($estate eq EXPTSTATE_PANICED() && !$isadmin) {
+ 		      ExitWithStatus(1,
+ 				     "Experiment $pid/$eid had its panic ".
+ 				     "button pressed!\n".
+ 				     "Only a testbed administrator can swap ".
+ 				     "this experiment out.");
+ 		  }
+
 		  if ($estate eq EXPTSTATE_ACTIVATING()) {
 		      #
 		      # All we can do is set the cancel flag and hope that
@@ -743,6 +769,7 @@ if ($inout eq "out") {
     }
     SetExpState($pid, $eid, EXPTSTATE_SWAPPED)
 	or fatal("Failed to set experiment state to " . EXPTSTATE_SWAPPED());
+    TBExptClearPanicBit($pid, $eid);
 }
 elsif ($inout eq "in") {
     my $optarg = "";
diff --git a/tbsetup/tbswap.in b/tbsetup/tbswap.in
index 802933d0588c46de6f13731a3d5e8bd57fdbd01a..6ba9cb5f46d62fb4c9a065d0f9aa0d1a16b2506c 100644
--- a/tbsetup/tbswap.in
+++ b/tbsetup/tbswap.in
@@ -416,15 +416,98 @@ sub doSwapout($) {
 	}
 
 	if ($firewalled) {
-	    # XXX put all nodes into admin mode
-	    print STDERR "Confining firewalled nodes.\n";
-	    TBDebugTimeStamp("moving nodes to purgatory");
+	    #
+	    # If the panic button was pressed, put all nodes into admin
+	    # mode and power them off. After the firewall is torn down,
+	    # we can power them back up.
+	    #
+	    my @nodes     = ExpNodes($pid, $eid, 1);
+	    my $ADMINOSID = TB_OSID_FREEBSD_MFS;
+	    my $paniced;
+
+	    TBExptGetPanicBit($pid, $eid, \$paniced);
+
+	    if ($paniced) {
+		print STDERR "Powering down nodes.\n";
+		TBDebugTimeStamp("Powering down nodes");
+
+		system("power off @nodes");
+		if ($?) {
+		    #
+		    # If an error, cannot continue. Must leave firewall in
+		    # place. 
+		    #
+		    print STDERR "*** Failed to power nodes off! Stopping.\n";
+		    return 1;
+		}
+
+		#
+		# XXX: node_admin should take a list of nodes, or pid,eid.
+		#      Instead, call os_select directly for now. 
+		#
+		# Clear any one-shot boots and partition boots.
+		#
+		print STDERR "Changing OSID to admin MFS.\n";
+		TBDebugTimeStamp("Changing OSID to admin MFS");
+
+		# Order matters cause os_select sillyness.
+		if (system("os_select -c -1 @nodes") ||
+		    system("os_select -t $ADMINOSID @nodes") ||
+		    system("os_select -c @nodes")) {
+
+		    #
+		    # If an error, cannot continue. Must leave firewall in
+		    # place. 
+		    #
+		    print STDERR "*** Failed to reset OSIDs! Stopping.\n";
+		    return 1;
+		}
+
+		#
+		# Now we can power up the nodes again.
+		#
+		print STDERR "Powering up nodes.\n";
+		TBDebugTimeStamp("Powering up nodes");
+
+		system("power on @nodes");
+		if ($?) {
+		    #
+		    # If an error, cannot continue. Must leave firewall in
+		    # place. Eventually, we can continue past this, if we
+		    # know what nodes failed. But for now we have to do the
+		    # ISUP test to make sure nodes really got into the MFS.
+		    #
+		    print STDERR "*** Failed to power on nodes! Stopping.\n";
+		    return 1;
+		}
+	    }
 
 	    #
 	    # Once all nodes are safely in the admin MFS, we can take
 	    # down the firewall
 	    #
 	    doFW($pid, $eid, FWTEARDOWN);
+
+	    if ($paniced) {
+		#
+		# Now wait for ISUP. I do this here cause the reload daemon
+		# will power cycle the nodes again if its reboot fails, and
+		# that will happen if the nodes are not back into the MFS in
+		# time. THe right thing to do is to store the power state in
+		# the DB, and have the reload daemon turn the nodes back on.
+		#
+		print STDERR "Waiting for nodes to boot the MFS.\n";
+		TBDebugTimeStamp("Waiting for nodes to boot the MFS");
+		
+		system("node_statewait -s " . TBDB_NODESTATE_ISUP . " @nodes");
+		if ($?) {
+		    #
+		    # Okay to continue; reload daemon will probably send
+		    # email later when the reload fails.
+		    #
+		    print STDERR "*** Some nodes failed to reboot. Continuing\n";
+		}
+	    }	    
 	}
 
 	#
@@ -1050,6 +1133,7 @@ sub doFW($$$) {
     #
     my $fwsetupstr1 = "snmpit $cnetstack -m $fwvlanname $portlist";
     my $fwsetupstr2 = "snmpit $cnetstack -T $fwport $cnetvlanname $fwvlanname";
+    my $fwtakedownstr0 = "snmpit $cnetstack -e $fwport";
     my $fwtakedownstr1 = "snmpit $cnetstack -m $cnetvlanname $portlist";
     my $fwtakedownstr2 = "snmpit $cnetstack -o $fwvlanname";
     my $fwtakedownstr3 = "snmpit $cnetstack -U $fwport";
@@ -1102,8 +1186,14 @@ sub doFW($$$) {
 	# Record VLAN info now that everything is done
 	TBSetExptFirewallVlan($pid, $eid, $fwvid, $fwvlan);
     } else {
-	TBDebugTimeStamp("snmpit firewall teardown: VLAN");
+	TBDebugTimeStamp("snmpit re-enable fw control port: $fwport");
 	my $failed = 0;
+	if (system($fwtakedownstr0)) {
+	    print STDERR
+		"*** Could not re-enable firewall control port $fwport!\n";
+	    $failed = 1;
+	}
+	TBDebugTimeStamp("snmpit firewall teardown: VLAN");
 	if (system($fwtakedownstr1)) {
 	    print STDERR
 		"*** Could not return $portlist to Control VLAN!\n";
diff --git a/tbsetup/webpanic.in b/tbsetup/webpanic.in
new file mode 100644
index 0000000000000000000000000000000000000000..8797d5bf12123ddcab825b2145cd01398f52c95b
--- /dev/null
+++ b/tbsetup/webpanic.in
@@ -0,0 +1,24 @@
+#!/usr/bin/perl -w
+
+#
+# EMULAB-COPYRIGHT
+# Copyright (c) 2004 University of Utah and the Flux Group.
+# All rights reserved.
+#
+use English;
+
+#
+# This gets invoked from the Web interface. Simply a wrapper ...
+#
+
+#
+# Configure variables
+#
+my $TB       = "@prefix@";
+
+#
+# Run the real thing, and never return.
+# 
+exec "$TB/sbin/panic", @ARGV;
+
+die("webpanic: Could not exec panic: $!");
diff --git a/utils/node_statewait.in b/utils/node_statewait.in
index 1c5b84a620ceeeede8487357b6c5ea7a62468c8d..d6c0c70cf58e85cffcaf032b2c82671344e816db 100644
--- a/utils/node_statewait.in
+++ b/utils/node_statewait.in
@@ -14,11 +14,13 @@ use Getopt::Std;
 #
 sub usage()
 {
-    print STDOUT "Usage: node_statewait [-t timeout] [-a] | [node ...]\n";
+    print STDOUT
+	"Usage: node_statewait [-s state] [-t timeout] [-a] | [node ...]\n";
     exit(-1);
 }
-my $optlist = "at:";
+my $optlist = "at:s:";
 my $timeout = 60 * 6;
+my $state   = TBDB_NODESTATE_PXEWAIT;
 
 #
 # Configure variables
@@ -50,6 +52,9 @@ if (! getopts($optlist, \%options)) {
 if (defined($options{"t"})) {
     $timeout = $options{"t"};
 }
+if (defined($options{"s"})) {
+    $state = $options{"s"};
+}
 
 #
 # All testnodes, or just some nodes.
@@ -90,7 +95,7 @@ foreach my $node (sort(@nodes)) {
     #
     # Skip if something failed earlier.
     #
-    if (!TBNodeStateWait($node, TBDB_NODESTATE_PXEWAIT, $waitstart, $timeout)) {
+    if (!TBNodeStateWait($node, $state, $waitstart, $timeout)) {
 	print STDOUT "nodewait ($node): Success\n";
 	next;
     }
diff --git a/www/dbdefs.php3.in b/www/dbdefs.php3.in
index aa13afa1bb9226e3e9a35e7cadc55d8c22daaa6c..e095d14905c20e4620ff8f87d613b30ce0dc8eea 100644
--- a/www/dbdefs.php3.in
+++ b/www/dbdefs.php3.in
@@ -130,6 +130,7 @@ $TB_EXPTSTATE_SWAPPING		= "swapping";
 $TB_EXPTSTATE_SWAPPED		= "swapped";
 $TB_EXPTSTATE_ACTIVATING	= "activating";
 $TB_EXPTSTATE_ACTIVE		= "active";
+$TB_EXPTSTATE_PANICED		= "paniced";
 $TB_EXPTSTATE_QUEUED		= "queued";
 
 # Interfaces roles.
@@ -1851,6 +1852,25 @@ function TBPlabAvail() {
     return $types;
 }
 
+#
+# Is an experiment firewalled.
+# 
+function TBExptFirewall($pid, $eid) {
+    #
+    # Short form: is there a firewall?
+    # Only check the firewalls table so that we can be called for a swapped
+    # experiment (swapped experiments don't have reserved table info).
+    #
+    $query_result =
+	DBQueryWarn("SELECT eid FROM firewalls ".
+		    "WHERE pid='$pid' and eid='$eid' ".
+		    "AND type LIKE '%-vlan'");
+    if (!$query_result || !mysql_num_rows($query_result))
+	return 0;
+   
+    return 1;
+}
+
 #
 # DB Interface.
 #
diff --git a/www/panicbutton.gif b/www/panicbutton.gif
new file mode 100644
index 0000000000000000000000000000000000000000..ed8fa6488d2934ff40d15a176c21f0e4b8be8ab9
Binary files /dev/null and b/www/panicbutton.gif differ
diff --git a/www/panicbutton.php3 b/www/panicbutton.php3
new file mode 100644
index 0000000000000000000000000000000000000000..62da119287714823d2bc809dd5cb2d95823ac0d3
--- /dev/null
+++ b/www/panicbutton.php3
@@ -0,0 +1,142 @@
+<?php
+#
+# EMULAB-COPYRIGHT
+# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# All rights reserved.
+#
+include("defs.php3");
+include("showstuff.php3");
+
+#
+# Only known and logged in users.
+#
+$uid = GETLOGIN();
+LOGGEDINORDIE($uid);
+
+#
+# Must provide the EID!
+# 
+if (!isset($pid) ||
+    strcmp($pid, "") == 0) {
+  USERERROR("The project ID was not provided!", 1);
+}
+
+if (!isset($eid) ||
+    strcmp($eid, "") == 0) {
+  USERERROR("The experiment ID was not provided!", 1);
+}
+
+$exp_eid = $eid;
+$exp_pid = $pid;
+
+# Canceled operation redirects back to showexp page. See below.
+if ($canceled) {
+    header("Location: showexp.php3?pid=$pid&eid=$eid");
+    return;
+}
+
+#
+# Standard Testbed Header, after checking for cancel above.
+#
+PAGEHEADER("Press the Panic Button!");
+
+#
+# Check to make sure thats this is a valid PID/EID, while getting the
+# experiment GID.
+#
+if (! TBExptGroup($exp_pid, $exp_eid, $exp_gid)) {
+    USERERROR("The experiment $exp_eid is not a valid experiment ".
+	      "in project $exp_pid.", 1);
+}
+
+#
+# Verify permissions.
+#
+if (! TBExptAccessCheck($uid, $exp_pid, $exp_eid, $TB_EXPT_MODIFY)) {
+    USERERROR("You do not have permission to press the panic button for ".
+	      "experiment $exp_eid!", 1);
+}
+
+echo "<font size=+2>Experiment <b>".
+     "<a href='showproject.php3?pid=$exp_pid'>$exp_pid</a>/".
+     "<a href='showexp.php3?pid=$exp_pid&eid=$exp_eid'>$exp_eid</a>".
+     "</b></font>\n";
+    
+#
+# We run this twice. The first time we are checking for a confirmation
+# by putting up a form. The next time through the confirmation will be
+# set. Or, the user can hit the cancel button, in which case redirect the
+# browser back up a level.
+#
+if (!$confirmed) {
+    echo "<center><h2><br>
+          Are you <b>REALLY</b>
+          sure you want to press the panic button for Experiment '$exp_eid?'
+          </h2>\n";
+
+    SHOWEXP($exp_pid, $exp_eid, 1);
+    
+    echo "<form action='panicbutton.php3?pid=$exp_pid&eid=$exp_eid'".
+	   "method=post>";
+    echo "<input type=hidden name=exp_pideid value=\"$exp_pideid\">\n";
+    echo "<b><input type=submit name=confirmed value=Confirm></b>\n";
+    echo "<b><input type=submit name=canceled value=Cancel></b>\n";
+    echo "</form>\n";
+    echo "</center>\n";
+
+    PAGEFOOTER();
+    return;
+}
+
+#
+# We need the unix gid for the project for running the scripts below.
+# Note usage of default group in project.
+#
+TBGroupUnixInfo($exp_pid, $exp_gid, $unix_gid, $unix_name);
+
+#
+# We run a wrapper script that does all the work.
+#
+echo "<center><br>";
+echo "<h2>Pressing the panic button. Please wait a moment ...
+      </h2></center>";
+
+flush();
+
+#
+# Run the backend script.
+#
+$retval = SUEXEC($uid, "$exp_pid,$unix_gid", "webpanic $exp_pid $exp_eid",
+		 SUEXEC_ACTION_IGNORE);
+
+#
+# Fatal Error. Report to the user, even though there is not much he can
+# do with the error. Also reports to tbops.
+# 
+if ($retval < 0) {
+    SUEXECERROR(SUEXEC_ACTION_DIE);
+    #
+    # Never returns ...
+    #
+    die("");
+}
+
+#
+# Exit status >0 means the operation could not proceed.
+# Exit status =0 means the experiment is terminating in the background.
+#
+echo "<br>\n";
+if ($retval) {
+    echo "<h3>Panic Button failure</h3>";
+    echo "<blockquote><pre>$suexec_output<pre></blockquote>";
+}
+else {
+    echo "<h3>The panic button has been pressed!</h3><br>
+              You will need to contact testbed operations to continue.\n";
+}
+
+#
+# Standard Testbed Footer
+# 
+PAGEFOOTER();
+?>
diff --git a/www/showexp.php3 b/www/showexp.php3
index a0cdd5dd0c92fd4a2c1063771db0430f7eb5c256..c96856aa29d48c160bdccec8b8bc38fd36dcc4ab 100644
--- a/www/showexp.php3
+++ b/www/showexp.php3
@@ -12,6 +12,7 @@ include("showstuff.php3");
 #
 $uid = GETLOGIN();
 LOGGEDINORDIE($uid);
+$isadmin = ISADMIN($uid);
 
 #
 # Verify page arguments.
@@ -59,7 +60,7 @@ if (! TBExptAccessCheck($uid, $exp_pid, $exp_eid, $TB_EXPT_READINFO)) {
 #
 $query_result =
     DBQueryFatal("select e.idx,e.state,e.batchmode,e.linktest_pid,".
-		 "       s.rsrcidx,r.wirelesslans ".
+		 "       e.paniced,e.panic_date,s.rsrcidx,r.wirelesslans ".
 		 "  from experiments as e ".
 		 "left join experiment_stats as s on s.exptidx=e.idx ".
 		 "left join experiment_resources as r on s.rsrcidx=r.idx ".
@@ -71,6 +72,8 @@ $rsrcidx    = $row["rsrcidx"];
 $isbatch    = $row["batchmode"];
 $wireless   = $row["wirelesslans"];
 $linktest_running = $row["linktest_pid"];
+$paniced    = $row["paniced"];
+$panic_date = $row["panic_date"];
 
 #
 # Get a list of node types and classes in this experiment
@@ -132,7 +135,8 @@ if ($expstate) {
 	    WRITESUBMENUBUTTON("Swap Experiment In",
 			"swapexp.php3?inout=in&pid=$exp_pid&eid=$exp_eid");
 	}
-	elseif ($expstate == $TB_EXPTSTATE_ACTIVE) {
+	elseif ($expstate == $TB_EXPTSTATE_ACTIVE ||
+		($expstate == $TB_EXPTSTATE_PANICED && $isadmin)) {
 	    WRITESUBMENUBUTTON("Swap Experiment Out",
 			"swapexp.php3?inout=out&pid=$exp_pid&eid=$exp_eid");
 	}
@@ -142,8 +146,10 @@ if ($expstate) {
 			       "&pid=$exp_pid&eid=$exp_eid");
 	}
     }
-    WRITESUBMENUBUTTON("Terminate Experiment",
-		       "endexp.php3?pid=$exp_pid&eid=$exp_eid");
+    if ($expstate != $TB_EXPTSTATE_PANICED) {
+	WRITESUBMENUBUTTON("Terminate Experiment",
+			   "endexp.php3?pid=$exp_pid&eid=$exp_eid");
+    }
 
     # Batch experiments can be modifed only when paused.
     if ($expstate == $TB_EXPTSTATE_SWAPPED ||
@@ -215,7 +221,7 @@ if ($types['garcia'] || $classes['sg']) {
 		   "moteleds.php3?pid=$exp_pid&eid=$exp_eid");
 }
 
-if (ISADMIN($uid)) {
+if ($isadmin) {
     if ($expstate == $TB_EXPTSTATE_ACTIVE) {
 	SUBMENUSECTION("Beta-Test Options");
 	WRITESUBMENUBUTTON("Restart Experiment",
@@ -241,12 +247,34 @@ SUBMENUEND_2A();
 echo "<br>
       <a href='shownsfile.php3?pid=$exp_pid&eid=$exp_eid'>
          <img border=1 alt='experiment vis'
-              src='showthumb.php3?idx=$rsrcidx'></a>\n";
+              src='showthumb.php3?idx=$rsrcidx'></a>";
 
 SUBMENUEND_2B();
 
 SHOWEXP($exp_pid, $exp_eid);
 
+if (TBExptFirewall($exp_pid, $exp_eid) &&
+    ($expstate == $TB_EXPTSTATE_ACTIVE ||
+     $expstate == $TB_EXPTSTATE_PANICED ||
+     $expstate == $TB_EXPTSTATE_ACTIVATING ||
+     $expstate == $TB_EXPTSTATE_SWAPPING)) {
+    echo "<center>\n";
+    if ($paniced) {
+	echo "<br><font size=+1 color=red><blink>".
+	     "Your experiment was cut off via the Panic Button on $panic_date!".
+	     "<br>".
+	     "You will need to contact testbed operations to make further ".
+  	     "changes (swap, terminate) to your experiment.</blink></font>";
+    }
+    else {
+	echo "<br><a href='panicbutton.php3?pid=$exp_pid&eid=$exp_eid'>
+                 <img border=1 alt='panic button' src='panicbutton.gif'></a>";
+	echo "<br><font color=red size=+2>".
+	     " Press the Panic Button to contain your experiment".
+	     "</font>\n";
+    }
+    echo "</center>\n";
+}
 SUBPAGEEND();
 
 #
@@ -254,7 +282,7 @@ SUBPAGEEND();
 #
 SHOWNODES($exp_pid, $exp_eid, $sortby);
 
-if (ISADMIN($uid)) {
+if ($isadmin) {
     echo "<center>
           <h3>Experiment Stats</h3>
          </center>\n";