Commit b3411d01 authored by Leigh Stoller's avatar Leigh Stoller

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
echo "</div>\n"; # body
echo "</div>\n"; # container
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment