From 23b0cbf1b886a248c99d6bdfeb709ea4a070be3b Mon Sep 17 00:00:00 2001
From: Timothy Stack <stack@flux.utah.edu>
Date: Mon, 12 Jul 2004 14:26:47 +0000
Subject: [PATCH] Merge feedback stuff from the virt tree.

---
 sensors/slothd/GNUmakefile.in  |   4 +-
 sensors/slothd/digest-slothd   | 350 ++++++++++++++++++++++++++++++
 sensors/slothd/webfeedback.in  | 328 ++++++++++++++++++++++++++++
 tbsetup/assign_prepass.in      |  26 ++-
 tbsetup/ns2ir/GNUmakefile.in   |   2 +-
 tbsetup/ns2ir/sim.tcl.in       |  20 ++
 tbsetup/ns2ir/tb_compat.tcl.in | 375 ++++++++++++++++++++++++++++++++-
 tbsetup/ptopgen.in             |   2 +
 www/feedback.php3              | 220 +++++++++++++++++++
 www/remapexp.php3              | 181 ++++++++++++++++
 www/showexp.php3               |  13 ++
 11 files changed, 1515 insertions(+), 6 deletions(-)
 create mode 100644 sensors/slothd/digest-slothd
 create mode 100644 sensors/slothd/webfeedback.in
 create mode 100644 www/feedback.php3
 create mode 100644 www/remapexp.php3

diff --git a/sensors/slothd/GNUmakefile.in b/sensors/slothd/GNUmakefile.in
index d0c203bb35..f24a8a0f06 100644
--- a/sensors/slothd/GNUmakefile.in
+++ b/sensors/slothd/GNUmakefile.in
@@ -43,7 +43,9 @@ version.c: slothd.c slothd.h sdcollectd.c sdcollectd.h
 
 client: slothd
 
-install: $(addprefix $(INSTALL_SBINDIR)/, $(SBIN_SCRIPTS) sdcollectd)
+install: $(addprefix $(INSTALL_SBINDIR)/, $(SBIN_SCRIPTS) sdcollectd) webfeedback
+	$(INSTALL_PROGRAM) $(SRCDIR)/digest-slothd $(INSTALL_LIBEXECDIR)
+	$(INSTALL_PROGRAM) webfeedback $(INSTALL_LIBEXECDIR)
 
 client-install: client
 	$(INSTALL_PROGRAM) -s slothd $(DESTDIR)$(CLIENT_BINDIR)
