Commit b3411d01 authored by Leigh B Stoller's avatar Leigh B Stoller
Browse files

The first implementation of Create Profile from Experiment,

only visible to admin users. The control flow is not how we
want it, and I don't want to spend too much time on it until
Jon gets his stuff committed, which I think will include some
better ajax code.
parent 8cdd9555
......@@ -33,22 +33,25 @@ use CGI;
#
sub usage()
{
print("Usage: manage_profile [-u] <xmlfile>\n");
print("Usage: manage_profile [-u | -s uuid] <xmlfile>\n");
print("Usage: manage_profile -r profile\n");
exit(-1);
}
my $optlist = "dur";
my $optlist = "durs:";
my $debug = 0;
my $verify = 0; # Check data and return status only.
my $update = 0;
my $delete = 0;
my $skipadmin = 0;
my $snapuuid;
my $instance;
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $QUICKVM = "$TB/sbin/protogeni/quickvm";
#
# Untaint the path
......@@ -71,6 +74,9 @@ use emutil;
use User;
use Project;
use APT_Profile;
use APT_Instance;
use GeniXML;
use GeniHRN;
# Protos
sub fatal($);
......@@ -94,6 +100,9 @@ if (defined($options{"v"})) {
if (defined($options{"u"})) {
$update = 1;
}
if (defined($options{"s"})) {
$snapuuid = $options{"s"};
}
if (@ARGV != 1) {
usage();
}
......@@ -233,6 +242,26 @@ elsif (!$project->AccessCheck($this_user, TB_PROJECT_MAKEIMAGEID())) {
$errors{"profile_pid"} = "Not enough permission in this project";
}
#
# Are we going to snapshot a node in an experiment? If so we
# sanity check to make sure there is just one node.
#
if (defined($snapuuid)) {
$instance = APT_Instance->Lookup($snapuuid);
if (!defined($instance)) {
fatal("Could not look up instance $snapuuid");
}
my $manifest = GeniXML::Parse($instance->manifest());
if (! defined($manifest)) {
fatal("Could not parse manifest");
}
my @nodes = GeniXML::FindNodes("n:node", $manifest)->get_nodelist();
if (@nodes != 1) {
$errors{"error"} = "Too many nodes (> 1) to snapshot";
UserError();
}
}
my $profile = APT_Profile->Lookup($new_args{"pid"}, $new_args{"name"});
if ($update) {
......@@ -264,6 +293,56 @@ else {
fatal("Could not create new profile");
}
}
#
# Now do the snapshot operation.
#
if (defined($instance)) {
my $manifest = GeniXML::Parse($instance->manifest());
if (! defined($manifest)) {
fatal("Could not parse manifest");
}
my ($node) = GeniXML::FindNodes("n:node", $manifest)->get_nodelist();
my $sliver_urn = GeniXML::GetSliverId($node);
my $apt_uuid = $instance->uuid();
my $imagename = $profile->name();
my $command = "$QUICKVM -s $apt_uuid $sliver_urn $imagename";
#
# This returns pretty fast, and then the imaging takes place in
# the background at the aggregate. quickvm keeps a process running
# in the background waiting for the sliver to unlock and the
# sliverstatus to indicate the node is running again.
#
my $output = emutil::ExecQuiet($command);
if ($?) {
$profile->Delete();
fatal("Failed to create disk image!");
}
#
# Parse the output to get the new image urn, then stick that
# into the rspec, and update the database.
#
my $image_urn;
if ($output =~ /^(urn:.*),/) {
$image_urn = $1;
}
else {
$profile->Delete();
fatal("Could not find image urn in:\n$output");
}
my $rspec = GeniXML::Parse($profile->rspec());
if (! defined($rspec)) {
$profile->Delete();
fatal("Could not parse rspec");
}
($node) = GeniXML::FindNodes("n:node", $rspec)->get_nodelist();
GeniXML::SetDiskImage($node, $image_urn);
if ($profile->Update({"rspec" => GeniXML::Serialize($rspec)})) {
$profile->Delete();
fatal("Could not update rspec");
}
}
exit(0);
sub fatal($)
......
......@@ -117,6 +117,7 @@ function ($, sup)
event.preventDefault();
return false;
}
sup.ShowModal("#waitwait");
return true;
});
......
......@@ -6,6 +6,7 @@ function ($, sup, moment)
{
'use strict';
var CurrentTopo = null;
var nodecount = 0;
function initialize()
{
......@@ -55,9 +56,7 @@ function ($, sup, moment)
event.preventDefault();
sup.HideModal('#terminate_modal');
// Disable buttons.
$("#terminate_button").prop("disabled", true);
$("#extend_button").prop("disabled", true);
ButtonDisable();
var callback = function(json) {
// This is considered the home page, for now.
......@@ -87,14 +86,22 @@ function ($, sup, moment)
// Call back for above.
function StatusWatchCallBack(uuid, json)
{
// Check to see if the static variable has been initialized
// Flag to indicate that we have seen ready and do not
// need to do initial stuff. We need this cause the
// the staus can change later, back to busy for a while.
if (typeof StatusWatchCallBack.active == 'undefined') {
// It has not... perform the initilization
StatusWatchCallBack.active = 0;
}
// Flag so we know status has changed since last check.
if (typeof StatusWatchCallBack.laststatus == 'undefined') {
// It has not... perform the initilization
StatusWatchCallBack.laststatus = "";
}
var status = json.value;
if (json.code) {
status = "terminated";
alert("The server has returned an error: " + json.value);
status = "unknown";
}
var status_html = "";
......@@ -118,10 +125,12 @@ function ($, sup, moment)
$("#quickvm_progress").addClass("progress-bar-success");
$("#quickvm_progress_bar").width("100%");
}
$("#terminate_button").prop("disabled", false);
$("#extend_button").prop("disabled", false);
ShowTopo(uuid);
StartResizeWatchdog()
if (! StatusWatchCallBack.active) {
ShowTopo(uuid);
StartResizeWatchdog()
StatusWatchCallBack.active = 1;
}
ButtonEnable();
}
else if (status == 'failed') {
bgtype = "bg-danger";
......@@ -134,14 +143,19 @@ function ($, sup, moment)
$("#quickvm_progress").addClass("progress-bar-danger");
$("#quickvm_progress_bar").width("100%");
}
$("#terminate_button").prop("disabled", false);
ButtonDisable();
}
else if (status == 'imaging') {
bgtype = "bg-warning";
statustext = "Your experiment is busy while we copy your disk ";
status_html = "<font color=red>imaging</font>";
ButtonDisable();
}
else if (status == 'terminating' || status == 'terminated') {
status_html = "<font color=red>" + status + "</font>";
bgtype = "bg-danger";
statustext = "Your experiment has been terminated!";
$("#terminate_button").prop("disabled", true);
$("#extend_button").prop("disabled", true);
ButtonDisable();
StartCountdownClock.stop = 1;
}
$("#statusmessage").html(statustext);
......@@ -151,11 +165,41 @@ function ($, sup, moment)
$("#quickvm_status").html(status_html);
}
StatusWatchCallBack.laststatus = status;
if (! (status == 'terminating' || status == 'terminated')) {
if (! (status == 'terminating' || status == 'terminated' ||
status == 'unknown')) {
setTimeout(function f() { GetStatus(uuid) }, 5000);
}
}
//
// Enable/Disable buttons.
//
function ButtonEnable()
{
ButtonState(1);
}
function ButtonDisable()
{
ButtonState(0);
}
function ButtonState(enable)
{
if (enable) {
$("#terminate_button").prop("disabled", false);
$("#extend_button").prop("disabled", false);
if ($nodecount == 1) {
$("#snapshot_button").prop("disabled", false);
}
}
else {
$("#terminate_button").prop("disabled", true);
$("#extend_button").prop("disabled", true);
if ($nodecount == 1) {
$("#snapshot_button").prop("disabled", true);
}
}
}
//
// Install a window resize handler to redraw the topomap.
//
......@@ -213,7 +257,7 @@ function ($, sup, moment)
var color = "";
// update the tag with id "countdown" every 1 second
var updaer = setInterval(function () {
var updater = setInterval(function () {
// Clock stop
if (StartCountdownClock.stop) {
// Amazing that this works!
......@@ -462,6 +506,11 @@ function ($, sup, moment)
console.info(json.value);
if ($("#manifest_textarea").length) {
$("#manifest_textarea").html(json.value);
$("#manifest_textarea").css("height", "300");
}
// Suck the instructions out of the tour and put them into
// the Usage area.
$(xml).find("rspec_tour").each(function() {
......@@ -482,7 +531,6 @@ function ($, sup, moment)
// Special case for a topology of a single node; start the
// ssh tab right away.
//
var nodecount = 0;
var nodehostport = null;
var nodename = null;
......@@ -532,6 +580,12 @@ function ($, sup, moment)
ReDrawTopoMap();
$("#showtopo_container").removeClass("invisible");
// If a single node, show the snapshot button. Only
// single node experiments can do this.
if (nodecount == 1) {
$("#snapshot_button").removeClass("invisible");
}
// And start up ssh for single node topologies.
if (nodecount == 1 && nodehostport) {
NewSSHTab(nodehostport, nodename);
......
......@@ -26,6 +26,7 @@ include("defs.php3");
chdir("apt");
include("quickvm_sup.php");
include("profile_defs.php");
include("instance_defs.php");
$page_title = "Manage Profile";
$notifyupdate = 0;
......@@ -40,6 +41,7 @@ $this_user = CheckLogin($check_status);
$optargs = OptionalPageArguments("create", PAGEARG_STRING,
"action", PAGEARG_STRING,
"idx", PAGEARG_INTEGER,
"snapuuid", PAGEARG_STRING,
"finished", PAGEARG_BOOLEAN,
"formfields", PAGEARG_ARRAY);
......@@ -48,7 +50,7 @@ $optargs = OptionalPageArguments("create", PAGEARG_STRING,
#
function SPITFORM($formfields, $errors)
{
global $this_user, $projlist, $action, $idx, $notifyupdate;
global $this_user, $projlist, $action, $idx, $notifyupdate, $snapuuid;
$editing = 0;
if ($action == "edit") {
......@@ -143,9 +145,13 @@ function SPITFORM($formfields, $errors)
if ($notifyupdate) {
echo "<font color=green><center>Update Successful!</center></font>";
}
# Mark as editing mode on post.
if ($editing) {
echo "<input type='hidden' name='action' value='edit'>\n";
# Send action back through.
if (isset($action)) {
echo "<input type='hidden' name='action' value='$action'>\n";
# And include the experiment getting snapped.
if (isset($snapuuid)) {
echo "<input type='hidden' name='snapuuid' value='$snapuuid'>\n";
}
}
echo " </div></div><fieldset>\n";
......@@ -221,7 +227,7 @@ function SPITFORM($formfields, $errors)
# See below for the modal. So, we need buttons to display the source
# modal, the topo modal, in addition to a file chooser for a new rspec.
#
$invisible = ($editing ? "" : "invisible");
$invisible = (isset($action) ? "" : "invisible");
$rspec_html =
"<div class='row'>
......@@ -453,9 +459,13 @@ function SPITFORM($formfields, $errors)
</div>
</div>
</div>\n";
SpitWaitModal("waitwait");
SpitOopsModal("oops");
echo "<script type='text/javascript'>\n";
echo " window.EDITING = $editing;\n";
echo " window.EDITING = " . (isset($action) ? 1 : 0) . ";\n";
echo " window.SNAPWAIT = " . (isset($snapuuid) ? 1 : 0) . ";\n";
echo "</script>\n";
echo "<script src='js/lib/require.js' data-main='js/manage_profile'>
</script>";
......@@ -474,7 +484,7 @@ $this_idx = $this_user->uid_idx();
#
# See what projects the user can do this in.
#
$projlist = $this_user->ProjectAccessList($TB_PROJECT_MAKEIMAGEID);
$projlist = $this_user->ProjectAccessList($TB_PROJECT_CREATEEXPT);
if (! isset($create)) {
$errors = array();
......@@ -485,55 +495,76 @@ if (! isset($create)) {
"You do not appear to be a member of any projects in which ".
"you have permission to create new profiles";
}
if ($action == "edit" || $action == "delete" || "snapshot") {
if (!isset($idx)) {
$errors["error"] = "No profile specified for edit/delete!";
}
else {
$profile = Profile::Lookup($idx);
if (!$profile) {
SPITUSERERROR("No such profile!");
if (isset($action) &&
($action == "edit" || $action == "delete" || "snapshot")) {
if ($action == "snapshot") {
if (! (isset($snapuuid) && IsValidUUID($snapuuid))) {
$errors["error"] = "No experiment specified for snapshot!";
}
$instance = Instance::Lookup($snapuuid);
if (!$instance) {
SPITUSERERROR("No such instance to snapshot!");
}
else if ($this_idx != $profile->creator_idx() && !ISADMIN()) {
else if ($this_idx != $instance->creator_idx() && !ISADMIN()) {
SPITUSERERROR("Not enough permission!");
}
else if ($action == "delete") {
DBQueryFatal("delete from apt_profiles where idx='$idx'");
header("Location: $APTBASE/myprofiles.php");
return;
$profile = Profile::Lookup($instance->profile_idx());
if (!$profile) {
SPITUSERERROR("Cannot load profile for instance!");
}
else if ($action == "snapshot") {
$defaults["profile_rspec"] = $profile->rspec();
else if ($this_idx != $profile->creator_idx() &&
!$profile->ispublic() && !ISADMIN()) {
SPITUSERERROR("Not enough permission!");
}
$defaults["profile_rspec"] = $profile->rspec();
}
else {
if (! isset($idx)) {
$errors["error"] = "No profile specified for edit/delete!";
}
else {
$defaults["profile_uuid"] = $profile->uuid();
$defaults["profile_pid"] = $profile->pid();
$defaults["profile_description"] = $profile->description();
$defaults["profile_name"] = $profile->name();
$defaults["profile_rspec"] = $profile->rspec();
$defaults["profile_created"] = $profile->created();
$defaults["profile_url"] = $profile->url();
$defaults["profile_listed"] =
($profile->listed() ? "checked" : "");
$defaults["profile_who"] =
($profile->shared() ? "shared" :
($profile->ispublic() ? "public" : "private"));
#
# If we are displaying after a successful edit, and it
# just happened (by looking at the modify time), show
# a message that the update was successful. This is pretty
# crappy, but I do not want to go for a fancy thing (popover)
# just yet, maybe later.
#
if (isset($finished) && $profile->modified()) {
$mod = new DateTime($profile->modified());
if ($mod) {
$now = new DateTime("now");
$diff = $now->getTimestamp() - $mod->getTimestamp();
if ($diff < 2) {
$notifyupdate = 1;
$profile = Profile::Lookup($idx);
if (!$profile) {
SPITUSERERROR("No such profile!");
}
else if ($this_idx != $profile->creator_idx() && !ISADMIN()) {
SPITUSERERROR("Not enough permission!");
}
else if ($action == "delete") {
DBQueryFatal("delete from apt_profiles where idx='$idx'");
header("Location: $APTBASE/myprofiles.php");
return;
}
else {
$defaults["profile_uuid"] = $profile->uuid();
$defaults["profile_pid"] = $profile->pid();
$defaults["profile_description"] = $profile->description();
$defaults["profile_name"] = $profile->name();
$defaults["profile_rspec"] = $profile->rspec();
$defaults["profile_created"] = $profile->created();
$defaults["profile_url"] = $profile->url();
$defaults["profile_listed"] =
($profile->listed() ? "checked" : "");
$defaults["profile_who"] =
($profile->shared() ? "shared" :
($profile->ispublic() ? "public" : "private"));
#
# If we are displaying after a successful edit, and it
# just happened (by looking at the modify time), show
# a message that the update was successful. This is pretty
# crappy, but I do not want to go for a fancy thing
# just yet, maybe later.
#
if (isset($finished) && $profile->modified()) {
$mod = new DateTime($profile->modified());
if ($mod) {
$now = new DateTime("now");
$diff = $now->getTimestamp() - $mod->getTimestamp();
if ($diff < 2) {
$notifyupdate = 1;
}
}
}
}
......@@ -625,6 +656,32 @@ else {
}
}
#
# Sanity check the snapuuid argument.
#
if (isset($action) && $action == "snapshot") {
if (! IsValidUUID($snapuuid)) {
$errors["error"] = "Invalid experiment specified for snapshot!";
}
$instance = Instance::Lookup($snapuuid);
if (!$instance) {
$errors["error"] = "No such experiment to snapshot!";
}
else if ($this_idx != $instance->creator_idx() && !ISADMIN()) {
$errors["error"] = "Not enough permission!";
}
else {
$profile = Profile::Lookup($instance->profile_idx());
if (!$profile) {
$errors["error"] = "Cannot load profile for instance!";
}
else if ($this_idx != $profile->creator_idx() &&
!$profile->ispublic() && !ISADMIN()) {
$errors["error"] = "Not enough permission!";
}
}
}
# Present these errors before we call out to do anything else.
if (count($errors)) {
SPITFORM($formfields, $errors);
......@@ -686,6 +743,9 @@ else {
# Call out to the backend.
#
$optarg = ($action == "edit" ? "-u" : "");
if (isset($snapuuid)) {
$optarg .= "-s " . escapeshellarg($snapuuid);
}
$retval = SUEXEC($this_user->uid(), $project->unix_gid(),
"webmanage_profile $optarg $xmlname",
SUEXEC_ACTION_IGNORE);
......
......@@ -55,6 +55,7 @@ body {
margin-top: 20px;
}
.navbar-btn.btn-xs,
.navbar-btn {
margin-top: 20px;
}
......
......@@ -111,7 +111,12 @@ if (! (isset($this_user) && ISADMIN())) {
isset($_COOKIE['quickvm_user']) &&
$_COOKIE['quickvm_user'] == $creator->uuid()))) {
if ($ajax_request) {
SPITAJAX_ERROR(1, "You do not have permission!");
if ($check_status & CHECKLOGIN_TIMEDOUT) {
SPITAJAX_ERROR(2, "Your login has timed out!");
}
else {
SPITAJAX_ERROR(1, "You do not have permission!");
}
exit();
}
PAGEERROR("You do not have permission to look at this experiment!");
......@@ -221,6 +226,12 @@ elseif ($instance_status == "ready") {
$bgtype = "bg-success";
$statustext = "Your experiment is ready!";
}
elseif ($instance_status == "imaging") {
$color = "color=green";
$spin = 0;
$bgtype = "bg-warning";
$statustext = "Your experiment is ready!";
}
elseif ($instance_status == "created") {
$spinwidth = "33";
}
......@@ -280,11 +291,11 @@ echo "<td class='uk-width-4-5' $style>
echo "</tr>\n";
echo "</table>\n";
echo "<div class='pull-right'>\n";
if (0) {
echo " <a class='btn btn-xs btn-primary' $disabled
id='snapshot_button' type=button
href='manage_profile.php?action=snapshot&snapuuid=$uuid'>
Snapshot</a>\n";
if (isset($this_user) && ISADMIN()) {
echo " <a class='btn btn-xs btn-primary' $disabled hidden
id='snapshot_button' type=button
href='manage_profile.php?action=snapshot&snapuuid=$uuid'>
Snapshot</a>\n";
}
echo " <button class='btn btn-xs btn-success' $disabled
id='extend_button' type=button
......@@ -335,8 +346,13 @@ echo " <ul id='quicktabs' class='nav nav-tabs'>
</li>
<li>
<a href='#listview' data-toggle='tab'>List View</a>
</li>
</ul>
</li>\n";
if (isset($this_user) && ISADMIN()) {
echo "<li>
<a href='#manifest' data-toggle='tab'>Manifest</a>
</li>\n";
}
echo " </ul>
<div id='quicktabs_content' class='tab-content'>
<div class='tab-pane active' id='profile'>
<div id='showtopo_statuspage'></div>
......@@ -359,8 +375,14 @@ echo " <ul id='quicktabs' class='nav nav-tabs'>
</tbody>
</table>
</div>
</div>
</div>\n";
</div>\n";
if (isset($this_user) && ISADMIN()) {
echo "<div class='tab-pane' id='manifest'>
<textarea id='manifest_textarea' style='width: 100%;'
type='textarea'></textarea>
</div>\n";
}
echo " </div>\n";
echo "</div>\n"; # quicktabs