Commit e3209e0f authored by Mike Hibler's avatar Mike Hibler

Merge branch 'master' into imagezip-gpt

Pull all the current master changes into imagezip-gpt prior to merging
imagezip changes into master.
parents 924a00b1 a40fb744
This diff is collapsed.
......@@ -48,7 +48,7 @@ sub usage()
print " -r Force a regenerate of initial key for user\n";
exit(-1);
}
my $optlist = "dkniwfu:rX:sRNC:S:I";
my $optlist = "dkniwfu:rX:sRNC:S:Ia";
my $iskey = 0;
my $verify = 0;
my $initmode = 0;
......@@ -59,6 +59,7 @@ my $noemail = 0;
my $remove = 0;
my $nodelete = 0;
my $internal = 0;
my $isaptkey = 0;
my $Comment;
my $xmlfile;
......@@ -156,6 +157,9 @@ if (! getopts($optlist, \%options)) {
if (defined($options{"d"})) {
$debug = 1;
}
if (defined($options{"a"})) {
$isaptkey = 1;
}
if (defined($options{"k"})) {
$iskey = 1;
}
......@@ -428,10 +432,15 @@ sub ParseKey($) {
DBQueryFatal("delete from user_pubkeys ".
"where uid_idx='$user_dbid' and comment=$safe_comment");
}
# Only one APT key allowed
if ($isaptkey) {
DBQueryFatal("delete from user_pubkeys ".
"where uid_idx='$user_dbid' and isaptkey=1");
}
DBQueryFatal("replace into user_pubkeys set ".
" uid='$user_uid', uid_idx='$user_dbid', ".
" internal='$internal', nodelete='$nodelete', ".
" idx=NULL, stamp=now(), ".
" isaptkey='$isaptkey',idx=NULL, stamp=now(), ".
" pubkey=$safe_key, comment=$safe_comment");
#
......
#!/usr/bin/perl -wT
#
# Copyright (c) 2000-2014 University of Utah and the Flux Group.
# Copyright (c) 2000-2015 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -62,7 +62,7 @@ my $TBAUDIT = "@TBAUDITEMAIL@";
my $CONTROL = "@USERNODE@";
my $BOSSNODE = "@BOSSNODE@";
my $WITHZFS = @WITHZFS@;
my $WITHSFS = @SFSSUPPORT@;
my $ZFS_NOEXPORT= @ZFS_NOEXPORT@;
my $WIKISUPPORT = @WIKISUPPORT@;
my $TRACSUPPORT = @TRACSUPPORT@;
my $BUGDBSUPPORT= @BUGDBSUPPORT@;
......@@ -85,10 +85,8 @@ my $USERDEL = "/usr/sbin/pw userdel";
my $USERMOD = "/usr/sbin/pw usermod";
my $CHPASS = "/usr/bin/chpass";
my $ACCOUNTPROXY= "$TB/sbin/accountsetup";
my $SFSKEYGEN = "/usr/local/bin/sfskey gen";
my $GENELISTS = "$TB/sbin/genelists";
my $MKUSERCERT = "$TB/sbin/mkusercert";
my $SFSUPDATE = "$TB/sbin/sfskey_update";
my $PBAG = "$TB/sbin/paperbag";
my $EXPORTSSETUP= "$TB/sbin/exports_setup";
my $ADDWIKIUSER = "$TB/sbin/addwikiuser";
......@@ -118,7 +116,6 @@ my %shellpaths = ("csh" => "/bin/csh", "sh" => "/bin/sh",
"nologin" => "/usr/sbin/nologin");
my $errors = 0;
my $sfsupdate = 0;
my @row;
my $query_result;
......@@ -159,6 +156,7 @@ use libdb;
use libtestbed;
use User;
use Project;
use emutil;
#
# Function prototypes
......@@ -173,7 +171,6 @@ sub ThawUser();
sub VerifyUser();
sub UpdateEmail();
sub CheckDotFiles();
sub GenerateSFSKey();
sub RevokeUser();
sub fatal($);
......@@ -356,19 +353,6 @@ SWITCH: for ($cmd) {
};
}
# Always do this!
CheckDotFiles();
#
# Invoke as real user for auditing (and cause of perl).
#
if ($WITHSFS && $sfsupdate) {
$EUID = $UID;
system($SFSUPDATE) == 0
or fatal("$SFSUPDATE failed!");
$EUID = 0;
}
#
# Now schedule account updates on all the nodes that this person has
# an account on.
......@@ -473,25 +457,31 @@ sub AddUser()
system("$SSH -host $CONTROL $ADDHOOK $user");
}
}
$UID = $SAVEUID;
$EUID = $UID;
if ($WITHZFS) {
if ($ZFS_NOEXPORT) {
#
# Have to force the new directories to be exported.
# See ZFS code in exports_setup
#
$target_user->BumpActivity();
system($EXPORTSSETUP) == 0 or
fatal("$EXPORTSSETUP failed");
}
#
# There is some lag before the automounter can mount the new
# volume. Lets delay until we can access the directory.
# There is some lag before the automounter can mount the new volume.
#
for (my $i = 0; $i < 10; $i++) {
sleep(1)
if (! -r "$HOMEDIR/$user/.cshrc");
}
if (! -r "$HOMEDIR/$user/.cshrc") {
if (emutil::waitForMount("$HOMEDIR/$user") < 0) {
fatal("Could not access new user directory");
}
}
$UID = $SAVEUID;
#
# Do the ssh thing. Invoke as real user for auditing.
# Do the ssh thing.
#
$EUID = $UID;
if ($user ne $PROTOUSER && system("$ADDKEY -i $user")) {
fatal("Could not generate initial ssh key for $user");
}
......@@ -570,10 +560,8 @@ sub AddUser()
}
$EUID = 0;
# SFS key.
if ($CONTROL ne $BOSSNODE) {
GenerateSFSKey();
}
CheckDotFiles();
skipstuff:
return 0;
}
......@@ -650,7 +638,6 @@ sub DelUser()
$EUID = 0;
$sfsupdate = 1;
skipstuff:
return 0;
}
......@@ -777,7 +764,8 @@ sub UpdatePassword()
if ($TRACSUPPORT && $user ne $PROTOUSER && !$webonly);
$EUID = 0;
CheckDotFiles();
return 0;
}
......@@ -848,6 +836,7 @@ sub UpdateUser(;$)
my ($freezeopt) = @_;
my $locshellarg = "";
my $remshellarg = "";
$freezeopt = 0 if (!defined($freezeopt));
#
# Sanity check.
......@@ -855,7 +844,7 @@ sub UpdateUser(;$)
if ($webonly || $isnonlocal) {
return 0;
}
if (!defined($freezeopt) && ($status ne USERSTATUS_ACTIVE)) {
if (!$freezeopt && ($status ne USERSTATUS_ACTIVE)) {
#
# If doing a modification to a frozen user, then just ignore
# it; the modification will happen later when the user is thawed.
......@@ -868,7 +857,7 @@ sub UpdateUser(;$)
}
# Shell is different on local vs control node.
if (defined($freezeopt) && $freezeopt) {
if ($freezeopt) {
$locshellarg = "-s $NOLOGIN";
$remshellarg = "-s $NOLOGIN";
}
......@@ -922,6 +911,8 @@ sub UpdateUser(;$)
# Update elists in case email changed.
system("$GENELISTS -m -u $user");
$EUID = 0;
CheckDotFiles()
if (! $freezeopt);
return 0;
}
......@@ -1028,7 +1019,6 @@ sub FreezeUser()
$target_user->SetStatus(USERSTATUS_FROZEN());
$status = USERSTATUS_FROZEN();
}
$sfsupdate = 1;
return UpdateUser(1);
}
......@@ -1053,7 +1043,6 @@ sub ThawUser()
$target_user->SetStatus(USERSTATUS_ACTIVE());
$status = USERSTATUS_ACTIVE();
}
$sfsupdate = 1;
#
# This lets users start off as frozen in an ELABINELAB, and then
......@@ -1206,74 +1195,6 @@ sub CheckDotFiles()
return 0;
}
#
# Do SFS stuff. Might move this out to its own script at some point.
#
sub GenerateSFSKey()
{
my $sfsdir = "$HOMEDIR/$user/.sfs";
#
# Set up the sfs key, but only if not done so already.
# This has to be done from root because the sfs_users file needs
# to be updated (and "sfskey register" won't work because it
# prompts for the user's UNIX password if not run from root.)
#
if ($WITHSFS && ! -e "$sfsdir/identity") {
if (! -e "$sfsdir" ) {
print "Setting up sfs configuration for $user.\n";
mkdir("$sfsdir", 0700) or
fatal("Could not mkdir $sfsdir: $!");
chown($user_number, $default_groupgid, "$sfsdir") or
fatal("Could not chown $sfsdir: $!");
}
print "Generating sfs key\n";
$UID = 0;
if (system("$SSH -host $CONTROL '$SFSKEYGEN -KPn ".
"$user\@ops.emulab.net $sfsdir/identity'")) {
fatal("Failure in sfskey gen!");
}
# Version 7 stuff for later.
#if (system("$SSH -host $CONTROL '$SFSKEYGEN -KP ".
# "-l $user\@ops.emulab.net $sfsdir/identity'")) {
# fatal("Failure in sfskey gen!");
#}
$UID = $SAVEUID;
chown($user_number, $default_groupgid, "$sfsdir/identity") or
fatal("Could not chown $sfsdir/identity: $!");
chmod(0600, "$sfsdir/identity") or
fatal("Could not chmod $sfsdir/identity: $!");
#
# Grab a copy for the DB. Causes an SFS update key to run so
# that key is inserted into the files.
#
my $ident = `cat $sfsdir/identity`;
if ($ident =~ /.*,.*,.*,(.*),(.*)/) {
# Version 6
DBQueryFatal("replace into user_sfskeys ".
"values ('$user', '$2', '${user}:${1}:${user}::', ".
"now())");
}
elsif ($ident =~ /.*:.*:.*:(.*):(.*)/) {
# Version 7
DBQueryFatal("replace into user_sfskeys ".
"values ('$user', '$2', '${user}:${1}:${user}::', ".
"now())");
}
else {
warn("*** $0:\n".
" Bad emulab SFS public key\n");
}
$sfsupdate = 1;
}
return 0;
}
sub fatal($) {
my($mesg) = $_[0];
......
......@@ -199,7 +199,7 @@ LoadModule rewrite_module libexec/apache22/mod_rewrite.so
#LoadModule proxy_http_module libexec/apache22/mod_proxy_http.so
#LoadModule proxy_connect_module libexec/apache22/mod_proxy_connect.so
LoadModule cache_module libexec/apache22/mod_cache.so
LoadModule suexec_module libexec/apache22/mod_suexec.so
#LoadModule suexec_module libexec/apache22/mod_suexec.so
#LoadModule disk_cache_module libexec/apache22/mod_disk_cache.so
#LoadModule file_cache_module libexec/apache22/mod_file_cache.so
#LoadModule mem_cache_module libexec/apache22/mod_mem_cache.so
......
#!/usr/bin/perl -wT
#
# Copyright (c) 2007-2014 University of Utah and the Flux Group.
# Copyright (c) 2007-2015 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -440,7 +440,7 @@ sub CreateDataset($)
if (defined($self->expires()));
my $cmurl = $authority->url();
$cmurl =~ s/protogeni/protogeni\/stoller/;
# $cmurl =~ s/protogeni/protogeni\/stoller/;
return Genixmlrpc::CallMethod($cmurl, $context, "CreateDataset", $args);
}
......@@ -471,7 +471,7 @@ sub DeleteDataset($)
$speaksfor_credential->asString()],
};
my $cmurl = $authority->url();
$cmurl =~ s/protogeni/protogeni\/stoller/;
# $cmurl =~ s/protogeni/protogeni\/stoller/;
return Genixmlrpc::CallMethod($cmurl, $context, "DeleteDataset", $args);
}
......@@ -506,7 +506,7 @@ sub ModifyDataset($)
if (defined($self->expires()));
my $cmurl = $authority->url();
$cmurl =~ s/protogeni/protogeni\/stoller/;
# $cmurl =~ s/protogeni/protogeni\/stoller/;
return Genixmlrpc::CallMethod($cmurl, $context, "ModifyDataset", $args);
}
......@@ -538,7 +538,7 @@ sub ExtendDataset($)
"extend" => 1,
};
my $cmurl = $authority->url();
$cmurl =~ s/protogeni/protogeni\/stoller/;
# $cmurl =~ s/protogeni/protogeni\/stoller/;
return Genixmlrpc::CallMethod($cmurl, $context, "ModifyDataset", $args);
}
......@@ -569,7 +569,7 @@ sub DescribeDataset($)
$speaksfor_credential->asString()],
};
my $cmurl = $authority->url();
$cmurl =~ s/protogeni/protogeni\/stoller/;
# $cmurl =~ s/protogeni/protogeni\/stoller/;
return Genixmlrpc::CallMethod($cmurl, $context, "DescribeDataset", $args);
}
......
#!/usr/bin/perl -wT
#
# Copyright (c) 2007-2014 University of Utah and the Flux Group.
# Copyright (c) 2007-2015 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -344,8 +344,9 @@ sub RecordHistory($)
DBQueryWarn("replace into apt_instance_history ".
"select uuid,profile_id,profile_version,slice_uuid, ".
" creator,creator_idx,creator_uuid,aggregate_urn, ".
" public_url,created,now(),servername ".
" creator,creator_idx,creator_uuid,pid,pid_idx, ".
" aggregate_urn,public_url,created,now(),servername, ".
" rspec,params,manifest ".
" from apt_instances where uuid='$uuid'")
or return -1;
......
......@@ -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'"));
......@@ -946,7 +950,28 @@ sub Publish($)
$self->{'DBROW'}->{'published'} = time();
return 0;
}
}
#
# Manage URL
#
sub AdminURL($)
{
my ($self) = @_;
my $uuid = $self->uuid();
require Project;
my $project = Project->Lookup($self->pid_idx());
return undef
if (!defined($project));
my $wwwbase = $project->wwwBase();
$wwwbase .= "/apt"
if ($project->Brand()->isEmulab());
return $wwwbase . "/manage_profile.php?uuid=$uuid";
}
# _Always_ make sure that this 1 is at the end of the file...
1;
#
# Copyright (c) 2000-2014 University of Utah and the Flux Group.
# Copyright (c) 2000-2015 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......
......@@ -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(),
......@@ -712,6 +728,8 @@ while ($seconds > 0) {
}
elsif ($blob->{'status'} eq "failed") {
$failed = 1;
$webtask->output("Experiment setup on $aggregate failed")
if (defined($webtask));
last;
}
}
......@@ -721,6 +739,8 @@ if ($failed || !$ready) {
$instance->SetStatus("failed");
if (!$ready) {
print STDERR "*** $slice_urn timed out.\n\n";
$webtask->output("Experiment setup on $aggregate timed out")
if (defined($webtask));
}
else {
print STDERR "*** $slice_urn failed.\n\n";
......
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2014 University of Utah and the Flux Group.
# Copyright (c) 2000-2015 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -53,7 +53,6 @@ my $webtask;
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $QUICKVM = "$TB/sbin/protogeni/quickvm";
my $VERSIONING = @PROFILEVERSIONS@;
#
# Untaint the path
......@@ -85,6 +84,7 @@ use Genixmlrpc;
use GeniResponse;
use GeniSlice;
use WebTask;
use EmulabFeatures;
# Protos
sub fatal($);
......@@ -176,6 +176,7 @@ sub DoSnapshot()
# create the name and update the underlying profile with the new
# image urn.
#
my $project;
my $imagename;
my $node_id;
my $sliver_urn;
......@@ -194,6 +195,10 @@ sub DoSnapshot()
else {
$imagename = $profile->name();
$update_profile = 1;
$project = Project->Lookup($profile->pid_idx());
if (!defined($project)) {
fatal("Could not lookup project for profile");
}
}
#
......@@ -442,7 +447,11 @@ sub DoSnapshot()
# that we expect the CM is doing image versioning, so do not
# bother to check if the image version is actually new.
#
if ($VERSIONING) {
my $doversions =
EmulabFeatures->FeatureEnabled("APT_ProfileVersions",
$this_user, $project);
if ($doversions) {
$profile = $profile->NewVersion($this_user);
if (!defined($profile)) {
print STDERR "Could not create new profile version\n";
......
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2014 University of Utah and the Flux Group.
# Copyright (c) 2000-2015 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -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;