diff --git a/sensors/slothd/digest-slothd b/sensors/slothd/digest-slothd
new file mode 100644
index 0000000000..50bacadc26
--- /dev/null
+++ b/sensors/slothd/digest-slothd
@@ -0,0 +1,350 @@
+#! /usr/bin/awk -f
+
+#
+# Digest slothd logs and generate a TCL file that contains resource usage data
+# that can be fed back into the mapper.
+#
+# Usage: digest-slothd <slothd-log1> [<slothd-log2> ...] <mapping>
+#
+# <slothd-logN> - The output of slothd when it is run in high resolution
+#   tracing mode.
+# <mapping> - A mapping of MAC addresses to node members and LAN/link names.
+#   XXX disgusting.
+#
+
+BEGIN {
+    OVERLOAD_MAX = 3; # XXX Make this a command line argument.
+    error = 0;
+    if( ARGC < 2 )
+    {
+	printf("Convert slothd output to a TCL feedback file.\n");
+	printf("Usage: digest-slothd <file1> [<file2> ...] <mapping>\n");
+	printf("\n");
+	printf("Required arguments:\n");
+	printf("  file1   - A file containing the output of slothd.\n");
+	printf("  mapping - A mapping of MAC addresses to node members\n");
+	printf("            and LAN/link names.");
+	printf("\n");
+	printf("Example:\n");
+	printf("  $ digest-slothd vhost-0.slothd - < mapping\n");
+	printf("\n");
+	printf("Example mapping:\n");
+	printf("  00:00:0a:01:01:02 node0 link");
+	printf("\n");
+	error = -1;
+	exit error;
+    }
+    
+    printf("# -*- TCL -*-\n");
+    printf("# Automatically generated feedback file.\n");
+    printf("#\n");
+    for( lpc = 1; lpc < ARGC; lpc++ )
+    {
+	printf("# ARGV[%d]: %s\n", lpc, ARGV[lpc]);
+    }
+    printf("#\n");
+    printf("# Generated at: %s\n", strftime());
+    printf("#\n\n");
+    printf("# BEGIN Node/LAN\n");
+}
+
+#
+# Process slothd output.  This will digest the "vnode" and "iface" attributes
+# and store the values in the following variables:
+#
+#   vnodes_cpu[<vnode_name>,<instance>] - The vnode percent of CPU used per
+#     second.
+#   vnodes_ram[<vnode_name>,<instance>] - The vnode percent of RAM used per
+#     second.
+#   links_mac_bw[<MAC addr>] - The peak kilobits per second used on the NIC.
+#   links_mac_pkts[<MAC addr>] - The peak number of packets per second used on
+#     the NIC.
+#
+# Example input:
+#
+# vers=3 mis=1084907941 lave=0.0000000000,0.0000000000,0.0000000000 abits=0x5 page=0,0 ints=1134,693,82 cpu=0,1,98 iface=00:02:b3:3f:7a:20,18804,3352,14047137,457522 iface=00:03:47:73:a2:42,9079,125118,3428409,60045802 iface=00:00:00:00:00:00,0,0,0,0 iface=00:00:0a:01:08:03,2846,125780,385828,58014417 iface=00:00:0a:01:12:03,125782,2843,58015462,385822 iface=00:00:0a:01:0d:02,6285,152,2896485,19985 iface=00:00:0a:01:12:02,2845,125781,385780,58015462 vnode=dslclient-11.testdssvm.tbres.emulab.net,0.0,1.8 vnode=server.testdssvm.tbres.emulab.net,2.1,2.5 vnode=corerouter.testdssvm.tbres.emulab.net,0.0,1.2
+#
+/vers=3/ {
+    line_count[ARGIND] += 1;
+    if( (vnode_count > 1) &&
+	(line_count[ARGIND - 1] > 0) &&
+	(line_count[ARGIND - 1] < 3) )
+    {
+#       There are not enough lines to deduce anything other than overload.
+	for( vnode_name in line_vnode_names )
+	{
+	    alerts[vnode_name] = 1;
+	}
+	for( mac in line_macs )
+	{
+	    alerts[mac] = 1;
+	}
+    }
+    vnode_count = 0;
+    total_vnode_cpu = 0;
+    time_diff_s = 0;
+    ovld = 0;
+    delete line_vnode_names;
+    delete line_macs;
+    for( lpc = 2; lpc <= NF; lpc++ )
+    {
+#       Determine the field type and
+	split($lpc, field, /=/);
+#       ... handle it.
+	if( field[1] == "stamp" )
+	{
+	    split(field[2], data, /,/);
+	    if( last_stamp_s[ARGIND] )
+	    {
+		time_diff_us = (data[1] - last_stamp_s[ARGIND]) * 1000000;
+		time_diff_us += (data[2] - last_stamp_us[ARGIND]);
+		time_diff_s = time_diff_us / 1000000.0
+	    }
+	    else
+	    {
+		time_diff_s = 1.0;
+	    }
+	    last_stamp_s[ARGIND] = data[1];
+	    last_stamp_us[ARGIND] = data[2];
+
+#           Assume overload if the time diff is relatively large.
+	    if( time_diff_s > 1.25 )
+	    {
+		ovld = 1;
+#               Signal multiple overload events for each missed time interval.
+		overload[ARGIND] += (time_diff_s - 1.0);
+	    }
+	}
+	else if( field[1] == "ovld" )
+	{
+	    if( !ovld )
+		ovld = field[2];
+	}
+	else if( field[1] == "cpu" )
+	{
+	    total_cpu = field[2];
+	}
+	else if( field[1] == "iface" )
+	{
+#           Split the data values and
+	    split(field[2], data, /,/);
+#           ... the ethernet MAC address.
+	    link_mac = data[1];
+	    links_mac[link_mac] = 1;
+#           ... the input bandwidth/packets for this second, and
+	    if( link_mac in links_last_in )
+	    {
+		last_pkt_value = links_last_pkt_in[link_mac];
+		links_last_pkt_in[link_mac] = data[2];
+		in_pkts = data[2] - last_pkt_value;
+
+		last_value = links_last_in[link_mac];
+		links_last_in[link_mac] = data[4];
+		in_bw = data[4] - last_value;
+	    }
+	    else
+	    {
+		links_last_pkt_in[link_mac] = data[2];
+		links_last_in[link_mac] = data[4];
+
+		in_pkts = 0;
+		in_bw = 0;
+	    }
+#           ... the output bandwidth/packets for this second.
+	    if( link_mac in links_last_out )
+	    {
+		last_pkt_value = links_last_pkt_out[link_mac];
+		links_last_pkt_out[link_mac] = data[3];
+		out_pkts = data[3] - last_pkt_value;
+
+		last_value = links_last_out[link_mac];
+		links_last_out[link_mac] = data[5];
+		out_bw = data[5] - last_value;
+	    }
+	    else
+	    {
+		links_last_pkt_out[link_mac] = data[3];
+		links_last_out[link_mac] = data[5];
+
+		out_pkts = 0;
+		out_bw = 0;
+	    }
+#           Make sure the measures are 'per second' since slothd will sometimes
+#           miss an interval.
+	    in_bw = in_bw / time_diff_s;
+	    in_pkts = in_pkts / time_diff_s;
+	    out_bw = out_bw / time_diff_s;
+	    out_pkts = out_pkts / time_diff_s;
+#           Find the maximum of bandwidth/packets of the data seen so far.
+	    if( in_pkts > links_mac_pkts[link_mac] )
+	    {
+		links_mac_pkts[link_mac] = in_pkts;
+	    }
+	    if( in_bw > links_mac_bw[link_mac] )
+	    {
+		links_mac_bw[link_mac] = in_bw;
+	    }
+	    if( out_pkts > links_mac_pkts[link_mac] )
+	    {
+		links_mac_pkts[link_mac] = out_pkts;
+	    }
+	    if( out_bw > links_mac_bw[link_mac] )
+	    {
+		links_mac_bw[link_mac] = out_bw;
+	    }
+	    line_macs[link_mac] = 1;
+	}
+	else if( field[1] == "vnode" )
+	{
+	    split(field[2], data, /,/);
+	    split(data[1], name, /\./);
+	    vnode_name = name[1];
+	    vnodes_name[vnode_name] = FNR;
+	    vnodes_cpu[vnode_name,FNR] = data[2];
+	    vnodes_ram[vnode_name,FNR] = data[3];
+	    line_vnode_names[vnode_name] = 1;
+	    total_vnode_cpu += data[2];
+	    vnode_count += 1;
+	}
+    }
+
+#   Need to deal with CPU time lost in interrupts.
+    if( vnode_count == 0 )
+    {
+	printf("error: no vnodes in line '%s'\n", $0) > /dev/stderr;
+	error = 1;
+	exit error;
+    }
+    else if( vnode_count == 1 )
+    {
+	vnodes_cpu[vnode_name,FNR] = total_cpu;
+    }
+    else
+    {
+#       Check if slothd signalled overload for this period.
+	if( ovld || (total_cpu >= 99) )
+	{
+#           We're in overload for this period, but do not signal an alert until
+#           we see OVERLOAD_MAX consecutive indicators.
+	    if( overload[ARGIND] >= OVERLOAD_MAX )
+	    {
+		for( vnode_name in line_vnode_names )
+		{
+		    alerts[vnode_name] = 1;
+		}
+		for( mac in line_macs )
+		{
+		    alerts[mac] = 1;
+		}
+	    }
+	    else
+	    {
+		overload[ARGIND] += 1;
+	    }
+	}
+	else
+	{
+#           We're not in overload, so we clear it, and
+	    overload[ARGIND] = 0;
+#           ... add any unaccounted for CPU to _all_ of the vnodes.  Kind of a
+#           hack, but, nothing else to do here except be conservative.
+	    diff = total_cpu - total_vnode_cpu;
+	    printf("warning: unaccounted for CPU %f\n", diff) > /dev/stderr;
+	    for( vnode_name in line_vnode_names )
+	    {
+		vnodes_cpu[vnode_name,FNR] += diff;
+	    }
+	}
+    }
+}
+
+#
+# Process the mapping lines so we can find the mapping of IP addresses to
+# symbolic link names.  Also, we generate the reservations for the node <-> LAN
+# connections, reservations for links will be done at the END when we know who
+# the maximums are for the NICs on the links.
+#
+# Example input:
+#
+#   00:00:0a:01:01:02 node0 link
+#
+# Example output:
+#
+#   set Reservations(link,node0,kbps) 123235.00
+#
+/^[[:xdigit:]][[:xdigit:]]\:[[:xdigit:]][[:xdigit:]]\:[[:xdigit:]][[:xdigit:]]\:[[:xdigit:]][[:xdigit:]]\:[[:xdigit:]][[:xdigit:]]\:[[:xdigit:]][[:xdigit:]] [[:alnum:]\-]* [[:alnum:]\-]*$/ {
+    if( $1 in links_mac )
+    {
+	printf("set Reservations(%s,%s,kbps) %f\n",
+	       $3,
+	       $2,
+	       (links_mac_bw[$1] * 8) / 1000.0);
+	printf("set Reservations(%s,%s,pps) %f\n",
+	       $3,
+	       $2,
+	       links_mac_pkts[$1]);
+	links_name[$3] = 1;
+	if( links_mac_bw[$1] > links_bw[$3] )
+	    links_bw[$3] = links_mac_bw[$1];
+	if( links_mac_pkts[$1] > links_pkts[$3] )
+	    links_pkts[$3] = links_mac_pkts[$1];
+	if( $1 in alerts )
+	{
+	    printf("set Alerts(%s,%s) 1\n", $3, $2);
+	    printf("set Alerts(%s) 1\n", $3);
+	}
+    }
+}
+
+#
+# All done, time to print out the peak values we've discovered while
+# processing.
+#
+END {
+    if( error )
+    {
+	exit error;
+    }
+
+    printf("# END Node/LAN\n\n");
+    
+    printf("# BEGIN Nodes\n");
+    for( vnode_name in vnodes_name )
+    {
+	vnode_cpu = 0.001;
+	vnode_ram = 0.001;
+#       Note that we ignore the first and last values in the log, since the
+#       first value is always 100% and I just felt like making it symmetrical
+#       so the last was dropped as well.
+	for( lpc = 2; lpc < vnodes_name[vnode_name] - 1; lpc++ )
+	{
+	    if( vnodes_cpu[vnode_name,lpc] > vnode_cpu )
+	    {
+		vnode_cpu = vnodes_cpu[vnode_name,lpc];
+	    }
+	    if( vnodes_ram[vnode_name,lpc] > vnode_ram )
+	    {
+		vnode_ram = vnodes_ram[vnode_name,lpc];
+	    }
+	}
+	printf("set Reservations(%s,cpupercent) %f\n", vnode_name, vnode_cpu);
+	printf("set Reservations(%s,rampercent) %f\n", vnode_name, vnode_ram);
+#       Check for an alert.
+	if( vnode_name in alerts )
+	{
+	    printf("set Alerts(%s) 1\n", vnode_name);
+	}
+    }
+    printf("# END Nodes\n\n");
+    printf("# BEGIN Links\n");
+    for( link_name in links_name )
+    {
+	printf("set Reservations(%s,kbps) %f\n",
+	       link_name,
+	       (links_bw[link_name] * 8) / 1000.0);
+	printf("set Reservations(%s,pps) %f\n",
+	       link_name,
+	       links_pkts[link_name]);
+    }
+    printf("# END Links\n\n");
+}
diff --git a/sensors/slothd/webfeedback.in b/sensors/slothd/webfeedback.in
new file mode 100644
index 0000000000..641471bfcf
--- /dev/null
+++ b/sensors/slothd/webfeedback.in
@@ -0,0 +1,328 @@
+#! /usr/bin/perl -wT
+
+use English;
+use Getopt::Std;
+use Socket;
+    
+#
+# Print out the usage statement for this script and exit with a -1 return code.
+#
+sub usage()
+{
+    print STDOUT
+	"Usage: webfeedback [-hc] [-d duration] pid gid eid\n".
+	"Web wrapper for dealing with feedback information.\n".
+	"\n".
+	"Required arguments:\n".
+	"  pid - The project ID.\n".
+	"  gid - The group ID.\n".
+	"  eid - The experiment ID.\n".
+	"\n".
+	"Optional arguments:\n".
+	"  -h       Print this message.\n".
+	"  -c       Clear the feedback data.\n".
+	"  -b       Clear the bootstrap data.\n".
+	"  -d secs  Record feedback for the given duration.\n";
+    
+    exit(-1);
+}
+
+#
+# Option list:
+#
+#   -h   Print the usage message.
+#
+my $optlist  = "hcbd:f";
+
+#
+# Configure variables
+#
+my $TB       = "@prefix@";
+
+# Locals
+my $digest_slothd = "$TB/libexec/digest-slothd";
+my $SAVEUID  = $UID;
+my $dbuid;
+my $pid;
+my $gid;
+my $eid;
+my $mode = "";
+my $duration;
+my $fake = 0;
+
+#
+# Untaint the path
+# 
+$ENV{'PATH'} = "$TB/bin:$TB/sbin:/bin:/usr/bin:/sbin:/usr/sbin";
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+
+#
+# Testbed Support libraries
+#
+use lib "@prefix@/lib";
+use libdb;
+use libtestbed;
+
+#
+# Parse command arguments. Once we return from getopts, all that should
+# left are the required arguments.
+#
+%options = ();
+if (! getopts($optlist, \%options)) {
+    usage();
+}
+if (defined($options{"h"})) {
+    usage();
+}
+if (defined($options{"c"}) || defined($options{"b"})) {
+    $mode = "clear";
+}
+if (defined($options{"d"})) {
+    if ($mode ne "") {
+	print STDERR "error: The clear and feedback options are mutually ".
+	    "exclusive.\n";
+	usage();
+    }
+    $mode = "record";
+    $duration = $options{"d"};
+}
+if (defined($options{"f"})) {
+    $fake = 1;
+}
+if ($mode eq "") {
+    print STDERR "error: No mode specified, use '-c' to clear feedback or ".
+	"'-d N' to record for N seconds.\n";
+    usage();
+}
+if (@ARGV != 3) {
+    usage();
+}
+
+$pid      = $ARGV[0];
+$gid      = $ARGV[1];
+$eid      = $ARGV[2];
+
+#
+# Must taint check!
+#
+if (defined($duration)) {
+    if ($duration =~ /^([0-9]+)$/) {
+	$duration = $1;
+    }
+    else {
+	die("Bad duration argument: $duration.");
+    }
+}
+
+if ($pid =~ /^([-\w]+)$/) {
+    $pid = $1;
+}
+else {
+    die("Bad pid argument: $pid.");
+}
+if ($eid =~ /^([-\w]+)$/) {
+    $eid = $1;
+}
+else {
+    die("Bad eid argument: $eid.");
+}
+if ($gid =~ /^([-\w]+)$/) {
+    $gid = $1;
+}
+else {
+    die("Bad gid argument: $gid.");
+}
+
+#
+# Experiment must exist.
+#
+if (!($state = ExpState($pid,$eid))) {
+    die("There is no experiment $eid in project $pid\n");
+}
+
+#
+# User must have permission to view the experiment.
+#
+if ($UID) {
+    if (!TBExptAccessCheck($UID, $pid, $eid, TB_EXPT_MODIFY)) {
+        die("*** You not have permission to view this experiment!\n");
+    }
+}
+
+# XXX
+$expdir = "/proj/$pid/exp/$eid";
+
+# Figure out which mode we are in and act accordingly.
+if ($mode eq "clear") {
+    if (defined($options{"c"})) {
+	unlink("$expdir/tbdata/feedback_data.tcl");
+    }
+    if (defined($options{"b"})) {
+	unlink("$expdir/tbdata/bootstrap_data.tcl");
+    }
+}
+if ($mode eq "record") {
+    
+    if ($state ne EXPTSTATE_ACTIVE) {
+	# nothing to do
+	print "Cannot record feedback for an inactive experiment.\n";
+	exit(0);
+    }
+    
+    print STDOUT "Starting tracers...\n";
+    
+    # Get the list of virtual hosts for the virtual nodes.
+    my $query_result =
+	DBQueryFatal("select node_id,vname from reserved ".
+		     "where pid='$pid' and eid='$eid' and erole='virthost'");
+    
+    # Iterate through the virthosts starting up slothd in high-resolution
+    # tracing mode.
+    while (my ($node_id,$vname) = $query_result->fetchrow_array()) {
+	my $cmd;
+	
+	$cmd = "rm -f /var/run/slothd.pid";
+	if (!$fake) {
+	    system("/usr/local/bin/sudo $TB/bin/sshtb -host $node_id \"$cmd\"");
+	}
+	$cmd = "/proj/tbres/kwebb/evslothd -e -i 1 -t ${duration}";
+	if (!$fake) {
+	    system("/usr/local/bin/sudo $TB/bin/sshtb -host $node_id \"$cmd\"");
+	}
+    }
+    
+    # Sleep for the duration of the run, then
+    sleep($duration + 3);
+    
+    $query_result->dataseek(0);
+    
+    print STDOUT "Pulling logs...\n";
+    
+    $vhost_logs = "";
+    
+    # ... iterate through the virthosts again picking up the logs.
+    while (my ($node_id,$vname) = $query_result->fetchrow_array()) {
+	my $cmd;
+	
+	$cmd = "/usr/local/bin/rsync -az ".
+	    "--rsh=\"/usr/local/bin/sudo sshtb -host \" ".
+		"${node_id}:/var/emulab/logs/ ${expdir}/logs/${vname}/";
+	$vhost_logs .= " ${expdir}/logs/${vname}/slothd.log";
+	#if (!$fake) {
+	    system($cmd);
+	#}
+    }
+    
+
+    # Now that we have the logs, we have to find out what the peak resource
+    # needs are and then dump them into a TCL file.  This TCL file is then
+    # included in the main NS file when it is reevaluated during a modify.
+    # Generating the file is actually done by a separate program,
+    # digest-slothd, but it requires a file that maps MAC addresses in the
+    # slothd log to the virtual node/link names.  The rest of this script
+    # generates that file and pipes it into digest-slothd.
+
+    # XXX Most of this code was just lifted from tbreport, it can probably be
+    # optimized a bit.
+
+    # Mappings for IP/MAC addresses and testbed internal node/port member
+    # descriptors.
+    my %ipmap;
+    my %macmap;
+    my %memmap;
+    
+    # Get the virtual node names and IP addresses and
+    my $virtnodes_result =
+	DBQueryFatal("SELECT vname,ips from virt_nodes ".
+		     "where pid='$pid' and eid='$eid' order by vname");
+
+    # ... convert them into "member" form (e.g. node0:0) so we can match them
+    # up against the virt_lans table.
+    while (($vname,$ips) = $virtnodes_result->fetchrow_array()) {
+	foreach $ipinfo (split(" ",$ips)) {
+	    ($port,$ip) = split(":",$ipinfo);
+	    $ipmap{"$vname:$port"} = $ip;
+	    $macmap{$ip}->{"MEMBER"} = "$vname:$port";
+	}
+    }
+    
+    # Get the addresses for regular interfaces and
+    my $result =
+	DBQueryFatal("select i.ip,i.mac,i.iface from reserved as r ".
+		     "left join interfaces as i on r.node_id=i.node_id ".
+		     "where r.pid='$pid' and r.eid='$eid' and ".
+		     "      i.ip is not NULL and i.ip!=''");
+    
+    # ... add the MAC and node vname to the memmap.
+    while (($ip,$mac,$iface) = $result->fetchrow_array()) {
+	if ($mac =~ /^(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})$/) {
+	    $mac = "$1:$2:$3:$4:$5:$6";
+	}
+	$macmap{$ip}->{"MAC"}   = $mac;
+	$macmap{$ip}->{"IFACE"} = $iface;
+	if (defined($macmap{$ip}->{"MEMBER"})) {
+	    my $member = $macmap{$ip}->{"MEMBER"};
+	    my ($node,$port) = split(":", $member);
+	    $memmap{$member}->{"MAC"} = $mac;
+	    $memmap{$member}->{"NODE"} = $node;
+	}
+    }
+    
+    # Get the addresses for veth interfaces and
+    $result =
+	DBQueryFatal("select i.IP,i.mac,i.iface from reserved as r ".
+		     "left join veth_interfaces as i on r.node_id=i.node_id ".
+		     "where r.pid='$pid' and r.eid='$eid' and ".
+		     "      i.IP is not NULL and i.IP!=''");
+    
+    # ... add the MAC and node vname to the memmap.
+    while (($ip,$mac,$iface) = $result->fetchrow_array()) {
+	if ($mac =~ /^(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})$/) {
+	    $mac = "$1:$2:$3:$4:$5:$6";
+	}
+	$macmap{$ip}->{"MAC"}   = $mac;
+	$macmap{$ip}->{"IFACE"} = $iface;
+	if (defined($macmap{$ip}->{"MEMBER"})) {
+	    my $member = $macmap{$ip}->{"MEMBER"};
+	    ($node,$port) = split(":", $member);
+	    $memmap{$member}->{"MAC"} = $mac;
+	    $memmap{$member}->{"NODE"} = $node;
+	}
+    }
+    
+    # Get all of the virtual LANs in this experiment and
+    $result =
+	DBQueryFatal("select vname,member ".
+		     "  from virt_lans as v ".
+		     "where pid='$pid' and eid='$eid' ".
+		     "order by vname,member");
+    
+    # ... add their vnames to the memmap.
+    while (($vname,$member) = $result->fetchrow_array()) {
+	$memmap{$member}->{"LAN"} = $vname;
+    }
+    
+    # Start digest-slothd with all of the slothd logs, tee its output to the
+    # feedback file, and
+    open(DIGESTER,
+	 "| $digest_slothd ${vhost_logs} - ".
+	 " | tee ${expdir}/tbdata/feedback_data.tcl") or
+	     fatal("Could not run digest-slothd!");
+    
+    # ... pipe in our mapping data over stdin.
+    foreach my $m (sort keys(%memmap)) {
+	if (defined($memmap{$m})) {
+	    printf DIGESTER 
+		"%s %s %s\n", 
+		$memmap{$m}->{"MAC"},
+		$memmap{$m}->{"NODE"},
+		$memmap{$m}->{"LAN"};
+	}
+    }
+    
+    # All done, cleanup.
+    close(DIGESTER) or 
+	fatal("$digest_slothd: " . ($? ? "exited with status $?."
+				    : "error closing pipe: $!"));
+}
+
diff --git a/tbsetup/assign_prepass.in b/tbsetup/assign_prepass.in
index 978d58f9be..67318237c5 100755
--- a/tbsetup/assign_prepass.in
+++ b/tbsetup/assign_prepass.in
@@ -1281,16 +1281,36 @@ sub binpack_sort(@) {
 	    }
 	}
 
