Commit 93963570 authored by Leigh Stoller's avatar Leigh Stoller

Checkpoint Parameterized Profile support before changing the web

UI to not use modals.
parent 6351b521
......@@ -326,6 +326,9 @@ sub Create($$$$$$)
}
if (exists($argref->{'script'}) && $argref->{'script'} ne "") {
$vquery .= ",script=" . DBQuoteSpecial($argref->{'script'});
if (exists($argref->{'paramdefs'}) && $argref->{'paramdefs'} ne "") {
$vquery .= ",paramdefs=" . DBQuoteSpecial($argref->{'paramdefs'});
}
}
# Back to the main table.
......@@ -391,10 +394,11 @@ sub NewVersion($$)
if (! DBQueryWarn("insert into apt_profile_versions ".
" (name,profileid,version,pid,pid_idx, ".
" creator,creator_idx,created,uuid, ".
" parent_profileid,parent_version,rspec) ".
" parent_profileid,parent_version,rspec, ".
" script,paramdefs) ".
"select name,profileid,'$newvers',pid,pid_idx, ".
" '$uid','$uid_idx',now(),uuid(),parent_profileid, ".
" '$version',rspec ".
" '$version',rspec,script,paramdefs ".
"from apt_profile_versions as v ".
"where v.profileid='$profileid' and ".
" v.version='$version'"));
......
......@@ -31,17 +31,17 @@ include $(OBJDIR)/Makeconf
SUBDIRS =
BIN_SCRIPTS = manage_profile manage_instance manage_dataset \
create_instance rungenilib
create_instance rungenilib rungenilibN genilibparams
SBIN_SCRIPTS =
LIB_SCRIPTS = APT_Profile.pm APT_Instance.pm APT_Dataset.pm APT_Geni.pm
WEB_BIN_SCRIPTS = webmanage_profile webmanage_instance webmanage_dataset \
webcreate_instance webrungenilib
WEB_SBIN_SCRIPTS=
LIBEXEC_SCRIPTS = $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS)
USERLIBEXEC = rungenilib.proxy
USERLIBEXEC = rungenilib.proxy rungenilibN.proxy
# These scripts installed setuid, with sudo.
SETUID_BIN_SCRIPTS = rungenilib
SETUID_BIN_SCRIPTS = rungenilib rungenilibN genilibparams
SETUID_SBIN_SCRIPTS =
SETUID_SUEXEC_SCRIPTS=
......
......@@ -225,6 +225,9 @@ if (!defined($cm_authority)) {
my $cmurl = $cm_authority->url();
#$cmurl =~ s/protogeni/protogeni\/stoller/;
my $iscloudlab =
($CMURN eq "urn:publicid:IDN+utah.cloudlab.us+authority+cm" ? 1 : 0);
#
# Must wrap the parser in eval since it exits on error.
#
......@@ -249,7 +252,7 @@ foreach my $key ("username", "email", "profile") {
# Gather up args and sanity check.
#
my ($value, $user_urn, $user_uid, $user_hrn, $user_email,
$sshkey, $profile, $version);
$sshkey, $profile, $profileid, $version, $rspecstr, $errmsg);
#
# Username and email has to be acceptable to Emulab user system.
......@@ -271,48 +274,52 @@ if (! TBcheck_dbslot($value, "users", "usr_email",
$user_email = $value;
#
# Not many choices; see if it exists.
# Profile.
#
$value = $xmlparse->{'attribute'}->{"profile"}->{'value'};
# This is a safe lookup.
my $profile_object = APT_Profile->Lookup($value);
if (!defined($profile_object)) {
$value = $xmlparse->{'attribute'}->{"profile"}->{'value'};
$profile = APT_Profile->Lookup($value);
if (!defined($profile)) {
fatal("No such profile: $value");
}
#
# Temp hack; replace URNs with URLs.
#
if (1 && $profile_object->ConvertDiskImages()) {
fatal("Not able to convert disk_image URNs to URLs.");
}
my $rspecstr = $profile_object->CheckFirewall(!$localuser);
$profile = $profile_object->profileid();
$version = $profile_object->version();
$profileid = $profile->profileid();
$version = $profile->version();
#
# Look for datasets; need to verify that the datasets being referenced
# still exist and are still permissible to use, and we have to generate
# credentials for those datasets (if not a global dataset). The tricky
# aspect is that while a dataset and a profile have project permissions,
# the experiment has no project association, so if the profile/dataset
# perms are okay, then we send over a credential that tells the CM to
# allow this experiment to use that dataset in that project.
# Optional rspec, as for a Parameterized Profile.
#
my $errmsg = "Bad dataset";
if (APT_Profile::CheckDatasets($rspecstr, $profile_object->pid(), \$errmsg)) {
UserError($errmsg);
if (exists($xmlparse->{'attribute'}->{"rspec"})) {
$rspecstr = $xmlparse->{'attribute'}->{"rspec"}->{'value'};
}
#
# A temporary hack to make sure that the user does not try to run
# an x386 image on the Cloudlab cluster (ARMs). This will eventually
# get replaced with Jon's constraint checking code.
#
my $iscloudlab =
($CMURN eq "urn:publicid:IDN+utah.cloudlab.us+authority+cm" ? 1 : 0);
if ($profile_object->CheckNodeConstraints($iscloudlab, \$errmsg)) {
UserError($errmsg);
else {
$rspecstr = $profile->CheckFirewall(!$localuser);
#
# Temp hack; replace URNs with URLs.
#
if (1 && $profile->ConvertDiskImages()) {
fatal("Not able to convert disk_image URNs to URLs.");
}
#
# Look for datasets; need to verify that the datasets being referenced
# still exist and are still permissible to use, and we have to generate
# credentials for those datasets (if not a global dataset). The tricky
# aspect is that while a dataset and a profile have project permissions,
# the experiment has no project association, so if the profile/dataset
# perms are okay, then we send over a credential that tells the CM to
# allow this experiment to use that dataset in that project.
#
$errmsg = "Bad dataset";
if (APT_Profile::CheckDatasets($rspecstr, $profile->pid(), \$errmsg)) {
UserError($errmsg);
}
#
# A temporary hack to make sure that the user does not try to run
# an x386 image on the Cloudlab cluster (ARMs). This will eventually
# get replaced with Jon's constraint checking code.
#
if ($profile->CheckNodeConstraints($iscloudlab, \$errmsg)) {
UserError($errmsg);
}
}
#
......@@ -450,19 +457,25 @@ if ($localuser) {
fatal("Could not update ssh keys for nonlocal user");
}
}
elsif (!$emulab_user->isEmulab() && defined($sshkey) &&
!$emulab_user->LookupSSHKey($sshkey)) {
elsif (defined($sshkey) && !$emulab_user->LookupSSHKey($sshkey)) {
#
# A local user created via the APT/Cloud interface. Rather then
# edit keys via the old web UI, they can change their one key
# by putting a new one in the web form. If the gave us a new one,
# insert it after deleting the old one.
# A local user. We mark keys that come through this path
# with the isaptkey flag (-a to addpubkey) so that we know
# which key in the DB it is. The reason for this is that the
# user might be a classic emulab user, but is now using the
# APT/Cloud UI. Class Emulab allows multiple keys, but the
# APT/Cloud UI only allows one (which is replaced in the DB
# if it changes). We do not want to expose the Emulab ssh key
# edit page, too messy. So always operate on the one apt key
# for all users.
#
$emulab_user->DeleteSSHKeys();
if (!$emulab_user->isEmulab()) {
$emulab_user->DeleteSSHKeys();
}
my ($fh, $keyfile) = tempfile(UNLINK => 0);
print $fh $sshkey;
if (system("$ADDPUBKEY -u $user_uid -f $keyfile")) {
if (system("$ADDPUBKEY -a -u $user_uid -f $keyfile")) {
fatal("Could not add new ssh pubkey");
}
close($fh);
......@@ -501,8 +514,9 @@ if ($geniuser->GetKeyBundle(\@sshkeys, 1) < 0 || !@sshkeys) {
# Generate the extra credentials that tells the backend this experiment
# can access the datasets.
my @dataset_credentials = ();
if (CreateDatasetCreds($rspecstr,
$profile_object->pid(), $geniuser,
if (defined($profile) &&
CreateDatasetCreds($rspecstr,
$profile->pid(), $geniuser,
\$errmsg, \@dataset_credentials)) {
fatal($errmsg);
}
......@@ -519,7 +533,9 @@ my $SERVER_NAME = (exists($ENV{"SERVER_NAME"}) ? $ENV{"SERVER_NAME"} : "");
print STDERR "\n";
print STDERR "User: $user_urn\n";
print STDERR "Email: $user_email" . (!$localuser ? " (guest)" : "") . "\n";
print STDERR "Profile: " . $profile_object->name() . ":${version}\n";
if (defined($profile)) {
print STDERR "Profile: " . $profile->name() . ":${version}\n";
}
print STDERR "Slice: $slice_urn\n";
print STDERR "Server: $SERVER_NAME\n";
print STDERR "Cluster: $CMURN\n";
......@@ -578,7 +594,7 @@ if (!defined($quickvm_uuid)) {
fatal("Could not generate a new uuid");
}
my $instance = APT_Instance->Create({'uuid' => $quickvm_uuid,
'profile_id' => $profile,
'profile_id' => $profileid,
'profile_version' => $version,
'slice_uuid' => $slice_uuid,
'creator' => $geniuser->uid(),
......
......@@ -25,6 +25,7 @@ use English;
use strict;
use Getopt::Std;
use XML::Simple;
use File::Temp qw(tempfile :POSIX );
use Data::Dumper;
use CGI;
use POSIX ":sys_wait_h";
......@@ -47,6 +48,7 @@ my $update = 0;
my $snap = 0;
my $uuid;
my $rspec;
my $script;
my $profile;
my $instance;
my $webtask;
......@@ -59,6 +61,7 @@ my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $TBLOGS = "@TBLOGSEMAIL@";
my $MANAGEINSTANCE = "$TB/bin/manage_instance";
my $RUNGENILIB = "$TB/bin/rungenilib";
#
# Untaint the path
......@@ -253,6 +256,9 @@ foreach $key (keys(%{ $xmlparse->{'attribute'} })) {
if ($key eq "rspec") {
$rspec = $value;
}
elsif ($key eq "script") {
$script = $value;
}
}
UserError()
if (keys(%errors));
......@@ -278,6 +284,29 @@ if (defined($rspec)) {
}
}
#
# See if this is a Parameterized Profile. Generate and store the form
# data if it is.
#
if (defined($script) && $script ne "") {
my ($fh, $filename) = tempfile();
fatal("Could not open temporary file for script")
if (!defined($fh));
print $fh $script;
my $paramdefs = `$RUNGENILIB -p $filename`;
fatal("$RUNGENILIB failed")
if ($?);
chomp($paramdefs);
if ($paramdefs ne "") {
if ($update) {
$update_args{"paramdefs"} = $paramdefs;
}
else {
$new_args{"paramdefs"} = $paramdefs;
}
}
}
#
# Are we going to snapshot a node in an experiment? If so we
# sanity check to make sure there is just one node.
......@@ -324,11 +353,15 @@ if ($update) {
if (exists($update_args{"rspec"}));
$profile->UpdateVersion({"script" => $update_args{"script"}})
if (exists($update_args{"script"}));
$profile->UpdateVersion({"paramdefs" => $update_args{"paramdefs"}})
if (exists($update_args{"paramdefs"}));
}
delete($update_args{"rspec"})
if (exists($update_args{"rspec"}));
delete($update_args{"script"})
if (exists($update_args{"script"}));
delete($update_args{"paramdefs"})
if (exists($update_args{"paramdefs"}));
}
$profile->UpdateMetaData(\%update_args) == 0 or
fatal("Could not update profile record");
......
......@@ -22,7 +22,7 @@
#
# }}}
#
use strict;
use English;
use Getopt::Std;
use Socket;
......@@ -30,6 +30,7 @@ use File::Basename;
use File::Temp qw(tempfile :POSIX );
use POSIX qw(:signal_h);
use POSIX ":sys_wait_h";
use File::stat;
#
# Parse an ns file. Since the parser runs arbitrary NS file for the user,
......@@ -45,8 +46,10 @@ sub usage()
exit(-1);
}
my $optlist = "do:";
my $debug = 0;
my $optlist = "do:pb:";
my $debug = 0;
my $getparams = 0;
my $paramfile;
my $ofile;
#
......@@ -98,13 +101,26 @@ use User;
# Parse command arguments. Once we return from getopts, all that should
# left are the required arguments.
#
%options = ();
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"d"})) {
$debug = 1;
}
if (defined($options{"p"})) {
$getparams = 1;
}
if (defined($options{"b"})) {
$paramfile = $options{"b"};
# Must taint check!
if ($paramfile =~ /^([-\w\/\.]+)$/) {
$paramfile = $1;
}
else {
die("Bad data in argument: $paramfile.");
}
}
if (defined($options{"o"})) {
$ofile = $options{"o"};
}
......@@ -131,9 +147,6 @@ if (defined($ofile)) {
}
}
my $infile = tmpnam();
my $outfile = tmpnam();
#
# Get DB uid for sending over to ops.
#
......@@ -144,9 +157,21 @@ if (! defined($this_user)) {
# Run as the user for most of this script.
$EUID = $UID;
# Now append the import file to. This part is hokey. Fix later.
system("cat $file >> $infile") == 0
or fatal("Could not combine defs file and the script file!");
#
# If looking for the parameter definition block, then see if there
# are any define_parameter calls. If so, run the script to get the
# JSON block.
#
if ($getparams && system("egrep -q -s 'defineParameter' $file")) {
# No parameters, return empty block.
if (defined($ofile)) {
system("echo '' > $ofile");
}
exit(0);
}
my $infile = tmpnam();
my $outfile = tmpnam();
#
# Touch the output file, to avoid a root owned, 644 file.
......@@ -159,7 +184,32 @@ system("touch $outfile") == 0 or
# result back to a file if not in anonmode. Remember to tack on the
# user ID to flip to, when not in testmode.
#
my $cmdargs = "$TB/libexec/rungenilib.proxy -u " . $this_user->uid();
my $cmdargs = "$TB/libexec/rungenilibN.proxy ";
$cmdargs .= " -u " . $this_user->uid();
$cmdargs .= ($getparams ? " -p " : "");
#
# We want to send over both files via STDIN, so combine them, and pass
# the first file size with the -b option
#
if ($paramfile) {
system("cat $paramfile > $infile") == 0
or fatal("Could not copy $paramfile to $infile");
$cmdargs .= " -b " . stat($infile)->size;
system("cat $file >> $infile") == 0
or fatal("Could not concat $file to $infile");
}
else {
system("cat $file > $infile") == 0
or fatal("Could not copy $file to $infile");
}
$cmdargs = "sshtb -host $CONTROL $cmdargs < $infile";
if ($debug) {
print $cmdargs . "\n";
}
#
# Run parser, redirecting stdout to a file to capture the parser results.
......@@ -167,7 +217,7 @@ my $cmdargs = "$TB/libexec/rungenilib.proxy -u " . $this_user->uid();
# Must flip to real root to run ssh.
#
$EUID = $UID = 0;
open ERR, "sshtb -host $CONTROL $cmdargs < $infile 2>&1 >> $outfile |";
open ERR, "$cmdargs 2>&1 >> $outfile |";
$EUID = $UID = $SAVEUID;
#
......
......@@ -22,17 +22,15 @@
#
# }}}
#
use strict;
use English;
use Getopt::Long;
use Getopt::Std;
use BSD::Resource;
use POSIX qw(:signal_h);
#
# Simply a wrapper for the geni-lib python environment
#
# When run in "impotent" mode, there is no output, just an exit code.
#
sub usage()
{
print STDOUT
......@@ -41,6 +39,10 @@ sub usage()
exit(-1);
}
my $optlist = "u:vpb:";
my $user;
my $getparams= 0;
my $paramsize;
#
# Configure variables
......@@ -54,9 +56,11 @@ my $debug = 0;
# Locals
my $tempdir = "/tmp/genilib-$$";
my $ifile = "$$.py";
my $ofile = "$$.rspec";
my $ofile = "$$.out";
my $pfile = "$$.json";
my $optlist = "u:v";
# Protos
sub fatal($);
#
# Turn off line buffering on output
......@@ -76,15 +80,26 @@ $ENV{"PYTHONPATH"} = $GENILIB;
use lib "@prefix@/lib";
use libtestbed;
my $user;
#
# Parse command arguments. Once we return from getopts, all that should be
# Parse command arguments. Once we return from getopts, all that should
# left are the required arguments.
#
if (! GetOptions("u:s" => \$user)) {
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"d"})) {
$debug = 1;
}
if (defined($options{"p"})) {
$getparams = 1;
}
if (defined($options{"b"})) {
$paramsize = $options{"b"};
}
if (defined($options{"u"})) {
$user = $options{"u"};
}
#
# First option has to be the -u option, the user to run this script as.
......@@ -97,7 +112,7 @@ if (! $TESTMODE) {
" Must be root to run this script!");
}
(undef,undef,$unix_uid) = getpwnam($user) or
my (undef,undef,$unix_uid) = getpwnam($user) or
die("*** $0:\n".
" No such user $user\n");
......@@ -142,26 +157,43 @@ if (! $TESTMODE) {
}
#
# Take our input and write it to the temp file.
# Take our input and write it to the temp file. If we are also getting
# a parameter file in the stream, write that to another file.
#
if (defined($paramsize) && $paramsize) {
my $paramstr;
open(PTMP, ">$pfile") ||
fatal("Couldn't open $pfile\n");
my $cc = read(STDIN, $paramstr, $paramsize);
if (!defined($cc) || $cc != $paramsize) {
fatal("Could not read paramter block from stdin");
}
print PTMP $paramstr;
close(PTMP);
chmod(0644, $pfile);
}
open(TMP, ">$ifile") ||
fatal("Couldn't open $ifile\n");
#
# Prepend our little helper function that spits the rspec to a file.
#
print TMP "\n\n";
print TMP "def printRspec(rspec):\n";
print TMP " f = open('$ofile', 'w+')\n";
print TMP " f.write(str(rspec))\n";
print TMP " f.close()\n";
print TMP " pass\n\n";
while (<STDIN>) {
print TMP $_;
}
close(TMP);
chmod(0644, $ifile);
#
# Need to add things to the environment for the portal module.
#
$ENV{'GENILIB_PORTAL_MODE'} = "Yep";
$ENV{'GENILIB_PORTAL_REQUEST_PATH'} = $ofile;
if ($getparams) {
$ENV{'GENILIB_PORTAL_DUMPPARAMS_PATH'} = $ofile;
}
elsif (defined($paramsize) && $paramsize) {
$ENV{'GENILIB_PORTAL_PARAMS_PATH'} = $pfile;
}
#
# Fork a child process to run the parser in.
#
......@@ -239,4 +271,10 @@ unless($debug) {
}
exit($exit_status);
sub fatal($) {
my ($mesg) = $_[0];
print STDERR "*** $0:\n".
" $mesg\n";
exit(-1);
}
......@@ -55,6 +55,7 @@ function Do_GetProfile()
if ($profile->BestAggregate()) {
$amdefault = $profile->BestAggregate();
}
$ispp = ($profile->isParameterized() ? 1 : 0);
#
# Knowing the UUID means the user can instantiate it,
......@@ -62,8 +63,120 @@ function Do_GetProfile()
#
SPITAJAX_RESPONSE(array('rspec' => $profile->rspec(),
'name' => $profile->name(),
'ispprofile' => $ispp,
'amdefault' => $amdefault));
}
#
# Return parameter form fragment and default values.
#
function Do_GetParameters()
{
global $this_user;
global $ajax_args;
global $suexec_output, $suexec_output_array;
if (!isset($ajax_args["uuid"])) {
SPITAJAX_ERROR(1, "Missing profile uuid");
return;
}
$profile = Profile::Lookup($ajax_args["uuid"]);
if (!$profile) {
SPITAJAX_ERROR(1, "Unknown profile uuid");
return;
}
if (!isset($this_user)) {
if (!$profile->ispublic()) {
SPITAJAX_ERROR(1, "Not enough permission to instantiate profile");
return;
}
}
elseif (!$profile->CanInstantiate($this_user)) {
SPITAJAX_ERROR(1, "Not enough permission to instantiate profile");
return;
}
if (!$profile->isParameterized()) {
SPITAJAX_ERROR(1, "Not a parameterized profile");
return;
}
list ($formfrag, $defaults) = $profile->GenerateFormFragment();
SPITAJAX_RESPONSE(array("formfrag" => htmlentities($formfrag),
"defaults" => $defaults));
}
#
# Instantiate profile (as the user)
#
function Do_Instantiate()
{
global $this_user, $am_array, $DEFAULT_AGGREGATE, $ISCLOUD;
global $ajax_args;
if (!isset($ajax_args["uuid"])) {
SPITAJAX_ERROR(1, "Missing profile uuid");
return;
}
$profile = Profile::Lookup($ajax_args["uuid"]);
if (!$profile) {
SPITAJAX_ERROR(1, "Unknown profile uuid");
return;
}
if (!isset($this_user)) {
if (!$profile->ispublic()) {
SPITAJAX_ERROR(1, "Not enough permission to instantiate profile");
return;
}
}
elseif (!$profile->CanInstantiate($this_user)) {
SPITAJAX_ERROR(1, "Not enough permission to instantiate profile");
return;
}
$args = array();
$args["username"] = $this_user->uid();
$args["email"] = $this_user->email();
$args["profile"] = $profile->uuid();
# Guest users not allowed to provide rspec
if (isset($this_user) && array_key_exists("rspec", $ajax_args)) {
$args["rspec"] = $ajax_args["rspec"];
}
$opts = "";
# Guest users not allowed to choose aggregate.
if (isset($this_user) && ($ISCLOUD || ISADMIN() || STUDLY()) &&
isset($ajax_args["where"])) {
if (array_key_exists($ajax_args["where"], $am_array)) {
$opts = "-a " . $am_array[$ajax_args["where"]];
}
else {
SPITAJAX_ERROR(1, "Invalid Aggregate");
return;
}
}
else {
# Temporary until constraint system in place.
$best = $profile->BestAggregate();
if (!$best) {
$best = $DEFAULT_AGGREGATE;
}
$opts = "-a " . $am_array[$best];
}
#
# Invoke the backend.
#
$errors = array();
list ($instance, $creator) =
Instance::Instantiate($this_user, $opts, $args, $errors);
if (!isset($instance) || is_null($instance)) {
SPITAJAX_ERROR(1, $errors["error"]);
}
#
# Redirect user to status page.
#
$uuid = $instance->uuid();
SPITAJAX_RESPONSE("status.php?uuid=$uuid");
}
# Local Variables:
# mode:php
# End:
......
......@@ -198,7 +198,9 @@ function SPITFORM($formfields, $newuser, $errors)
{
global $TBBASE, $APTMAIL, $ISCLOUD;
global $profile_array, $this_user, $profilename, $profile, $am_array;
$showabout = ($ISCLOUD || !$this_user ? 1 : 0);
$amlist = array();
$showabout = ($ISCLOUD || !$this_user ? 1 : 0);
$registered = (isset($this_user) ? "true" : "false");
# XSS prevention.
while (list ($key, $val) = each ($formfields)) {
......@@ -406,6 +408,7 @@ function SPITFORM($formfields, $newuser, $errors)
if (isset($this_user) && ($ISCLOUD || ISADMIN() || STUDLY())) {
$am_options = "";
while (list($am, $urn) = each($am_array)) {
$amlist[] = $am;
$selected = "";
if ($formfields["where"] == $am) {
$selected = "selected";
......@@ -420,7 +423,11 @@ function SPITFORM($formfields, $newuser, $errors)
}
echo "</fieldset>
<div class='form-group row'>
<div class='col-md-6 col-md-offset-3'>
<div class='col-sm-6 col-sm-offset-3'>
<button class='btn btn-primary btn-block hidden'
id='configurator_button'
type='button'>Configure
</button>
<button class='btn btn-success btn-block' id='instantiate_submit'
type='submit' name='create'>Create!
</button>
......@@ -449,15 +456,30 @@ function SPITFORM($formfields, $newuser, $errors)
}
}
echo "</div>\n";
# This is for a PP rspec.
echo "<textarea name='formfields[pp_rspec]'
id='pp_rspec_textarea'
class='form-control hidden'