diff --git a/backend/GNUmakefile.in b/backend/GNUmakefile.in index 07550b74b10d244affc8206ded34e917f079db7e..9743a963fe9753fc75e4d1197b91032eadf6152a 100644 --- a/backend/GNUmakefile.in +++ b/backend/GNUmakefile.in @@ -12,8 +12,8 @@ UNIFIED = @UNIFIED_BOSS_AND_OPS@ include $(OBJDIR)/Makeconf -BIN_SCRIPTS = moduserinfo newgroup newmmlist -WEB_BIN_SCRIPTS = webmoduserinfo webnewgroup webnewmmlist +BIN_SCRIPTS = moduserinfo newgroup newmmlist editexp +WEB_BIN_SCRIPTS = webmoduserinfo webnewgroup webnewmmlist webeditexp WEB_SBIN_SCRIPTS= LIBEXEC_SCRIPTS = $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS) diff --git a/backend/editexp.in b/backend/editexp.in new file mode 100644 index 0000000000000000000000000000000000000000..9dd5a475fa0b5e1b639b38e6127f76155e290cc9 --- /dev/null +++ b/backend/editexp.in @@ -0,0 +1,447 @@ +#!/usr/bin/perl -wT +# +# EMULAB-COPYRIGHT +# Copyright (c) 2000-2007 University of Utah and the Flux Group. +# All rights reserved. +# +use English; +use strict; +use Getopt::Std; +use XML::Simple; +use Data::Dumper; + +# +# Back-end script to change experiment info from an XML description. +# +sub usage() +{ + print("Usage: editexp [-v] <xmlfile>\n"); + exit(-1); +} +my $optlist = "dv"; +my $debug = 0; +my $verify = 0; # Check data and return status only. + +# +# Configure variables +# +my $TB = "@prefix@"; +my $TBOPS = "@TBOPSEMAIL@"; +my $TBAUDIT = "@TBAUDITEMAIL@"; + +# +# Untaint the path +# +$ENV{'PATH'} = "$TB/bin:$TB/sbin:/bin:/usr/bin:/usr/bin:/usr/sbin"; +delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + +# +# Turn off line buffering on output +# +$| = 1; + +# +# Load the Testbed support stuff. +# +use lib "@prefix@/lib"; +use libdb; +use libtestbed; +use User; +use Project; +use Experiment; + +# Protos +sub fatal($); +sub UserError(;$); +sub escapeshellarg($); + +# +# Parse command arguments. Once we return from getopts, all that should be +# left are the required arguments. +# +my %options = (); +if (! getopts($optlist, \%options)) { + usage(); +} +if (defined($options{"d"})) { + $debug = 1; +} +if (defined($options{"v"})) { + $verify = 1; +} +if (@ARGV != 1) { + usage(); +} +my $xmlfile = shift(@ARGV); + +# +# Map invoking user to object. +# If invoked as "nobody" we are coming from the web interface and the +# current user context is "implied" (see tbauth.php3). +# +my $this_user; + +if (getpwuid($UID) ne "nobody") { + $this_user = User->ThisUser(); + + if (! defined($this_user)) { + fatal("You ($UID) do not exist!"); + } + # You don't need admin privileges to change experiment info. +} +else { + # + # Check the filename when invoked from the web interface; must be a + # file in /tmp. + # + if ($xmlfile =~ /^([-\w\.\/]+)$/) { + $xmlfile = $1; + } + else { + fatal("Bad data in pathname: $xmlfile"); + } + + # Use realpath to resolve any symlinks. + my $translated = `realpath $xmlfile`; + if ($translated =~ /^(\/tmp\/[-\w\.\/]+)$/) { + $xmlfile = $1; + } + else { + fatal("Bad data in translated pathname: $xmlfile"); + } + + # The web interface (and in the future the xmlrpc interface) sets this. + $this_user = User->ImpliedUser(); + + if (! defined($this_user)) { + fatal("Cannot determine implied user!"); + } +} + +# +# These are the fields that we allow to come in from the XMLfile. +# +my $SLOT_OPTIONAL = 0x1; # The field is not required. +my $SLOT_REQUIRED = 0x2; # The field is required and must be non-null. +my $SLOT_ADMINONLY = 0x4; # Only admins can set this field. +# +# XXX We should encode all of this in the DB so that we can generate the +# forms on the fly, as well as this checking code. +# +my %xmlfields = + # XML Field Name DB slot name Flags Default + ("experiment" => ["eid", $SLOT_REQUIRED], + + # The rest are optional, so we can skip passing ones that are not changing. + "description" => ["description", $SLOT_OPTIONAL], + "idle_ignore" => ["idle_ignore", $SLOT_OPTIONAL], + "swappable" => ["swappable", $SLOT_OPTIONAL], + "noswap_reason" => ["noswap_reason", $SLOT_OPTIONAL], + "idleswap" => ["idleswap", $SLOT_OPTIONAL], + "idleswap_timeout" => ["idleswap_timeout", $SLOT_OPTIONAL], + "noidleswap_reason"=> ["noidleswap_reason",$SLOT_OPTIONAL], + "autoswap" => ["autoswap", $SLOT_OPTIONAL], + "autoswap_timeout" => ["autoswap_timeout", $SLOT_OPTIONAL], + "savedisk" => ["savedisk", $SLOT_OPTIONAL], + "cpu_usage" => ["cpu_usage", $SLOT_OPTIONAL], + "mem_usage" => ["mem_usage", $SLOT_OPTIONAL], + "batchmode" => ["batchmode", $SLOT_OPTIONAL], + "linktest_level" => ["linktest_level", $SLOT_OPTIONAL]); + +# +# Must wrap the parser in eval since it exits on error. +# +my $xmlparse = eval { XMLin($xmlfile, + VarAttr => 'name', + ContentKey => '-content', + SuppressEmpty => undef); }; +fatal($@) + if ($@); + +# +# Process and dump the errors (formatted for the web interface). +# We should probably XML format the errors instead but not sure I want +# to go there yet. +# +my %errors = (); + +# +# Make sure all the required arguments were provided. +# +foreach my $key (keys(%xmlfields)) { + my (undef, $required, undef) = @{$xmlfields{$key}}; + + $errors{$key} = "Required value not provided" + if ($required & $SLOT_REQUIRED && + ! exists($xmlparse->{'attribute'}->{"$key"})); +} +UserError() + if (keys(%errors)); + +# +# We build up an array of arguments to pass to Experiment->Editexp() as we check +# the attributes. +# +my %editexp_args = (); + +foreach my $key (keys(%{ $xmlparse->{'attribute'} })) { + my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'}; + + if ($debug) { + print STDERR "User attribute: '$key' -> '$value'\n"; + } + + $errors{$key} = "Unknown attribute" + if (!exists($xmlfields{$key})); + + my ($dbslot, $required, $default) = @{$xmlfields{$key}}; + + if ($required & $SLOT_REQUIRED) { + # A slot that must be provided, so do not allow a null value. + if (!defined($value)) { + $errors{$key} = "Must provide a non-null value"; + next; + } + } + if ($required & $SLOT_OPTIONAL) { + # Optional slot. If value is null skip it. Might not be the correct + # thing to do all the time? + if (!defined($value)) { + next + if (!defined($default)); + $value = $default; + } + } + if ($required & $SLOT_ADMINONLY) { + # Admin implies optional, but thats probably not correct approach. + $errors{$key} = "Administrators only" + if (! $this_user->IsAdmin()); + } + + # Now check that the value is legal. + if (! TBcheck_dbslot($value, "experiments", + $dbslot, TBDB_CHECKDBSLOT_ERROR)) { + $errors{$key} = TBFieldErrorString(); + next; + } + + $editexp_args{$dbslot} = $value; +} +UserError() + if (keys(%errors)); + +# +# Now do special checks. +# +my $doemail = 0; + +my $experiment = Experiment->Lookup($editexp_args{"eid"}); +if (!defined($experiment)) { + UserError("Experiment: No such experiment"); +} +if (!$experiment->AccessCheck($this_user, TB_EXPT_MODIFY())) { + UserError("Experiment: Not enough permission"); +} + +# +# Description must not be blank. +# +if ((exists($editexp_args{"description"}) ? + $editexp_args{"description"} eq "" : + $experiment->description() eq "")) { + UserError("Description: Missing Field"); +} + +# +# Swappable/Idle Ignore +# Any of these which are not "1" become "0". +# +# Idle Ignore +# +if (exists($editexp_args{"idle_ignore"})) { + if ($editexp_args{"idle_ignore"} ne "1") { + $editexp_args{"idle_ignore"} = 0; + } +} + +# +# Swappable +# +if (exists($editexp_args{"swappable"})) { + if ($editexp_args{"swappable"} ne "1") { + $editexp_args{"swappable"} = 0; + + # Turning off swappable, must provide justification. + if ((exists($editexp_args{"noswap_reason"}) ? + $editexp_args{"noswap_reason"} eq "" : + $experiment->noswap_reason() eq "")) { + if (!$this_user->IsAdmin()) { + UserError("Swappable: No justification provided"); + } + else { + $editexp_args{"noswap_reason"} = "ADMIN"; + } + } + if ($experiment->swappable()) { + $doemail = 1; + } + } +} +if (exists($editexp_args{"noswap_reason"})) { + $editexp_args{"noswap_reason"} = + escapeshellarg($editexp_args{"noswap_reason"}); +} + +# +# IdleSwap +# +my $idleswaptimeout = TBGetSiteVar("idle/threshold"); +if (exists($editexp_args{"idleswap_timeout"})) { + if ($editexp_args{"idleswap_timeout"} <= 0 || + ($editexp_args{"idleswap_timeout"} > $idleswaptimeout && + !$this_user->IsAdmin())) { + UserError("Idleswap: Invalid time provided" . + " (0 < X <= $idleswaptimeout)"); + } +} +if (exists($editexp_args{"idleswap"})) { + if ($editexp_args{"idleswap"} ne "1") { + $editexp_args{"idleswap"} = 0; + + # Turning off idleswap, must provide justification. + if ((exists($editexp_args{"noidleswap_reason"}) ? + $editexp_args{"noidleswap_reason"} eq "" : + $experiment->noidleswap_reason() eq "")) { + if (! $this_user->IsAdmin()) { + UserError("IdleSwap: No justification provided"); + } + else { + $editexp_args{"noidleswap_reason"} = "ADMIN"; + } + } + if ($experiment->idleswap()) { + $doemail = 1; + } + #XXX $editexp_args{"idleswap_timeout"} = 0; + } +} +if (exists($editexp_args{"noidleswap_reason"})) { + $editexp_args{"noidleswap_reason"} = + escapeshellarg($editexp_args{"noidleswap_reason"}); +} + +# +# AutoSwap +# +if (exists($editexp_args{"autoswap_timeout"})) { + if ($editexp_args{"autoswap_timeout"} <= 0) { + UserError("Max Duration: Invalid time provided"); + } +} +if (exists($editexp_args{"autoswap"})) { + if ($editexp_args{"autoswap"} ne "1") { + $editexp_args{"autoswap"} = 0; + #XXX $editexp_args{"autoswap_timeout"} = 0; + } +} + +# +# Swapout disk state saving +# +if (exists($editexp_args{"savedisk"})) { + if ($editexp_args{"savedisk"} ne "1") { + $editexp_args{"savedisk"} = 0; + } +} + +# +# CPU Usage +# +if (exists($editexp_args{"cpu_usage"}) && + $editexp_args{"cpu_usage"} ne "") { + + if ($editexp_args{"cpu_usage"} < 0 || + $editexp_args{"cpu_usage"} > 5) { + UserError("CPU Usage: Invalid (0 <= X <= 5)"); + } +} + +# +# Mem Usage +# +if (exists($editexp_args{"mem_usage"}) && + $editexp_args{"mem_usage"} ne "") { + + if ($editexp_args{"mem_usage"} < 0 || + $editexp_args{"mem_usage"} > 5) { + UserError("Mem Usage: Invalid (0 <= X <= 5)"); + } +} + +# +# Linktest level +# +if (exists($editexp_args{"linktest_level"}) && + $editexp_args{"linktest_level"} ne "") { + + if ($editexp_args{"linktest_level"} < 0 || + $editexp_args{"linktest_level"} > 4) { + UserError("Linktest Level: Invalid (0 <= X <= 4)"); + } +} + +exit(0) + if ($verify); + +# +# Now safe to change experiment info. +# +# We pass the Experiment along as an argument to EditExp(), so remove it from +# the argument array. +# +delete($editexp_args{"experiment"}); + +my $usrerr; +my $editexp_val = Experiment->EditExp($experiment, $this_user, $doemail, + \%editexp_args, \$usrerr); +UserError($usrerr) + if (defined($usrerr)); +fatal("Could not modify Experiment!") + if (!defined($editexp_val)); + +exit(0); + +sub fatal($) +{ + my ($mesg) = @_; + + print STDERR "*** $0:\n". + " $mesg\n"; + # Exit with negative status so web interface treats it as system error. + exit(-1); +} + +sub UserError(;$) +{ + my ($mesg) = @_; + + if (keys(%errors)) { + foreach my $key (keys(%errors)) { + my $val = $errors{$key}; + print "${key}: $val\n"; + } + } + print "$mesg\n" + if (defined($mesg)); + + # Exit with positive status so web interface treats it as user error. + exit(1); +} + +sub escapeshellarg($) +{ + my ($str) = @_; + + $str =~ s/[^[:alnum:]]/\\$&/g; + return $str; +} diff --git a/configure b/configure index 006a1b7b283532aefb3f78039f5d75497ec2d195..cf944cb2313d8e63b838aa1cb3c76459f07d5339 100755 --- a/configure +++ b/configure @@ -2428,7 +2428,7 @@ outfiles="$outfiles Makeconf GNUmakefile \ account/addpubkey account/addsfskey account/genpubkeys \ account/quotamail account/mkusercert account/newproj account/newuser \ backend/GNUmakefile backend/moduserinfo backend/newgroup \ - backend/newmmlist \ + backend/newmmlist backend/editexp \ tbsetup/GNUmakefile tbsetup/console_setup tbsetup/spewlogfile \ tbsetup/spewrpmtar tbsetup/gentopofile tbsetup/power_sgmote.pm \ tbsetup/console_reset tbsetup/bwconfig tbsetup/power_rpc27.pm \ diff --git a/configure.in b/configure.in index 103ed1e7bc5ea7a3c02dccf2558b5d5c2e375bae..fe0ee04c9f2d095699d458620afef5046670d1c0 100755 --- a/configure.in +++ b/configure.in @@ -808,7 +808,7 @@ outfiles="$outfiles Makeconf GNUmakefile \ account/addpubkey account/addsfskey account/genpubkeys \ account/quotamail account/mkusercert account/newproj account/newuser \ backend/GNUmakefile backend/moduserinfo backend/newgroup \ - backend/newmmlist \ + backend/newmmlist backend/editexp \ tbsetup/GNUmakefile tbsetup/console_setup tbsetup/spewlogfile \ tbsetup/spewrpmtar tbsetup/gentopofile tbsetup/power_sgmote.pm \ tbsetup/console_reset tbsetup/bwconfig tbsetup/power_rpc27.pm \ diff --git a/db/Experiment.pm.in b/db/Experiment.pm.in index 86cd6efce3d03a81acea9ea0589b803826183c9b..edafb46f5b49d75d0db8dd7b79c60833c5d401f1 100644 --- a/db/Experiment.pm.in +++ b/db/Experiment.pm.in @@ -39,6 +39,7 @@ my $TBOPS = "@TBOPSEMAIL@"; my $PROJROOT = "@PROJROOT_DIR@"; my $EVENTSYS = @EVENTSYS@; my $STAMPS = @STAMPS@; +my $TBBASE = "@TBBASE@"; my $TEVC = "$TB/bin/tevc"; my $DBCONTROL = "$TB/sbin/opsdb_control"; my $RSYNC = "/usr/local/bin/rsync"; @@ -189,6 +190,7 @@ sub pid_idx($) { return field($_[0], 'pid_idx'); } sub gid_idx($) { return field($_[0], 'gid_idx'); } sub eid($) { return field($_[0], 'eid'); } sub idx($) { return field($_[0], 'idx'); } +sub description($) { return field($_[0], 'expt_name'); } sub path($) { return field($_[0], 'path'); } sub state($) { return field($_[0], 'state'); } sub batchstate($) { return field($_[0], 'batchstate'); } @@ -887,6 +889,194 @@ sub Update($$) return Refresh($self); } +# +# Worker class method to change experiment info. +# Assumes most argument checking was done elsewhere. +# +sub EditExp($$$$$$) +{ + my ($class, $experiment, $user, $doemail, $argref, $usrerr_ref) = @_; + + my %mods; + my $noreport; + my %updates; + + # + # Converting the batchmode is tricky, but we can let the DB take care + # of it by requiring that the experiment not be locked, and it be in + # the swapped state. If the query fails, we know that the experiment + # was in transition. + # + + if (!exists($argref->{"batchmode"})) { + $argref->{"batchmode"} = 0; + } + if ($experiment->batchmode() != $argref->{"batchmode"}) { + my $success = 0; + + my $batchmode; + if ($argref->{"batchmode"} ne "1") { + $batchmode = 0; + $argref->{"batchmode"} = 0; + } + else { + $batchmode = 1; + $argref->{"batchmode"} = 1; + } + + if ($experiment->SetBatchMode($batchmode) != 0) { + $$usrerr_ref = "Batch Mode: Experiment is running or in transition; ". + "try again later"; + return undef; + } + $mods{"batchmode"} = $batchmode; + } + + # + # Now update the rest of the information in the DB. + # + + # Name change for experiment description. + if (exists($argref->{"description"})) { + $updates{"expt_name"} = ($mods{"description"} = $argref->{"description"}); + } + + # Note that timeouts are in hours in the UI, but in minutes in the DB. + if (exists($argref->{"idleswap_timeout"})) { + $updates{"idleswap_timeout"} = 60 * + ($mods{"idleswap_timeout"} = $argref->{"idleswap_timeout"}); + } + if (exists($argref->{"autoswap_timeout"})) { + $updates{"autoswap_timeout"} = 60 * + ($mods{"autoswap_timeout"} = $argref->{"autoswap_timeout"}); + } + + foreach my $col ("idle_ignore", "swappable", "noswap_reason", + "idleswap", "noidleswap_reason", "autoswap", "savedisk", + "cpu_usage", "mem_usage", "linktest_level") { + # Copy args we want so that others can't get through. + if (exists($argref->{$col})) { + $updates{$col} = $mods{$col} = $argref->{$col}; + } + } + + # Save state before change for the email message below. + my $olds = ($experiment->swappable() ? "Yes" : "No"); + my $oldsr= $experiment->noswap_reason(); + my $oldi = ($experiment->idleswap() ? "Yes" : "No"); + my $oldit= $experiment->idleswap_timeout() / 60.0; + my $oldir= $experiment->noidleswap_reason(); + my $olda = ($experiment->autoswap() ? "Yes" : "No"); + my $oldat= $experiment->autoswap_timeout() / 60.0; + + if (keys %updates) { + if ($experiment->Update(\%updates)) { + return undef; + } + } + + my $creator = $experiment->creator(); + my $swapper = $experiment->swapper(); + my $uid = $user->uid(); + my $pid = $experiment->pid(); + my $eid = $experiment->eid(); + + if (!keys %mods) { + if (!$noreport) { + # Warn the user that the submit button was pressed with no effect. + $$usrerr_ref = "Submit: Nothing changed"; + return undef; + } + } + # Do not send this email if the user is an administrator + # (adminmode does not matter), and is changing an expt he created + # or swapped in. Pointless email. + elsif ( $doemail && + ! ($user->admin() && + ($uid eq $creator || $uid eq $swapper)) ) { + + # Send an audit e-mail reporting what is being changed. + my $target_creator = $experiment->GetCreator(); + my $target_swapper = $experiment->GetSwapper(); + + my $user_name = $user->name(); + my $user_email = $user->email(); + my $cname = $target_creator->name(); + my $cemail = $target_creator->email(); + my $sname = $target_swapper->name(); + my $semail = $target_swapper->email(); + + my $s = ($experiment->swappable() ? "Yes" : "No"); + my $sr = $experiment->noswap_reason(); + my $i = ($experiment->idleswap() ? "Yes" : "No"); + my $it = $experiment->idleswap_timeout() / 60.0; + my $ir = $experiment->noidleswap_reason(); + my $a = ($experiment->autoswap() ? "Yes" : "No"); + my $at = $experiment->autoswap_timeout() / 60.0; + + my $msg = "\n". + "The swap settings for $pid/$eid have changed\n". + "\nThe old settings were:\n". + "Swappable:\t$olds\t($oldsr)\n". + "Idleswap:\t$oldi\t(after $oldit hrs)\t($oldir)\n". + "MaxDuration:\t$olda\t(after $oldat hrs)\n". + "\nThe new settings are:\n". + "Swappable:\t$s\t($sr)\n". + "Idleswap:\t$i\t(after $it hrs)\t($ir)\n". + "MaxDuration:\t$a\t(after $at hrs)\n". + "\nCreator:\t$creator ($cname <$cemail>)\n". + "Swapper:\t$swapper ($sname <$semail>)\n". + "\nDifferences were:\n"; + my @report = + ("Description:description", "Idle Ignore:idle_ignore", + "Swappable:swappable", "Noswap Reason:noswap_reason", + "Idleswap:idleswap", "Idleswap Timeout:idleswap_timeout", + "Noidleswap Reason:noidleswap_reason", "Autoswap:autoswap", + "Autoswap timeout:autoswap_timeout", "Savedisk:savedisk", + "Cpu Usage:cpu_usage", "Mem Usage:mem_usage", + "Batch Mode:batchmode", "Linktest Level:linktest_level"); + foreach my $line (@report) { + my ($label, $field) = split /:/, $line; + if (exists($mods{$field})) { + $msg .= sprintf "%-20s%s\n", $label .":", $mods{$field}; + } + } + $msg .= "\n". + "\nIf it is necessary to change these settings, ". + "please reply to this message \nto notify the user, ". + "then change the settings here:\n\n". + "$TBBASE/showexp.php3?pid=$pid&eid=$eid\n\n". + "Thanks,\nTestbed WWW\n"; + + SENDMAIL("$user_name <$user_email>", + "$pid/$eid swap settings changed", + $msg, TBMAIL_OPS(), sprintf("Bcc: %s\nErrors-To:%s", + TBMAIL_AUDIT(), TBMAIL_WWW())); + } + return 1; +} + +sub SetBatchMode($$) { + my ($self, $mode) = @_; + + my $reqstate = EXPTSTATE_SWAPPED(); + my $idx = $self->idx(); + $mode = ($mode ? 1 : 0); + + DBQueryFatal("lock tables experiments write"); + + my $query_result = + DBQueryFatal("update experiments set ". + " batchmode=$mode ". + "where idx='$idx' and ". + " expt_locked is NULL and state='$reqstate'"); + + my $success = $query_result->numrows; # XXX Was DBAffectedRows(). + dbqueryfatal("unlock tables"); + + return ($success ? 0 : -1); +} + # # Stringify for output. # diff --git a/sql/database-fill.sql b/sql/database-fill.sql index c9137e68d09a9202d11ffd7b96f623ec7908662a..ad9dedf8730a364fa9617c220ee1ce832fa2fe51 100644 --- a/sql/database-fill.sql +++ b/sql/database-fill.sql @@ -562,8 +562,6 @@ REPLACE INTO table_regex VALUES ('experiments','uselatestwadata','int','redirect REPLACE INTO table_regex VALUES ('experiments','wa_delay_solverweight','float','redirect','default:float',0,1024,NULL); REPLACE INTO table_regex VALUES ('experiments','wa_bw_solverweight','float','redirect','default:float',0,1024,NULL); REPLACE INTO table_regex VALUES ('experiments','wa_plr_solverweight','float','redirect','default:float',0,1024,NULL); -REPLACE INTO table_regex VALUES ('experiments','cpu_usage','int','redirect','default:tinyint',0,5,NULL); -REPLACE INTO table_regex VALUES ('experiments','mem_usage','int','redirect','default:tinyint',0,5,NULL); REPLACE INTO table_regex VALUES ('experiments','sync_server','text','redirect','virt_nodes:vname',0,0,NULL); REPLACE INTO table_regex VALUES ('groups','project','text','redirect','projects:pid',0,0,NULL); @@ -718,6 +716,7 @@ REPLACE INTO table_regex VALUES ('projects','num_members','int','redirect','defa REPLACE INTO table_regex VALUES ('projects','num_pcs','int','redirect','default:int',0,2048,NULL); REPLACE INTO table_regex VALUES ('projects','num_pcplab','int','redirect','default:int',0,2048,NULL); REPLACE INTO table_regex VALUES ('projects','num_ron','int','redirect','default:int',0,1024,NULL); + REPLACE INTO table_regex VALUES ('experiments','encap_style','text','regex','^(alias|veth|veth-ne|vlan|default)$',0,0,NULL); REPLACE INTO table_regex VALUES ('experiments','veth_encapsulate','int','redirect','default:boolean',0,0,NULL); REPLACE INTO table_regex VALUES ('experiments','allowfixnode','int','redirect','default:boolean',0,0,NULL); @@ -726,14 +725,25 @@ REPLACE INTO table_regex VALUES ('experiments','delay_osname','text','redirect', REPLACE INTO table_regex VALUES ('experiments','use_ipassign','int','redirect','default:boolean',0,0,NULL); REPLACE INTO table_regex VALUES ('experiments','ipassign_args','text','regex','^[\\w\\s-]*$',0,255,NULL); REPLACE INTO table_regex VALUES ('experiments','expt_name','text','redirect','default:tinytext',1,255,NULL); +REPLACE INTO table_regex VALUES ('experiments','dpdb','int','redirect','default:tinyint',0,1,NULL); + +REPLACE INTO table_regex VALUES ('experiments','description','text','regex','^[\\040-\\176\\012\\015\\011]*$',1,256,NULL); +REPLACE INTO table_regex VALUES ('experiments','idle_ignore','int','redirect','default:boolean',0,0,NULL); +REPLACE INTO table_regex VALUES ('experiments','swappable','int','redirect','default:boolean',0,0,NULL); REPLACE INTO table_regex VALUES ('experiments','noswap_reason','text','redirect','default:tinytext',1,255,NULL); -REPLACE INTO table_regex VALUES ('experiments','noidleswap_reason','text','redirect','default:tinytext',1,255,NULL); +REPLACE INTO table_regex VALUES ('experiments','idleswap','int','redirect','default:boolean',0,0,NULL); REPLACE INTO table_regex VALUES ('experiments','idleswap_timeout','int','redirect','default:int',1,2147483647,NULL); +REPLACE INTO table_regex VALUES ('experiments','noidleswap_reason','text','redirect','default:tinytext',1,255,NULL); +REPLACE INTO table_regex VALUES ('experiments','autoswap','int','redirect','default:boolean',0,0,NULL); REPLACE INTO table_regex VALUES ('experiments','autoswap_timeout','int','redirect','default:int',1,2147483647,NULL); +REPLACE INTO table_regex VALUES ('experiments','savedisk','int','redirect','default:boolean',0,0,NULL); +REPLACE INTO table_regex VALUES ('experiments','cpu_usage','int','redirect','default:tinyint',0,5,NULL); +REPLACE INTO table_regex VALUES ('experiments','mem_usage','int','redirect','default:tinyint',0,5,NULL); +REPLACE INTO table_regex VALUES ('experiments','batchmode','int','redirect','default:boolean',0,0,NULL); +REPLACE INTO table_regex VALUES ('experiments','linktest_level','int','redirect','default:tinyint',0,4,NULL); + REPLACE INTO table_regex VALUES ('virt_lans','protocol','text','redirect','default:tinytext',0,0,NULL); REPLACE INTO table_regex VALUES ('virt_lans','is_accesspoint','int','redirect','default:boolean',0,0,NULL); -REPLACE INTO table_regex VALUES ('experiments','linktest_level','int','redirect','default:tinyint',0,4,NULL); -REPLACE INTO table_regex VALUES ('experiments','dpdb','int','redirect','default:tinyint',0,1,NULL); REPLACE INTO table_regex VALUES ('virt_lan_settings','pid','text','redirect','projects:pid',0,0,NULL); REPLACE INTO table_regex VALUES ('virt_lan_settings','eid','text','redirect','experiments:eid',0,0,NULL); REPLACE INTO table_regex VALUES ('virt_lan_settings','vname','text','redirect','virt_lans:vname',0,0,NULL); diff --git a/www/editexp.php3 b/www/editexp.php3 index 4d316ce28cb910783ab1fd9f0c662489882d2daf..80be71fe846088fabc896d77ac8a1fad48154c9c 100644 --- a/www/editexp.php3 +++ b/www/editexp.php3 @@ -41,7 +41,7 @@ if (! $experiment->AccessCheck($this_user, $TB_EXPT_MODIFY)) { # function SPITFORM($experiment, $formfields, $errors) { - global $TBDOCBASE, $linktest_levels, $EXPOSELINKTEST; + global $isadmin, $TBDOCBASE, $linktest_levels, $EXPOSELINKTEST; # # Standard Testbed Header @@ -99,7 +99,7 @@ function SPITFORM($experiment, $formfields, $errors) </td> <td> <table cellpadding=0 cellspacing=0 border=0>\n"; - if (ISADMIN()) { + if ($isadmin) { # # Batch Experiment? # @@ -309,16 +309,9 @@ function SPITFORM($experiment, $formfields, $errors) </table>\n"; } -# -# We might need these later for email. -# -$creator = $experiment->creator(); -$swapper = $experiment->swapper(); -$doemail = 0; - # # Construct a defaults array based on current DB info. Used for the initial -# form, and to determine if any changes were made and to send email. +# form, and to determine if any changes were made. # $defaults = array(); $defaults["description"] = $experiment->description(); @@ -347,7 +340,7 @@ if (!$defaults["idleswap"]) { } # -# On first load, display initial values. +# On first load, display initial values and exit. # if (! isset($submit)) { SPITFORM($experiment, $defaults, 0); @@ -356,311 +349,106 @@ if (! isset($submit)) { } # -# Otherwise, must validate and redisplay if errors. Build up a DB insert -# string as we go. +# Build up argument array to pass along. # -$errors = array(); -$inserts = array(); - -# -# Description -# -if (!isset($formfields["description"]) || - strcmp($formfields["description"], "") == 0) { - $errors["Description"] = "Missing Field"; -} -else { - $inserts[] = "expt_name='" . addslashes($formfields["description"]) . "'"; -} +$args = array(); -# -# Swappable/Idle Ignore -# Any of these which are not "1" become "0". -# -# Idle Ignore -# -if (!isset($formfields["idle_ignore"]) || - strcmp($formfields["idle_ignore"], "1")) { - $formfields["idle_ignore"] = 0; - $inserts[] = "idle_ignore=0"; -} -else { - $formfields["idle_ignore"] = 1; - $inserts[] = "idle_ignore=1"; +# Skip passing ones that are not changing from the default (DB state.) +if (isset($formfields["description"]) && $formfields["description"] != "" && + ($formfields["description"] != $experiment->description())) { + $args["description"] = $formfields["description"]; } -# -# Swappable -# -if (ISADMIN() && (!isset($formfields["swappable"]) || - strcmp($formfields["swappable"], "1"))) { - $formfields["swappable"] = 0; - - if (!isset($formfields["noswap_reason"]) || - !strcmp($formfields["noswap_reason"], "")) { - - if (!ISADMIN()) { - $errors["Swappable"] = "No justification provided"; - } - else { - $formfields["noswap_reason"] = "ADMIN"; - } +if ($isadmin) { # A couple of admin-only options. + # Filter booleans from checkboxes to 0 or 1. + $formfields["idle_ignore"] = + (!isset($formfields["idle_ignore"]) || + strcmp($formfields["idle_ignore"], "1")) ? 0 : 1; + if ($formfields["idle_ignore"] != $experiment->idle_ignore()) { + $args["idle_ignore"] = $formfields["idle_ignore"]; } - if ($defaults["swappable"]) - $doemail = 1; - $inserts[] = "swappable=0"; - $inserts[] = "noswap_reason='" . - addslashes($formfields["noswap_reason"]) . "'"; -} -else { - $inserts[] = "swappable=1"; -} - -# -# IdleSwap -# -if (!isset($formfields["idleswap"]) || - strcmp($formfields["idleswap"], "1")) { - $formfields["idleswap"] = 0; - - if (!isset($formfields["noidleswap_reason"]) || - !strcmp($formfields["noidleswap_reason"], "")) { - if (! ISADMIN()) { - $errors["IdleSwap"] = "No justification provided"; - } - else { - $formfields["noidleswap_reason"] = "ADMIN"; - } + $formfields["swappable"] = (!isset($formfields["swappable"]) || + strcmp($formfields["swappable"], "1")) ? 0 : 1; + if ($formfields["swappable"] != $experiment->swappable()) { + $args["swappable"] = $formfields["swappable"]; + } + if (isset($formfields["noswap_reason"]) && + $formfields["noswap_reason"] != "" && + ($formfields["noswap_reason"] != $experiment->noswap_reason())) { + $args["noswap_reason"] = $formfields["noswap_reason"]; } - if ($defaults["idleswap"]) - $doemail = 1; - $inserts[] = "idleswap=0"; - $inserts[] = "idleswap_timeout=0"; - $inserts[] = "noidleswap_reason='" . - addslashes($formfields["noidleswap_reason"]) . "'"; -} -elseif (!isset($formfields["idleswap_timeout"]) || - !preg_match("/^[\d]+$/", $formfields["idleswap_timeout"]) || - ($formfields["idleswap_timeout"] + 0) <= 0 || - ( (($formfields["idleswap_timeout"] + 0) > $idleswaptimeout) && - !ISADMIN()) ) { - $errors["Idleswap"] = "Invalid time provided (0 < X <= $idleswaptimeout)"; -} -else { - $inserts[] = "idleswap=1"; - $inserts[] = "idleswap_timeout=" . 60 * $formfields["idleswap_timeout"]; - $inserts[] = "noidleswap_reason='" . - addslashes($formfields["noidleswap_reason"]) . "'"; } -# -# AutoSwap -# -if (!isset($formfields["autoswap"]) || - strcmp($formfields["autoswap"], "1")) { - $formfields["autoswap"] = 0; - $inserts[] = "autoswap=0"; - $inserts[] = "autoswap_timeout=0"; -} -elseif (!isset($formfields["autoswap_timeout"]) || - !preg_match("/^[\d]+$/", $formfields["autoswap_timeout"]) || - ($formfields["autoswap_timeout"] + 0) == 0) { - $errors["Max Duration"] = "Invalid time provided"; +$formfields["idleswap"] = (!isset($formfields["idleswap"]) || + strcmp($formfields["idleswap"], "1")) ? 0 : 1; +if ($formfields["idleswap"] != $experiment->idleswap()) { + $args["idleswap"] = $formfields["idleswap"]; } -else { - $inserts[] = "autoswap=1"; - $inserts[] = "autoswap_timeout=" . 60 * $formfields["autoswap_timeout"]; +# Note that timeouts are in hours in the UI, but in minutes in the DB. +if (isset($formfields["idleswap_timeout"]) && + $formfields["idleswap_timeout"] != "" && + ($formfields["idleswap_timeout"] != + $experiment->idleswap_timeout() / 60.0)) { + $args["idleswap_timeout"] = $formfields["idleswap_timeout"]; } - -# -# Swapout disk state saving -# -if (!isset($formfields["savedisk"]) || - strcmp($formfields["savedisk"], "1")) { - $formfields["savedisk"] = 0; - $inserts[] = "savedisk=0"; +if (isset($formfields["noidleswap_reason"]) && + $formfields["noidleswap_reason"] != "" && + ($formfields["noidleswap_reason"] != $experiment->noidleswap_reason())) { + $args["noidleswap_reason"] = $formfields["noidleswap_reason"]; } -else { - $formfields["savedisk"] = 1; - $inserts[] = "savedisk=1"; -} - -# -# CPU Usage -# -if (isset($formfields["cpu_usage"]) && - strcmp($formfields["cpu_usage"], "")) { - if (!preg_match("/^[\d]+$/", $formfields["cpu_usage"])) { - $errors["CPU Usage"] = "Invalid character"; - } - elseif (($formfields["cpu_usage"] + 0) < 0 || - ($formfields["cpu_usage"] + 0) > 5) { - $errors["CPU Usage"] = "Invalid (0 <= X <= 5)"; - } - else { - $inserts[] = "cpu_usage=" . $formfields["cpu_usage"]; - } +$formfields["autoswap"] = (!isset($formfields["autoswap"]) || + strcmp($formfields["autoswap"], "1")) ? 0 : 1; +if ($formfields["autoswap"] != $experiment->autoswap()) { + $args["autoswap"] = $formfields["autoswap"]; } -else { - $inserts[] = "cpu_usage=0"; +if (isset($formfields["autoswap_timeout"]) && + $formfields["autoswap_timeout"] != "" && + ($formfields["autoswap_timeout"] != + $experiment->autoswap_timeout() / 60.0)) { + $args["autoswap_timeout"] = $formfields["autoswap_timeout"]; } -# -# Mem Usage -# -if (isset($formfields["mem_usage"]) && - strcmp($formfields["mem_usage"], "")) { - - if (!preg_match("/^[\d]+$/", $formfields["mem_usage"])) { - $errors["Mem Usage"] = "Invalid character"; - } - elseif (($formfields["mem_usage"] + 0) < 0 || - ($formfields["mem_usage"] + 0) > 5) { - $errors["Mem Usage"] = "Invalid (0 <= X <= 5)"; - } - else { - $inserts[] = "mem_usage=" . $formfields["mem_usage"]; - } +$formfields["savedisk"] = (!isset($formfields["savedisk"]) || + strcmp($formfields["savedisk"], "1")) ? 0 : 1; +if ($formfields["savedisk"] != $experiment->savedisk()) { + $args["savedisk"] = $formfields["savedisk"]; } -else { - $inserts[] = "mem_usage=0"; -} - -# -# Linktest level -# -if (isset($formfields["linktest_level"]) && - strcmp($formfields["linktest_level"], "")) { - if (!preg_match("/^[\d]+$/", $formfields["linktest_level"])) { - $errors["Linktest Level"] = "Invalid character"; - } - elseif (($formfields["linktest_level"] + 0) < 0 || - ($formfields["linktest_level"] + 0) > 4) { - $errors["Linktest Level"] = "Invalid (0 <= X <= 4)"; - } - else { - $inserts[] = "linktest_level=" . $formfields["linktest_level"]; - } -} -else { - $inserts[] = "linktest_level=0"; +if (isset($formfields["cpu_usage"]) && $formfields["cpu_usage"] != "" && + ($formfields["cpu_usage"] != $experiment->cpu_usage())) { + $args["cpu_usage"] = $formfields["cpu_usage"]; } -# -# Spit any errors before dealing with batchmode, which changes the DB. -# -if (count($errors)) { - SPITFORM($experiment, $formfields, $errors); - PAGEFOOTER(); - return; +if (isset($formfields["mem_usage"]) && $formfields["mem_usage"] != "" && + ($formfields["mem_usage"] != $experiment->mem_usage())) { + $args["mem_usage"] = $formfields["mem_usage"]; } -# -# Converting the batchmode is tricky, but we can let the DB take care -# of it by requiring that the experiment not be locked, and it be in -# the swapped state. If the query fails, we know that the experiment -# was in transition. -# -if (!isset($formfields["batchmode"])) { - $formfields["batchmode"] = 0; +$formfields["batchmode"] = (!isset($formfields["batchmode"]) || + strcmp($formfields["batchmode"], "1")) ? 0 : 1; +if ($formfields["batchmode"] != $experiment->batchmode()) { + $args["batchmode"] = $formfields["batchmode"]; } -if ($defaults["batchmode"] != $formfields["batchmode"]) { - $success = 0; - - if (strcmp($formfields["batchmode"], "1")) { - $batchmode = 0; - $formfields["batchmode"] = 0; - } - else { - $batchmode = 1; - $formfields["batchmode"] = 1; - } - if ($experiment->SetBatchMode($batchmode) != 0) { - $errors["Batch Mode"] = "Experiment is running or in transition; ". - "try again later"; - SPITFORM($experiment, $formfields, $errors); - PAGEFOOTER(); - return; - } +# Select defaults to "none" if not set. +if (isset($formfields["linktest_level"]) && + $formfields["linktest_level"] != "none" && + $formfields["linktest_level"] != "" && + ($formfields["linktest_level"] != $experiment->linktest_level())) { + $args["linktest_level"] = $formfields["linktest_level"]; } -# -# Otherwise, do the other inserts. -# -if ($experiment->UpdateOldStyle($inserts) != 0) { - $errors["Updating"] = "Error updating experiment; please try again later"; +$errors = array(); +if (! ($result = Experiment::EditExp($experiment, $args, $errors))) { + # Always respit the form so that the form fields are not lost. + # I just hate it when that happens so lets not be guilty of it ourselves. SPITFORM($experiment, $formfields, $errors); PAGEFOOTER(); return; } -# -# Do not send this email if the user is an administrator -# (adminmode does not matter), and is changing an expt he created -# or swapped in. Pointless email. -if ($doemail && - ! (ISADMINISTRATOR() && - (!strcmp($uid, $creator) || !strcmp($uid, $swapper)))) { - - $target_creator = $experiment->GetCreator(); - $target_swapper = $experiment->GetSwapper(); - # Needed below. - $swappable = $experiment->swappable(); - $idleswap = $experiment->idleswap(); - $noswap_reason = $experiment->noswap_reason(); - $idleswap_timeout = $experiment->idleswap_timeout() / 60.0; - $noidleswap_reason = $experiment->noidleswap_reason(); - $autoswap = $experiment->autoswap(); - $autoswap_timeout = $experiment->autoswap_timeout() / 60.0; - - $user_name = $this_user->name(); - $user_email = $this_user->email(); - $cname = $target_creator->name(); - $cemail = $target_creator->email(); - $sname = $target_swapper->name(); - $semail = $target_swapper->email(); - - $olds = ($defaults["swappable"] ? "Yes" : "No"); - $oldsr= $defaults["noswap_reason"]; - $oldi = ($defaults["idleswap"] ? "Yes" : "No"); - $oldit= $defaults["idleswap_timeout"]; - $oldir= $defaults["noidleswap_reason"]; - $olda = ($defaults["autoswap"] ? "Yes" : "No"); - $oldat= $defaults["autoswap_timeout"]; - - $s = ($formfields["swappable"] ? "Yes" : "No"); - $sr = $formfields["noswap_reason"]; - $i = ($formfields["idleswap"] ? "Yes" : "No"); - $it = $formfields["idleswap_timeout"]; - $ir = $formfields["noidleswap_reason"]; - $a = ($formfields["autoswap"] ? "Yes" : "No"); - $at = $formfields["autoswap_timeout"]; - - TBMAIL($TBMAIL_OPS,"$pid/$eid swap settings changed", - "\nThe swap settings for $pid/$eid have changed.\n". - "\nThe old settings were:\n". - "Swappable:\t$olds\t($oldsr)\n". - "Idleswap:\t$oldi\t(after $oldit hrs)\t($oldir)\n". - "MaxDuration:\t$olda\t(after $oldat hrs)\n". - "\nThe new settings are:\n". - "Swappable:\t$s\t($sr)\n". - "Idleswap:\t$i\t(after $it hrs)\t($ir)\n". - "MaxDuration:\t$a\t(after $at hrs)\n". - "\nCreator:\t$creator ($cname <$cemail>)\n". - "Swapper:\t$swapper ($sname <$semail>)\n". - "\nIf it is necessary to change these settings, ". - "please reply to this message \nto notify the user, ". - "then change the settings here:\n\n". - "$TBBASE/showexp.php3?pid=$pid&eid=$eid\n\n". - "Thanks,\nTestbed WWW\n", - "From: $user_name <$user_email>\n". - "Errors-To: $TBMAIL_WWW"); -} - # # Spit out a redirect so that the history does not include a post # in it. The back button skips over the post and to the form. diff --git a/www/experiment_defs.php b/www/experiment_defs.php index 7d8b03f88d454a1a7e0ddfe9c7e67f8bdd4dfee0..337141e406c28d787252285199e5fc7b488e8ba1 100644 --- a/www/experiment_defs.php +++ b/www/experiment_defs.php @@ -135,6 +135,80 @@ class Experiment return 0; } + # + # Class function to change experiment info via XML to a backend script. + # + function EditExp($experiment, $args, &$errors) { + global $suexec_output, $suexec_output_array; + + if (!count($args)) { + $errors[] = "No changes to submit."; + return null; + } + + # + # Generate a temporary file and write in the XML goo. + # + $xmlname = tempnam("/tmp", "editexp"); + if (! $xmlname) { + TBERROR("Could not create temporary filename", 0); + $errors[] = "Transient error; please try again later."; + return null; + } + if (! ($fp = fopen($xmlname, "w"))) { + TBERROR("Could not open temp file $xmlname", 0); + $errors[] = "Transient error; please try again later."; + return null; + } + + # Add these. Maybe caller should do this? + $args["experiment"] = $experiment->idx(); + + fwrite($fp, "<experiment>\n"); + foreach ($args as $name => $value) { + fwrite($fp, "<attribute name=\"$name\">"); + fwrite($fp, " <value>" . htmlspecialchars($value) . "</value>"); + fwrite($fp, "</attribute>\n"); + } + fwrite($fp, "</experiment>\n"); + fclose($fp); + chmod($xmlname, 0666); + + $retval = SUEXEC("nobody", "nobody", "webeditexp $xmlname", + SUEXEC_ACTION_IGNORE); + + if ($retval) { + if ($retval < 0) { + $errors[] = "Transient error; please try again later."; + SUEXECERROR(SUEXEC_ACTION_CONTINUE); + } + else { + # unlink($xmlname); + if (count($suexec_output_array)) { + for ($i = 0; $i < count($suexec_output_array); $i++) { + $line = $suexec_output_array[$i]; + if (preg_match("/^([-\w]+):\s*(.*)$/", + $line, $matches)) { + $errors[$matches[1]] = $matches[2]; + } + else + $errors[] = $line; + } + } + else + $errors[] = "Transient error; please try again later."; + } + return null; + } + + # There are no return value(s) to parse at the end of the output. + + # Unlink this here, so that the file is left behind in case of error. + # We can then create the experiment by hand from the xmlfile, if desired. + unlink($xmlname); + return true; + } + # # Update fields. Array of "foo=bar" ... #