+	if ($b->{'desires'} && $b->{'desires'}{'?+cpupercent'} &&
+	    $a->{'desires'} && $a->{'desires'}{'?+cpupercent'}) {
+
+	    $rv = ( $b->{'desires'}{'?+cpupercent'} <=>
+		    $a->{'desires'}{'?+cpupercent'} );
+	    if ($rv != 0) {
+		return $rv;
+	    }
+	}
+
 	# Then by memory
-	if ($b->{'desires'} && $b->{'desires'}{'?+mem'} &&
-	    $a->{'desires'} && $a->{'desires'}{'?+mem'}) {
+	if ($b->{'desires'} && $b->{'desires'}{'?+ram'} &&
+	    $a->{'desires'} && $a->{'desires'}{'?+ram'}) {
 
-	    $rv = ( $b->{'desires'}{'?+mem'} <=> $a->{'desires'}{'?+mem'} );
+	    $rv = ( $b->{'desires'}{'?+ram'} <=> $a->{'desires'}{'?+ram'} );
 	    if ($rv != 0) {
 		return $rv;
 	    }
 	}
 	
+	if ($b->{'desires'} && $b->{'desires'}{'?+rampercent'} &&
+	    $a->{'desires'} && $a->{'desires'}{'?+rampercent'}) {
+
+	    $rv = ( $b->{'desires'}{'?+rampercent'} <=>
+		    $a->{'desires'}{'?+rampercent'} );
+	    if ($rv != 0) {
+		return $rv;
+	    }
+	}
+
 	# Fall back to count if neither of the others were given
 	return $b->{'count'} <=> $a->{'count'};
     } @_;
