Commit 611aa66a authored by Leigh Stoller's avatar Leigh Stoller

Add "snapshot" to status page. A little different then clone, snapsnot

allows the user to create a new disk image in place, instead of having
to create a new profile (Clone).
parent 0625cf7b
......@@ -30,10 +30,10 @@ include $(OBJDIR)/Makeconf
SUBDIRS =
BIN_SCRIPTS = manage_profile
BIN_SCRIPTS = manage_profile manage_instance
SBIN_SCRIPTS =
LIB_SCRIPTS = APT_Profile.pm APT_Instance.pm
WEB_BIN_SCRIPTS = webmanage_profile
WEB_BIN_SCRIPTS = webmanage_profile webmanage_instance
WEB_SBIN_SCRIPTS=
LIBEXEC_SCRIPTS = $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS)
......
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2014 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
use English;
use strict;
use Getopt::Std;
use XML::Simple;
use Data::Dumper;
use CGI;
use POSIX ":sys_wait_h";
use POSIX qw(setsid);
#
# Back-end script to manage APT profiles.
#
sub usage()
{
print("Usage: manage_instance -s instance [imagename]\n");
print("Usage: manage_instance -r instance\n");
exit(-1);
}
my $optlist = "dsrt:";
my $debug = 0;
my $delete = 0;
my $snapshot = 0;
my $webtask;
my $webtask_id;
my $webtask_delete;
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $QUICKVM = "$TB/sbin/protogeni/quickvm";
#
# 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 EmulabConstants;
use emdb;
use emutil;
use User;
use Project;
use APT_Profile;
use APT_Instance;
use GeniXML;
use GeniHRN;
use WebTask;
# Protos
sub fatal($);
sub DoSnapshot();
#
# 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{"s"})) {
$snapshot = 1;
}
if (defined($options{"r"})) {
$delete = 1;
}
if (defined($options{"t"})) {
$webtask_id = $options{"t"};
}
if (@ARGV < 1) {
usage();
}
# The web interface (and in the future the xmlrpc interface) sets this.
my $this_user = User->ImpliedUser();
if (! defined($this_user)) {
$this_user = User->ThisUser();
if (!defined($this_user)) {
fatal("You ($UID) do not exist!");
}
}
my $uuid = shift(@ARGV);
my $instance = APT_Instance->Lookup($uuid);
if (!defined($instance)) {
fatal("No such instance $uuid");
}
if ($snapshot) {
DoSnapshot();
}
exit(0);
#
# Take a snapshot. Implies a single node instance, for now.
#
sub DoSnapshot()
{
#
# If we get an imagename on the command line, the caller is
# saying it is responsible. If we do not get one, we create
# the name and update the underlying profile with the new image
# urn.
#
my $imagename;
my $update_profile = 0;
my $rspec;
my $profile = APT_Profile->Lookup($instance->profile_idx());
if (!defined($profile)) {
fatal("Could not lookup profile for instance");
}
if (@ARGV) {
$imagename = shift(@ARGV);
}
else {
$imagename = $profile->name();
$update_profile = 1;
}
#
# Sanity check to make sure there is just one node.
#
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) {
fatal("Too many nodes (> 1) to snapshot");
}
my ($node) = @nodes;
my $sliver_urn = GeniXML::GetSliverId($node);
#
# But we eventually update the rspec, so get that.
#
if ($update_profile) {
$rspec = GeniXML::Parse($profile->rspec());
if (! defined($rspec)) {
fatal("Could not parse rspec for profile");
}
($node) = GeniXML::FindNodes("n:node", $rspec)->get_nodelist();
}
#
# Create the webtask object.
#
if (defined($webtask_id)) {
$webtask = WebTask->Lookup($webtask_id);
$webtask_delete = 0;
}
#
# We always create one for the called script. Makes it easy to
# communicate.
#
if (!defined($webtask)) {
$webtask = WebTask->Create($instance->uuid(), $webtask_id);
if (!defined($webtask)) {
fatal("Could not create webtask object");
}
# We created this cause caller did not specify it needed one,
# so we will delete it when we are done.
if (!defined($webtask_id)) {
$webtask_id = $webtask_id->task_id();
$webtask_delete = 1;
}
}
# Convenient.
$webtask->AutoStore(1);
my $command = "$QUICKVM -t $webtask_id -s $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 ($?) {
$webtask->Delete()
if ($webtask_delete);
print STDERR $output;
fatal("Failed to create disk image");
}
#
# Parse the output to get the new image urn, we will stick that
# into the rspec if the operation succeeds.
#
my $image_urn;
if ($output =~ /^(urn:.*),/) {
$image_urn = $1;
}
else {
$webtask->Delete()
if ($webtask_delete);
fatal("Could not find image urn in:\n$output");
}
#
# Exit and leave child to poll.
#
if (! $debug) {
my $child = fork();
if ($child) {
# Pass info to caller.
print $output;
exit(0);
}
# Let parent exit;
sleep(2);
POSIX::setsid();
}
my $seconds = 1200;
my $interval = 5;
while ($seconds >= 0) {
sleep($interval);
$seconds -= $interval;
# Pick up new DB values.
$webtask->Refresh();
last
if (defined($webtask->exited()));
}
if ($webtask->exitcode()) {
$webtask->Delete()
if ($webtask_delete);
exit(1);
}
if ($update_profile) {
GeniXML::SetDiskImage($node, $image_urn);
if ($profile->Update({"rspec" => GeniXML::Serialize($rspec)})) {
fatal("Could not update rspec with new image urn");
}
}
$webtask->Delete()
if ($webtask_delete);
exit(0);
}
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 escapeshellarg($)
{
my ($str) = @_;
$str =~ s/[^[:alnum:]]/\\$&/g;
return $str;
}
......@@ -323,6 +323,7 @@ if (defined($instance)) {
if (!defined($webtask)) {
$profile->Delete();
}
$webtask->AutoStore(1);
if ($profile->Lock()) {
$profile->Delete();
......
......@@ -1019,6 +1019,7 @@ sub SnapShot($$$)
if (defined($sliverblob)) {
$webtask->state($sliverblob->{'state'});
$webtask->rawstate($sliverblob->{'rawstate'});
$webtask->Store();
}
}
if ($blob->{'status'} eq "failed") {
......
......@@ -149,6 +149,7 @@ BLOBFILES += $(wildcard blob/*.php3)
APTUIFILES = $(wildcard $(SRCDIR)/aptui/*.php)
APTUIFILES += $(wildcard $(SRCDIR)/aptui/*.ajax)
APTUIFILES += $(wildcard $(SRCDIR)/aptui/*.png)
APTUIFILES += $(wildcard $(SRCDIR)/aptui/*.ico)
APTUIFILES += $(wildcard $(SRCDIR)/aptui/*.gif)
APTUIFILES += $(wildcard $(SRCDIR)/aptui/.htaccess)
APTJSFILES = $(wildcard $(SRCDIR)/aptui/js/*.js)
......
......@@ -590,7 +590,7 @@ if ($retval != 0) {
}
else {
if (count($suexec_output_array)) {
$line = $suexec_output_array[$i];
$line = $suexec_output_array[0];
$errors["error"] = $line;
}
else {
......
//
// Progress Modal
//
define(['underscore', 'js/quickvm_sup', 'filesize',
'js/lib/text!template/imaging-modal.html'],
function(_, sup, filesize, imagingString)
{
'use strict';
var imagingTemplate = null;
var imaging_modal_display = true;
var imaging_modal_active = false;
var status_callback;
var completion_callback;
function ShowImagingModal()
{
//
// Ask the server for information to populate the imaging modal.
//
var callback = function(json) {
var value = json.value;
//console.log(json);
if (json.code) {
if (imaging_modal_active) {
sup.HideModal("#imaging-modal");
imaging_modal_active = false;
$('#imaging_modal').off('hidden.bs.modal');
}
sup.SpitOops("oops", "Server says: " + json.value);
completion_callback(1);
return;
}
if (! imaging_modal_active && imaging_modal_display) {
sup.ShowModal("#imaging-modal");
imaging_modal_active = true;
imaging_modal_display = false;
// Handler so we know the user closed the modal.
$('#imaging_modal').on('hidden.bs.modal', function (e) {
imaging_modal_active = false;
$('#imaging_modal').off('hidden.bs.modal');
})
}
//
// Fill in the details.
//
if (! _.has(value, "node_status")) {
value["node_status"] = "unknown";
}
if (_.has(value, "image_size")) {
// We get KB to avoid overflow along the way.
value["image_size"] = filesize(value["image_size"]*1024);
}
else {
value["image_size"] = "unknown";
}
$('#imaging_modal_node_status').html(value["node_status"]);
$('#imaging_modal_image_size').html(value["image_size"]);
if (_.has(value, "image_status")) {
var status = value["image_status"];
if (status == "imaging") {
$('#tracker-imaging').removeClass('progtrckr-todo');
$('#tracker-imaging').addClass('progtrckr-done');
}
else if (status == "finishing") {
$('#tracker-imaging').removeClass('progtrckr-todo');
$('#tracker-imaging').addClass('progtrckr-done');
$('#tracker-finishing').removeClass('progtrckr-todo');
$('#tracker-finishing').addClass('progtrckr-done');
}
else if (status == "ready") {
$('#tracker-imaging').removeClass('progtrckr-todo');
$('#tracker-imaging').addClass('progtrckr-done');
$('#tracker-finishing').removeClass('progtrckr-todo');
$('#tracker-finishing').addClass('progtrckr-done');
$('#tracker-ready').removeClass('progtrckr-todo');
$('#tracker-ready').addClass('progtrckr-done');
$('#imaging-spinner').addClass("invisible");
completion_callback(0);
return;
}
else if (status == "failed") {
$('#tracker-imaging').removeClass('progtrckr-todo');
$('#tracker-imaging').addClass('progtrckr-done');
$('#tracker-finishing').removeClass('progtrckr-todo');
$('#tracker-finishing').addClass('progtrckr-done');
$('#tracker-ready').removeClass('progtrckr-todo');
$('#tracker-ready').addClass('progtrckr-done');
$('#tracker-ready').html("Failed");
$('#imaging-spinner').addClass("invisible");
completion_callback(1);
return;
}
}
//
// Done, we need to do something here if we exited before
// ready or failed above.
//
if (_.has(value, "exited")) {
$('#imaging-spinner').addClass("invisible");
completion_callback(0);
return;
}
// And check again in a little bit.
setTimeout(function f() { ShowImagingModal() }, 5000);
}
var $xmlthing = status_callback();
$xmlthing.done(callback);
}
return function(s_callback, c_callback)
{
status_callback = s_callback;
completion_callback = c_callback;
if (imagingTemplate == null) {
imagingTemplate = _.template(imagingString);
var imaging_html = imagingTemplate({});
$('#imaging_div').html(imaging_html);
}
imaging_modal_display = true;
ShowImagingModal();
}
}
);
\ No newline at end of file
window.APT_OPTIONS.config();
require(['underscore', 'js/quickvm_sup', 'filesize',
require(['underscore', 'js/quickvm_sup', 'filesize', 'js/image',
'js/lib/text!template/manage-profile.html',
'js/lib/text!template/waitwait-modal.html',
'js/lib/text!template/imaging-modal.html',
'js/lib/text!template/renderer-modal.html',
'js/lib/text!template/showtopo-modal.html',
'js/lib/text!template/oops-modal.html',
'js/lib/text!template/rspectextview-modal.html',
// jQuery modules
'filestyle','marked','jquery-ui','jquery-grid'],
function (_, sup, filesize,
manageString, waitwaitString, imagingString,
rendererString, showtopoString, rspectextviewString)
function (_, sup, filesize, ShowImagingModal,
manageString, waitwaitString,
rendererString, showtopoString, oopsString, rspectextviewString)
{
'use strict';
var editing = 0;
......@@ -21,10 +21,10 @@ require(['underscore', 'js/quickvm_sup', 'filesize',
var ajaxurl = "";
var manageTemplate = _.template(manageString);
var waitwaitTemplate = _.template(waitwaitString);
var imagingTemplate = _.template(imagingString);
var rendererTemplate = _.template(rendererString);
var showtopoTemplate = _.template(showtopoString);
var rspectextTemplate = _.template(rspectextviewString);
var oopsTemplate = _.template(oopsString);
function initialize()
{
......@@ -62,14 +62,14 @@ require(['underscore', 'js/quickvm_sup', 'filesize',
var waitwait_html = waitwaitTemplate({});
$('#waitwait_div').html(waitwait_html);
var imaging_html = imagingTemplate({});
$('#imaging_div').html(imaging_html);
var showtopo_html = showtopoTemplate({});
$('#showtopomodal_div').html(showtopo_html);
var renderer_html = rendererTemplate({});
$('#renderer_div').html(renderer_html);
var rspectext_html = rspectextTemplate({});
$('#rspectext_div').html(rspectext_html);
var oops_html = oopsTemplate({});
$('#oops_div').html(oops_html);
//
// Fix for filestyle problem; not a real class I guess, it
......@@ -515,111 +515,24 @@ require(['underscore', 'js/quickvm_sup', 'filesize',
//
// Progress Modal
//
var imaging_modal_display = true;
var imaging_modal_active = false;
function ShowProgressModal()
{
//
// Ask the server for information to populate the imaging modal.
//
var callback = function(json) {
var value = json.value;
if (json.code) {
if (imaging_modal_active) {
sup.HideModal("#imaging-modal");
imaging_modal_active = false;
$('#imaging_modal').off('hidden.bs.modal');
}
EnableButton("profile_delete_button");
return;
}
if (! imaging_modal_active && imaging_modal_display) {
sup.ShowModal("#imaging-modal");
imaging_modal_active = true;
imaging_modal_display = false;
// Handler so we know the user closed the modal.
$('#imaging_modal').on('hidden.bs.modal', function (e) {
imaging_modal_active = false;
$('#imaging_modal').off('hidden.bs.modal');
})
}
//
// Fill in the details.
//
if (! _.has(value, "node_status")) {
value["node_status"] = "unknown";
}
if (_.has(value, "image_size")) {
// We get KB to avoid overflow along the way.
value["image_size"] = filesize(value["image_size"] * 1024);
}
else {
value["image_size"] = "unknown";
}
$('#imaging_modal_node_status').html(value["node_status"]);
$('#imaging_modal_image_size').html(value["image_size"]);
if (_.has(value, "image_status")) {
var status = value["image_status"];
if (status == "imaging") {
$('#tracker-imaging').removeClass('progtrckr-todo');
$('#tracker-imaging').addClass('progtrckr-done');
}
if (status == "finishing") {
$('#tracker-imaging').removeClass('progtrckr-todo');
$('#tracker-imaging').addClass('progtrckr-done');
$('#tracker-finishing').removeClass('progtrckr-todo');
$('#tracker-finishing').addClass('progtrckr-done');
}
else if (status == "ready") {
$('#tracker-imaging').removeClass('progtrckr-todo');
$('#tracker-imaging').addClass('progtrckr-done');
$('#tracker-finishing').removeClass('progtrckr-todo');
$('#tracker-finishing').addClass('progtrckr-done');
$('#tracker-ready').removeClass('progtrckr-todo');
$('#tracker-ready').addClass('progtrckr-done');
$('#imaging-spinner').addClass("invisible");
EnableButtons();
return;
}
else if (status == "failed") {
$('#tracker-imaging').removeClass('progtrckr-todo');
$('#tracker-imaging').addClass('progtrckr-done');
$('#tracker-finishing').removeClass('progtrckr-todo');
$('#tracker-finishing').addClass('progtrckr-done');
$('#tracker-ready').removeClass('progtrckr-todo');
$('#tracker-ready').addClass('progtrckr-done');
$('#tracker-ready').html("Failed");
$('#imaging-spinner').addClass("invisible");
EnableButton("profile_delete_button");
return;
}
}
//
// Done, we need to do something here if we exited before
// ready or failed above.
//
if (_.has(value, "exited")) {
$('#imaging-spinner').addClass("invisible");
EnableButtons();
return;
}
// And check again in a little bit.
setTimeout(function f() { ShowProgressModal() }, 5000);
}
var $xmlthing = sup.CallServerMethod(ajaxurl,
"manage_profile",
"CloneStatus",
{"uuid" : uuid});
$xmlthing.done(callback);
ShowImagingModal(function()
{
return sup.CallServerMethod(ajaxurl,
"manage_profile",
"CloneStatus",
{"uuid" : uuid});
},
function(failed)
{
if (failed) {
EnableButton("profile_delete_button");
}
else {
EnableButtons();
}
});
}
//
......
......@@ -13,28 +13,6 @@ function HideModal(which)
$( which ).modal('hide');
}
function CallMethod(method, callback, uuid, arg)
{
return $.ajax({
// the URL for the request
url: window.location.href,
// the data to send (will be converted to a query string)
data: {
uuid: uuid,
ajax_request: 1,
ajax_method: method,
ajax_argument: arg,
},
// whether this is a POST or GET request
type: (arg ? "GET" : "GET"),
// the type of data we expect back
dataType : "json",
});
}
function CallServerMethod(url, route, method, args)
{
if (url == null) {
......@@ -366,16 +344,16 @@ function ConvertManifestToJSON(name, xml)
// Spit out the oops modal.
function SpitOops(id, msg)
{
var modal_name = "#" + id + "_modal";
var modal_text_name = "#" + id + "_text";
$(modal_text_name).html(msg);
ShowModal("#" + id);
ShowModal(modal_name);
}
// Exports from this module for use elsewhere
return {
ShowModal: ShowModal,
HideModal: HideModal,
CallMethod: CallMethod,
CallServerMethod: CallServerMethod,
ConvertManifestToJSON: ConvertManifestToJSON,
maketopmap: maketopmap,
......
window.APT_OPTIONS.config();
require(['underscore', 'js/quickvm_sup', 'moment',
require(['underscore', 'js/quickvm_sup', 'moment', 'js/image',
'js/lib/text!template/status.html',
'js/lib/text!template/waitwait-modal.html',
'js/lib/text!template/oops-modal.html',
......@@ -8,14 +8,18 @@ require(['underscore', 'js/quickvm_sup', 'moment',
'js/lib/text!template/terminate-modal.html',
'js/lib/text!template/extend-modal.html',
'js/lib/text!template/clone-help.html',
'js/lib/text!template/snapshot-help.html',
'tablesorter', 'tablesorterwidgets'],
function (_, sup, moment, statusString, waitwaitString, oopsString,
registerString, terminateString, extendString, cloneHelpString)
function (_, sup, moment, ShowImagingModal,
statusString, waitwaitString, oopsString,
registerString, terminateString, extendString,
cloneHelpString, snapshotHelpString)
{
'use strict';