diff --git a/tbsetup/ns2ir/GNUmakefile.in b/tbsetup/ns2ir/GNUmakefile.in
index 598603772f..4868152795 100644
--- a/tbsetup/ns2ir/GNUmakefile.in
+++ b/tbsetup/ns2ir/GNUmakefile.in
@@ -1,6 +1,6 @@
 #
 # EMULAB-COPYRIGHT
-# Copyright (c) 2000-2003 University of Utah and the Flux Group.
+# Copyright (c) 2000-2004 University of Utah and the Flux Group.
 # All rights reserved.
 #
 
diff --git a/tbsetup/ns2ir/sim.tcl.in b/tbsetup/ns2ir/sim.tcl.in
index 06c08c6d92..75d1695c4e 100644
--- a/tbsetup/ns2ir/sim.tcl.in
+++ b/tbsetup/ns2ir/sim.tcl.in
@@ -303,6 +303,26 @@ Simulator instproc run {} {
     # If any errors occur stop here.
     if {$errors == 1} {return}
 
+    # Write out the feedback "bootstrap" file.
+    var_import ::TBCOMPAT::expdir;
+    var_import ::TBCOMPAT::BootstrapReservations;
+
+    if {! [file isdirectory $expdir]} {
+	# Experiment directory does not exist, so we cannot write the file...
+    } elseif {[array size BootstrapReservations] > 0} {
+	set file [open "$expdir/tbdata/bootstrap_data.tcl" w]
+	puts $file "# -*- TCL -*-"
+	puts $file "# Automatically generated feedback bootstrap file."
+	puts $file "#"
+	puts $file "# Generated at: [clock format [clock seconds]]"
+	puts $file "#"
+	puts $file ""
+	foreach res [array names BootstrapReservations] {
+	    puts $file "set Reservations($res) $BootstrapReservations($res)"
+	}
+	close $file
+    }
+
     # If we are running in impotent mode we stop here
     if {$impotent == 1 && $passmode == 0} {return}
     
diff --git a/tbsetup/ns2ir/tb_compat.tcl.in b/tbsetup/ns2ir/tb_compat.tcl.in
index 8e0939f041..cff3ce2b4e 100644
--- a/tbsetup/ns2ir/tb_compat.tcl.in
+++ b/tbsetup/ns2ir/tb_compat.tcl.in
@@ -48,6 +48,37 @@ namespace eval TBCOMPAT {
 	$node ip $port $ip
     }
 
+    #
+    # Named argument helper function.
+    #
+    # Example:
+    #
+    #  proc replace {s args} {
+    #    named $args {-from 0 -to end -with ""}
+    #    string replace $s $(-from) $(-to) $(-with)
+    #  }
+    #
+    #  % replace suchenwirth -from 4 -to 6 -with xx
+    #  suchxxirth
+    #  % replace suchenwirth -from 4 -to 6 -witha xx
+    #  bad option '-witha', should be one of: -from -to -with
+    #
+    # @param args The optional arguments the caller received.
+    # @param defaults The option list with default values.
+    #
+    # @see http://wiki.tcl.tk/10702
+    #
+    proc named-args {args defaults} {
+	upvar 1 "" ""
+	array set "" $defaults
+	foreach {key value} $args {
+	    if {![info exists ($key)]} {
+		error "bad option '$key', should be one of: [lsort [array names {}]]"
+	    }
+	    set ($key) $value
+	}
+    }
+
     # Let's set up a hwtypes table that contains all valid hardware types.
     variable hwtypes
     variable isremote
@@ -90,6 +121,102 @@ namespace eval TBCOMPAT {
 
     # Physical node names
     variable physnodes
+
+    ## Feedback related stuff below:
+
+    # Experiment directory name.
+    variable expdir
+
+    # Mapping of "resource classes" and "reservation types" to bootstrap
+    # values, where a resource class is a symbolic string provided by the user
+    # (e.g. Client, Server), and a reservation type is a resource name provided
+    # by the system (e.g. cpupercent, kbps).  This array will be filled by the
+    # tb-feedback methods and then written out to a "bootstrap_data.tcl" file
+    # to be read in during future evaluations of the NS file.
+    variable BootstrapReservations
+
+    # The experiment directory, this is where the feedback related files will
+    # be read from and dumped to.  XXX Hacky
+    set expdir "/proj/${::GLOBALS::pid}/exp/${::GLOBALS::eid}/"
+
+    # XXX Just for now...
+    global tbxlogfile
+    if {[file exists "$expdir"]} {
+	set tbxlogfile [open "$expdir/logs/feedback.log" w];
+    }
+
+    # Get any Emulab generated feedback data from the experiment directory.
+    if {[file exists "${expdir}/tbdata/feedback_data.tcl"]} {
+	source "${expdir}/tbdata/feedback_data.tcl"
+    }
+    # Get any bootstrap feedback data from a previous run.
+    if {[file exists "${expdir}/tbdata/bootstrap_data.tcl"]} {
+	source "${expdir}/tbdata/bootstrap_data.tcl"
+    }
+
+    #
+    # Configure the default reservations for an object based on an optional
+    # "resource class".  First, the function will check for a reservation
+    # specifically made for the object, then it will try to initialize the
+    # reservation from the resource class, otherwise it does nothing and
+    # returns zero.
+    #
+    # @param object The object name for which to configure the feedback
+    #   defaults.
+    # @param rclass The "resource class" of the object or the empty string if
+    #   it is not part of any class.  This is just a symbolic string, such as
+    #   "Client" or "Server".
+    # @return One, if there is an initialized slot in the "Reservations" array
+    #   for the given object, or zero if it could not be initialized.
+    #
+    proc feedback-defaults {object rclass} {
+	var_import ::TBCOMPAT::Reservations;  # The reservations to make
+
+	if {[array get Reservations $object,*] == ""} {
+	    # No node-specific values exist, try to initialize from the rclass.
+	    if {[array get Reservations $rclass,*] != ""} {
+		# Use bootstrap feedback from a previous topology,
+		set rcdefaults [array get Reservations $rclass,*]
+		# ... substitute the node name for the rclass, and
+		regsub -all -- $rclass $rcdefaults $object rcdefaults
+		# ... add all the reservations to the table.
+		array set Reservations $rcdefaults
+		set retval 1
+	    } else {
+		# No feedback exists yet, let the caller fill it in.
+		set retval 0
+	    }
+	} else {
+	    # Node-specific values exist, use those.
+	    set retval 1
+	}
+	return $retval
+    }
+
+    #
+    # Record bootstrap feedback data for a resource class.  This function
+    # should be called for every member of a resource class so that the one
+    # with the highest reservation will be used to bootstrap.
+    #
+    # @param rclass The "resource class" for which to update the bootstrap
+    #   feedback data.  This is just a symbolic string, such as "Client" or
+    #   "Server".
+    # @param rtype The type of reservation (e.g. cpupercent,kbps).
+    # @param res The amount to reserve.
+    #
+    proc feedback-bootstrap {rclass rtype res} {
+	# The bootstrap reservations
+	var_import ::TBCOMPAT::BootstrapReservations
+
+	if {$rclass == ""} {
+	    # No class to operate on...
+	} elseif {([array get BootstrapReservations($rclass,$rtype)] == "") ||
+	    ($res > $BootstrapReservations($rclass,$rtype))} {
+		# This is either the first time this function was called for
+		# this rclass/rtype or the new value is greater than the old.
+		set BootstrapReservations($rclass,$rtype) $res
+	}
+    }
 }
 
 # IP addresses routines.  These all do some checks and convert into set-ip
@@ -892,6 +1019,253 @@ proc tb-use-physnaming {onoff} {
     set use_physnaming $onoff
 }
 
+#
+# Write to the tb-experimental log file, as defined by the tbxlogfile global
+# variable.  If the tbxlogfile variable is not set, the message is sent to
+# /dev/null.
+#
+# @param msg The message to write to the log file.
+#
+# @global tbxlogfile The path to the log file, if defined.
+#
+proc tbx-log {msg} {
+    global tbxlogfile;
+
+    if {[info exists tbxlogfile]} {
+	puts $tbxlogfile $msg
+    }
+}
+
+##
+## BEGIN Feedback
+##
+
+proc tb-feedback-vnode {vnode hardware args} {
+    var_import ::TBCOMPAT::isvirt;        # Make sure $hardware is a vnode.
+    var_import ::TBCOMPAT::Reservations;  # The reservations to make for nodes.
+    var_import ::TBCOMPAT::BootstrapReservations;  # Bootstrap file.
+    var_import ::TBCOMPAT::Alerts;        # Alert indicators
+
+    ::TBCOMPAT::named-args $args {-scale 1.2 -rclass "" -alertscale 3.0}
+
+    # Check our inputs,
+    if {[$vnode info class] != "Node"} {
+	perror "\[tb-feedback-vnode] $vnode is not a node."
+	return
+    }
+    if {(! [info exists isvirt($hardware)]) || (! $isvirt($hardware))} {
+	perror "\[tb-feedback-vnode] Unknown hardware type: $hardware"
+	return
+    }
+    if {$(-scale) <= 0.0} {
+	perror "\[tb-feedback-vnode] Feedback scale is not greater than zero: $(-scale)"
+	return
+    }
+
+    tbx-log "BEGIN feedback for $vnode"
+
+    # ... set computed default values, and
+    if {[::TBCOMPAT::feedback-defaults $vnode $(-rclass)] == 0} {
+	# No feedback exists yet, so we assume 100%.
+	set Reservations($vnode,cpupercent) 100.0
+	set Reservations($vnode,rampercent) 100.0
+	tbx-log "  Initializing node, $vnode, to one-to-one."
+    }
+
+    # ... make the reservations.
+    foreach name [array names Reservations $vnode,*] {
+	# Get the type of reservation and
+	set reservation_type [lindex [split $name {,}] 1]
+	# ... the amount consumed.
+	set raw_reservation [set Reservations($name)]
+
+	::TBCOMPAT::feedback-bootstrap \
+		$(-rclass) $reservation_type $raw_reservation
+
+	# Then scale the reservation
+	set desired_reservation [expr $raw_reservation * $(-scale)]
+	# ... making sure it is still within the range of the hardware.
+	if {$desired_reservation < 0.0} {
+	    # XXX Not allowing negative values might be too restrictive...
+	    perror "\[tb-feedback-vnode] Bad reservation value: $name = $raw_reservation"
+	    return
+	}
+	if {([array get Alerts $vnode] != "") && [set Alerts($vnode)] > 0} {
+	    tbx-log "Alert for $vnode"
+	    set desired_reservation \
+		    [expr $desired_reservation * $(-alertscale)]; # XXX
+	}
+	if {$desired_reservation > 100.0} {
+	    set desired_reservation 100.0
+	}
+	tbx-log "  $reservation_type: ${desired_reservation}"
+	# Finally, tell assign about our desire.
+	$vnode add-desire ?+${reservation_type} ${desired_reservation}
+    }
+
+    tb-set-hardware $vnode $hardware
+
+    tbx-log "END feedback for $vnode"
+}
+
+proc tb-feedback-vlan {vnode lan args} {
+    var_import ::TBCOMPAT::Reservations;   # The reservations to make for lans
+    var_import ::TBCOMPAT::Alerts;         # Alert indicators
+
+    ::TBCOMPAT::named-args $args {-scale 1.0 -rclass "" -alertscale 3.0}
+
+    if {[$vnode info class] != "Node"} {
+	perror "\[tb-feedback-vlan] $vnode is not a node."
+	return
+    }
+    if {[$lan info class] != "Lan"} {
+	perror "\[tb-feedback-vlan] $lan is not a LAN."
+	return
+    }
+    if {$(-scale) <= 0.0} {
+	perror "\[tb-feedback-vlan] Feedback scale is not greater than zero: $(-scale)"
+	return
+    }
+
+    tbx-log "BEGIN feedback for node $vnode on lan $lan"
+
+    if {[::TBCOMPAT::feedback-defaults "$vnode,$lan" $(-rclass)] == 0} {
+	# No feedback exists yet, so we assume 100%.  Fortunately, everything
+	# already assumes 100%, so we do not have to do anything extra.
+	tbx-log "  Initializing vlan, $vnode $lan, to one-to-one."
+    }
+
+    foreach name [array names Reservations ${vnode},${lan},kbps] {
+	# Get the type of reservation and
+	set reservation_type [lindex [split $name {,}] 1]
+	# ... its value.
+	set raw_reservation [set Reservations($name)]
+	tbx-log "  raw: $raw_reservation"
+	# Get the maximum allowed value and
+	set max_reservation 0
+	foreach pair [$lan array names bandwidth "${vnode} *"] {
+	    tbx-log "  pair: $pair - [$lan set bandwidth($pair)]"
+	    if {[$lan set bandwidth($pair)] > $max_reservation} {
+		set max_reservation [$lan set bandwidth($pair)]
+	    }
+	}
+	tbx-log "  max: $max_reservation"
+	# ... fix any measuring/shaping error.
+	if {$raw_reservation > $max_reservation} {
+	    tbx-log "  request > max: $raw_reservation $max_reservation"
+	    set raw_reservation $max_reservation
+	}
+
+	::TBCOMPAT::feedback-bootstrap \
+		$(-rclass) $reservation_type $raw_reservation
+
+	# Then scale the reservation
+	set desired_reservation \
+		[expr int(sqrt($raw_reservation * $max_reservation) * $(-scale))]
+	# ... making sure it is still within the range of the hardware.
+	if {$desired_reservation < 0.0} {
+	    # XXX Not allowing negative values might be too restrictive...
+	    perror "\[tb-feedback-vlan] Bad reservation value: $name = $raw_reservation"
+	} elseif {$desired_reservation < 10.0} {
+	    set desired_reservation 10; # XXX see parse.tcl.in
+	}
+	if {([array get Alerts $lan,$vnode] != "") &&
+	    [set Alerts($lan,$vnode)] > 0} {
+	    tbx-log "Alert for $lan, $vnode"
+	    set desired_reservation \
+		    [expr $desired_reservation * $(-alertscale)]; # XXX
+	}
+	if {$desired_reservation > $max_reservation} {
+	    set desired_reservation $max_reservation
+	}
+
+	tbx-log "  $reservation_type: ${desired_reservation}"
+
+	# Finally, adjust the cap.
+	tb-set-node-lan-est-bandwidth $vnode $lan ${desired_reservation}kb
+    }
+    
+    tbx-log "END feedback for node $vnode on lan $lan"
+}
+
+proc tb-feedback-vlink {link args} {
+    var_import ::TBCOMPAT::Reservations;   # The reservations to make for links
+    var_import ::TBCOMPAT::Alerts;         # Alert indicators
+
+    ::TBCOMPAT::named-args $args {-scale 1.2 -rclass "" -alertscale 3.0}
+
+    if {[$link info class] != "Link"} {
+	perror "\[tb-feedback-vlink] $link is not a link."
+	return
+    }
+    if {$(-scale) <= 0.0} {
+	perror "\[tb-feedback-vlink] Feedback scale is not greater than zero: $(-scale)"
+	return
+    }
+
+    tbx-log "BEGIN feedback for link $link"
+
+    if {[::TBCOMPAT::feedback-defaults $link $(-rclass)] == 0} {
+	# No feedback exists yet, so we assume 100%.  Fortunately, not
+	# specifying anything implies 100%, so we do not have to do anything
+	# extra.
+	tbx-log "  Initializing vlink, $link, to one-to-one."
+    }
+
+    foreach name [array names Reservations $link,kbps] {
+	# Get the type of reservation and
+	set reservation_type [lindex [split $name {,}] 1]
+	# ... its value.
+	set raw_reservation [set Reservations($name)]
+	# Get the maximum allowed value and
+	set max_reservation 0
+	foreach pair [$link array names bandwidth] {
+	    if {[$link set bandwidth($pair)] > $max_reservation} {
+		set max_reservation [$link set bandwidth($pair)]
+	    }
+	}
+	# ... fix any measuring/shaping error.
+	if {$raw_reservation > $max_reservation} {
+	    tbx-log "  request > max: $raw_reservation $max_reservation"
+	    set raw_reservation $max_reservation
+	}
+
+	::TBCOMPAT::feedback-bootstrap \
+		$(-rclass) $reservation_type $raw_reservation
+
+	# Then scale the reservation
+	set desired_reservation \
+		[expr int(sqrt($raw_reservation * $max_reservation))]
+	# ... making sure it is still within the range of the hardware.
+	if {$desired_reservation < 0.0} {
+	    # XXX Not allowing negative values might be too restrictive...
+	    perror "\[tb-feedback-vlink] Bad reservation value: $name = $raw_reservation"
+	    return
+	} elseif {$desired_reservation < 10.0} {
+	    set desired_reservation 10; # XXX see parse.tcl.in
+	}
+	if {([array get Alerts $link] != "") && [set Alerts($link)] > 0} {
+	    tbx-log "Alert for $link"
+	    set desired_reservation \
+		    [expr $desired_reservation * $(-alertscale)]; # XXX
+	}
+	if {$desired_reservation > $max_reservation} {
+	    set desired_reservation $max_reservation
+	}
+
+	tbx-log "  $reservation_type: ${desired_reservation}"
+
+	# Finally, adjust the cap.
+	tb-set-link-est-bandwidth $link ${desired_reservation}kb
+    }
+    
+    tbx-log "END feedback for link $link"
+}
+
+##
+## END Feedback
+##
+
 #
 # User indicates that this is a modelnet experiment. Be default, the number
 # of core and edge nodes is set to one each. The user must increase those
@@ -921,4 +1295,3 @@ proc tb-set-modelnet-physnodes {cores edges} {
     set modelnet_cores $cores
     set modelnet_edges $edges
 }
-
diff --git a/tbsetup/ptopgen.in b/tbsetup/ptopgen.in
index fb6a21cff0..da6cf9a7bc 100644
--- a/tbsetup/ptopgen.in
+++ b/tbsetup/ptopgen.in
@@ -388,6 +388,8 @@ foreach $node (keys(%nodes)) {
 	# Add CPU and RAM information
 	push @features, "?+cpu:$cpu_speed";
 	push @features, "?+ram:$ram";
+	push @features, "?+cpupercent:100";
+	push @features, "?+rampercent:100";
     }
 
     # Add features
diff --git a/www/feedback.php3 b/www/feedback.php3
new file mode 100644
index 0000000000..f5d057eb68
--- /dev/null
+++ b/www/feedback.php3
@@ -0,0 +1,220 @@
+<?php
+#
+# EMULAB-COPYRIGHT
+# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# All rights reserved.
+#
+include("defs.php3");
+include("showstuff.php3");
+
+#
+# No PAGEHEADER since we spit out a Location header later. See below.
+# 
+
+#
+# Only known and logged in users can do this.
+#
+$uid = GETLOGIN();
+LOGGEDINORDIE($uid);
+
+#
+# Check to make sure a valid experiment.
+#
+if (isset($pid) && strcmp($pid, "") &&
+    isset($eid) && strcmp($eid, "")) {
+    if (! TBvalid_eid($eid)) {
+	PAGEARGERROR("$eid contains invalid characters!");
+    }
+    if (! TBvalid_pid($pid)) {
+	PAGEARGERROR("$pid contains invalid characters!");
+    }
+    if (! TBValidExperiment($pid, $eid)) {
+	USERERROR("$pid/$eid is not a valid experiment!", 1);
+    }
+    if (! TBExptAccessCheck($uid, $pid, $eid, $TB_EXPT_MODIFY)) {
+	USERERROR("You do not have permission to run feedback on $pid/$eid!",
+		  1);
+    }
+}
+else {
+    PAGEARGERROR("Must specify pid and eid!");
+}
+
+if (isset($mode) && strcmp($mode, "")) {
+    if (strcmp($mode, "record") && strcmp($mode, "clear")) {
+	PAGEARGERROR("Mode value, $mode, is not 'record' or 'clear'!");
+    }
+}
+else {
+    PAGEARGERROR("Must specify mode!");
+}
+
+$query_result = DBQueryFatal("select gid from experiments ".
+			     "where pid='$pid' and eid='$eid'");
+$row = mysql_fetch_array($query_result);
+$gid = $row[0];
+
+#
+# Get the duration of the feedback run, default to 30 seconds if nothing was
+# given.
+#
+if (!isset($duration) || $duration == "") {
+	$duration = 30; # seconds
+}
+elseif (! TBvalid_tinyint($duration) || $duration < 3) {
+	PAGEARGERROR("Duration must be an integer >= 3");
+}
+
+#
+# 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 we should
+# probably redirect the browser back up a level.
+#
+if ($canceled) {
+    PAGEHEADER("$mode Feedback");
+	
+    echo "<center><h3><br>
+          Operation canceled!
+          </h3></center>\n";
+    
+    PAGEFOOTER();
+    return;
+}
+
+if (!$confirmed) {
+    PAGEHEADER("$mode feedback");
+
+    echo "<font size=+2>Experiment <b>".
+	"<a href='showproject.php3?pid=$pid'>$pid</a>/".
+	"<a href='showexp.php3?pid=$pid&eid=$eid'>$eid</a></b></font>\n";
+
+    if(strcmp($mode, "record") == 0) {
+	echo "<center><font size=+2><br>
+                      How much feedback data should be recorded?<br>
+                      Must be atleast 3 seconds.
+                      </font>\n";
+    }
+    else if(strcmp($mode, "clear") == 0) {
+	echo "<center><font size=+2><br>
+                      Really clear feedback data?
+                      </font>\n";
+    }
+    
+    SHOWEXP($pid, $eid, 1);
+    
+    echo "<form action=feedback.php3 method=get>";
+    echo "<input type=hidden name=pid value=$pid>\n";
+    echo "<input type=hidden name=eid value=$eid>\n";
+    echo "<input type=hidden name=mode value=$mode>\n";
+
+    if(strcmp($mode, "record") == 0) {
+	echo "<table align=center border=1>\n";
+	echo "<tr>
+                  <td>
+                  <input type='text' name='duration' value='$duration'> seconds
+                  </td>
+              </tr>
+              </table><br>\n";
+    }
+    else if(strcmp($mode, "clear") == 0) {
+	echo "<br>Data to clear:</br>";
+	echo "<table align=center border=1>\n";
+	echo "<tr>
+                  <td>
+                  <input type='checkbox' name='clear_last' value='1' checked='1'> Last recorded trace.
+                  </td>
+              </tr>
+              <tr>
+                  <td>
+                  <input type='checkbox' name='clear_bootstrap' value='1'> Bootstrap data.
+                  </td>
+              </tr>
+              </table><br>\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;
+}
+
+function SPEWCLEANUP()
+{
+    global $fp;
+
+    if (connection_aborted() && $fp) {
+	pclose($fp);
+    }
+    exit();
+}
+
+# For backend.
+TBGroupUnixInfo($pid, $gid, $unix_gid, $unix_name);
+
+if (strcmp($mode, "record") == 0) {
+    #
+    # A cleanup function to keep the child from becoming a zombie, since
+    # the script is terminated, but the children are left to roam.
+    #
+    $fp = 0;
+    
+    register_shutdown_function("SPEWCLEANUP");
+    ignore_user_abort(1);
+    
+    # Start the script and pipe its output to the user.
+    $fp = popen("$TBSUEXEC_PATH $uid $unix_gid webfeedback -d $duration $pid $gid $eid",
+		"r");
+    if (! $fp) {
+	USERERROR("Feedback failed!", 1);
+    }
+
+    header("Content-Type: text/plain");
+    header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
+    header("Cache-Control: no-cache, must-revalidate");
+    header("Pragma: no-cache");
+    flush();
+    
+    echo date("D M d G:i:s T");
+    echo "\n";
+    echo "Recording feedback for $duration seconds\n";
+    flush();
+    while (!feof($fp)) {
+	$string = fgets($fp, 1024);
+	echo "$string";
+	flush();
+    }
+    $retval = pclose($fp);
+    $fp = 0;
+    if ($retval == 0)
+	echo "Feedback run was successful!\n";
+    echo date("D M d G:i:s T");
+    echo "\n";
+    return;
+}
+else if (strcmp($mode, "clear") == 0) {
+    PAGEHEADER("Clearing feedback");
+
+    echo "foo $clear_last $clear_bootstrap";
+    
+    $options = "";
+    if (isset($clear_last) && !strcmp($clear_last, "1"))
+	$options .= " -c";
+    if (isset($clear_bootstrap) && !strcmp($clear_bootstrap, "1"))
+	$options .= " -b";
+    if ($options != "") {
+	$retval = SUEXEC($uid, $unix_gid,
+			 "webfeedback $options $pid $gid $eid",
+			 SUEXEC_ACTION_USERERROR);
+    }
+    
+    echo "<center><h3><br>Done! $options</h3></center>\n";
+
+    PAGEFOOTER();
+    return;
+}
+
+?>
diff --git a/www/remapexp.php3 b/www/remapexp.php3
new file mode 100644
index 0000000000..0366a792f5
--- /dev/null
+++ b/www/remapexp.php3
@@ -0,0 +1,181 @@
+<?php
+#
+# EMULAB-COPYRIGHT
+# Copyright (c) 2000-2004 University of Utah and the Flux Group.
+# All rights reserved.
+#
+include("defs.php3");
+include("showstuff.php3");
+
+PAGEHEADER("Remap Virtual Nodes");
+
+#
+# Only known and logged in users can do this.
+#
+$uid = GETLOGIN();
+LOGGEDINORDIE($uid);
+
+#
+# Check to make sure a valid experiment.
+#
+if (isset($pid) && strcmp($pid, "") &&
+    isset($eid) && strcmp($eid, "")) {
+    if (! TBvalid_eid($eid)) {
+	PAGEARGERROR("$eid is contains invalid characters!");
+    }
+    if (! TBvalid_pid($pid)) {
+	PAGEARGERROR("$pid is contains invalid characters!");
+    }
+    if (! TBValidExperiment($pid, $eid)) {
+	USERERROR("$pid/$eid is not a valid experiment!", 1);
+    }
+    if (! TBExptAccessCheck($uid, $pid, $eid, $TB_EXPT_MODIFY)) {
+	USERERROR("You do not have permission to run remap on $pid/$eid!",
+		  1);
+    }
+}
+else {
+    PAGEARGERROR("Must specify pid and eid!");
+}
+
+$expstate = TBExptState($pid, $eid);
+
+if (strcmp($expstate, $TB_EXPTSTATE_ACTIVE) &&
+    strcmp($expstate, $TB_EXPTSTATE_SWAPPED)) {
+    USERERROR("You cannot remap an experiment in transition.", 1);
+}
+
+if (!strcmp($expstate, $TB_EXPTSTATE_ACTIVE)) {
+	$reboot = 1;
+	$eventrestart = 1;
+}
+else {
+	$reboot = 0;
+	$eventrestart = 0;
+}
+
+if ($canceled) {
+	
+    echo "<center><h3><br>
+          Operation canceled!
+          </h3></center>\n";
+    
+    PAGEFOOTER();
+    return;
+}
+
+if (!$confirmed) {
+
+    echo "<font size=+2>Experiment <b>".
+	"<a href='showproject.php3?pid=$pid'>$pid</a>/".
+	"<a href='showexp.php3?pid=$pid&eid=$eid'>$eid</a></b></font>\n";
+
+    echo "<center><font size=+2><br>
+              Are you sure you want to remap your experiment?
+              </font>\n";
+
+    SHOWEXP($pid, $eid, 1);
+
+    echo "<form action=remapexp.php3 method=post>";
+    echo "<input type=hidden name=pid value=$pid>\n";
+    echo "<input type=hidden name=eid value=$eid>\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;
+}
+
+# For backend.
+TBExptGroup($pid, $eid, $gid);
+TBGroupUnixInfo($pid, $gid, $unix_gid, $unix_name);
+
+echo "<center>";
+echo "<h2>Starting experiment remap. Please wait a moment ...
+      </h2></center>";
+
+flush();
+
+#	
+# Avoid SIGPROF in child.
+# 
+set_time_limit(0);
+
+$query_result = DBQueryFatal("SELECT nsfile from nsfiles ".
+			     "where pid='$pid' and eid='$eid'");
+if (mysql_num_rows($query_result)) {
+    $row    = mysql_fetch_array($query_result);
+    $nsdata = stripslashes($row[nsfile]);
+}
+else {
+    $nsdata = ""; # XXX what to do...
+}
+
+list($usec, $sec) = explode(' ', microtime());
+srand((float) $sec + ((float) $usec * 100000));
+$foo = rand();
+$nsfile = "/tmp/$uid-$foo.nsfile";
+
+if (! ($fp = fopen($nsfile, "w"))) {
+    TBERROR("Could not create temporary file $nsfile", 1);
+}
+fwrite($fp, $nsdata);
+fclose($fp);
+chmod($nsfile, 0666);
+
+$retval = SUEXEC($uid, $unix_gid,
+		 "webswapexp " . ($reboot ? "-r " : "") .
+		 ($eventrestart ? "-e " : "") .
+  		 "-s modify $pid $eid $nsfile",
+		 SUEXEC_ACTION_IGNORE);
+
+unlink($nsfile);
+
+#
+# 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 experiment is swapping, or will be.
+#
+echo "<br>\n";
+if ($retval) {
+    echo "<h3>Experiment remap could not proceed: $retval</h3>";
+    echo "<blockquote><pre>$suexec_output<pre></blockquote>";
+}
+else {
+    #
+    # Exit status 0 means the experiment is modifying.
+    #
+    echo "<br>";
+    echo "Your experiment is being remapped!<br><br>";
+
+    echo "You will be notified via email when the experiment has ".
+	"finished remapping and you are able to proceed. This ".
+	"typically takes less than 10 minutes, depending on the ".
+	"number of nodes in the experiment. ".
+	"If you do not receive email notification within a ".
+	"reasonable amount time, please contact $TBMAILADDR. ".
+	"<br><br>".
+	"While you are waiting, you can watch the log of experiment ".
+	"modification in ".
+	"<a target=_blank href=spewlogfile.php3?pid=$pid&eid=$eid> ".
+	"realtime</a>.\n";
+}
+
+#
+# Standard Testbed Footer
+# 
+PAGEFOOTER();
+?>
diff --git a/www/showexp.php3 b/www/showexp.php3
index d98f8e9200..b3ddad5636 100644
--- a/www/showexp.php3
+++ b/www/showexp.php3
@@ -166,8 +166,21 @@ if ($expstate == $TB_EXPTSTATE_ACTIVE) {
 	WRITESUBMENUBUTTON(($linktest_running ?
 			    "Stop LinkTest" : "Run LinkTest"), 
 			   "linktest.php3?pid=$exp_pid&eid=$exp_eid");
+	
+	WRITESUBMENUBUTTON("Record Feedback Data",
+			   "feedback.php3?pid=$exp_pid&eid=$exp_eid&mode=record");
     }
 }
+
+if (($expstate == $TB_EXPTSTATE_ACTIVE ||
+     $expstate == $TB_EXPTSTATE_SWAPPED) &&
+    STUDLY()) {
+    WRITESUBMENUBUTTON("Clear Feedback Data",
+		       "feedback.php3?pid=$exp_pid&eid=$exp_eid&mode=clear");
+    
+    WRITESUBMENUBUTTON("Remap Virtual Nodes",
+		       "remapexp.php3?pid=$exp_pid&eid=$exp_eid");
+}
     
 # Wireless maps if experiment includes wireless lans.
 if ($wireless) {
-- 
GitLab