Commit 108d3d6f authored by Leigh Stoller's avatar Leigh Stoller

New page and support to display a historical experiment using the

manifest we have store in the DB. Basically a highly stripped down
version of the status page.
parent 2102dbce
......@@ -821,6 +821,133 @@ class Instance
}
}
class InstanceHistory
{
var $record;
var $slivers;
#
# Constructor by lookup on unique index.
#
function InstanceHistory($uuid) {
$safe_uuid = addslashes($uuid);
$query_result =
DBQueryWarn("select h.*,f.exitmessage,f.exitcode ".
" from apt_instance_history as h ".
"left join apt_instance_failures as f ".
" on f.uuid=h.uuid ".
"where h.uuid='$safe_uuid'");
if (!$query_result || !mysql_num_rows($query_result)) {
$this->record = null;
return;
}
$this->record = mysql_fetch_array($query_result);
#
# Get the list of aggregate records. Early records do not have one.
#
$query_result =
DBQueryWarn("select * from apt_instance_aggregate_history ".
"where uuid='$uuid'");
if (!$query_result) {
$this->record = null;
return;
}
if (!mysql_num_rows($query_result)) {
$this->slivers = array(
array("uuid" => $this->record["uuid"],
"name" => $this->record["name"],
"aggregate_urn" => $this->record["aggregate_urn"],
"status" => $this->record["status"],
"public_url" => $this->record["public_url"],
"manifest" => $this->record["manifest"],
));
}
else {
$this->slivers = array();
while ($row = mysql_fetch_array($query_result)) {
$this->slivers[] = $row;
}
}
}
# accessors
function slivers() { return $this->slivers; }
function field($name) {
return (is_null($this->record) ? -1 : $this->record[$name]);
}
function uuid() { return $this->field('uuid'); }
function name() { return $this->field('name'); }
function profile_id() { return $this->field('profile_id'); }
function profile_version() { return $this->field('profile_version'); }
function slice_uuid() { return $this->field('slice_uuid'); }
function creator() { return $this->field('creator'); }
function creator_idx() { return $this->field('creator_idx'); }
function creator_uuid() { return $this->field('creator_uuid'); }
function pid() { return $this->field('pid'); }
function pid_idx() { return $this->field('pid_idx'); }
function gid() { return $this->field('gid'); }
function gid_idx() { return $this->field('gid_idx'); }
function aggregate_urn(){ return $this->field('aggregate_urn'); }
function public_url() { return $this->field('public_url'); }
function logfileid() { return $this->field('logfileid'); }
function created() { return $this->field('created'); }
function destroyed() { return $this->field('destroyed'); }
function expired() { return $this->field('expired'); }
function extension_count() { return $this->field('extension_count'); }
function extension_days() { return $this->field('extension_days'); }
function extension_hours() { return $this->field('extension_hours'); }
function physnode_count() { return $this->field('physnode_count'); }
function virtnode_count() { return $this->field('virtnode_count'); }
function servername() { return $this->field('servername'); }
function repourl() { return $this->field('repourl'); }
function reporef() { return $this->field('reporef'); }
function repohash() { return $this->field('repohash'); }
function rspec() { return $this->field('rspec'); }
function script() { return $this->field('script'); }
function params() { return $this->field('params'); }
function manifest() { return $this->field('manifest'); }
function IsAPT() {
return preg_match('/aptlab/', $this->servername());
}
function IsCloud() {
return preg_match('/cloudlab/', $this->servername());
}
function IsPNet() {
return preg_match('/phantomnet/', $this->servername());
}
# Hmm, how does one cause an error in a php constructor?
function IsValid() {
return !is_null($this->record);
}
# Lookup up an instance by uuid
function Lookup($uuid) {
$foo = new InstanceHistory($uuid);
if ($foo->IsValid()) {
# Insert into cache.
return $foo;
}
return null;
}
function LookupBySlice($slice_uuid)
{
$safe_uuid = addslashes($slice_uuid);
$query_result =
DBQueryWarn("select uuid from apt_instance_history ".
"where slice_uuid='$safe_uuid'");
if (!$query_result || !mysql_num_rows($query_result)) {
return null;
}
$row = mysql_fetch_array($query_result);
return InstanceHistory::Lookup($row[0]);
}
}
class InstanceSliver
{
var $sliver;
......
$(function ()
{
'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['memlane',
'waitwait-modal',
'oops-modal']);
var mainTemplate = _.template(templates['memlane']);
var EMULAB_NS = "http://www.protogeni.net/resources/rspec/ext/emulab/1";
var amlist = null;
function initialize()
{
window.APT_OPTIONS.initialize(sup);
amlist = decodejson('#amlist-json');
var xmlthing = sup.CallServerMethod(null, "memlane",
"HistoryRecord",
{"uuid" : window.uuid});
xmlthing.done(function (json) {
GeneratePageBody(json);
});
}
function GeneratePageBody(json)
{
console.info("GeneratePageBody", json);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
$('#page-body').html(mainTemplate({
"record" : json.value,
}));
// Format dates with moment before display.
$('.format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("lll"));
}
});
$('#waitwait_div').html(templates['waitwait-model']);
$('#oops_div').html(templates['oops-model']);
if (json.value.exitcode) {
ShowError(json.value);
}
else {
ShowTopo(json.value);
}
}
var listview_row =
"<tr id='listview-row'>" +
" <td name='client_id'>n/a</td>" +
" <td name='node_id'>n/a</td>" +
" <td name='type'>n/a</td>" +
" <td name='image'>n/a</td>" +
"</tr>";
//
// Show the topology inside the topo container. Called from the status
// watchdog and the resize wachdog. Replaces the current topo drawing.
//
function ShowTopo(record)
{
//
// Process the nodes in a single manifest.
//
var ProcessNodes = function(aggregate_urn, xml) {
var rawcount = $(xml).find("node, emulab\\:vhost").length;
// Find all of the nodes, and put them into the list tab.
// Clear current table.
$(xml).find("node, emulab\\:vhost").each(function() {
// Only nodes that match the aggregate being processed,
// since we send the same rspec to every aggregate.
var manager_urn = $(this).attr("component_manager_id");
if (!manager_urn.length || manager_urn != aggregate_urn) {
return;
}
var tag = $(this).prop("tagName");
var isvhost= (tag == "emulab:vhost" ? 1 : 0);
var node = $(this).attr("client_id");
var stype = $(this).find("sliver_type");
var vnode = this.getElementsByTagNameNS(EMULAB_NS, 'vnode');
var isfw = 0;
var clone = $(listview_row);
// Change the ID of the clone so its unique.
clone.attr('id', 'listview-row-' + node);
// Set the client_id in the first column.
clone.find(" [name=client_id]").html(node);
// And the node_id/type. This is an emulab extension.
if (vnode.length) {
var node_id = $(vnode).attr("name");
// Admins get a link to the shownode page.
if (window.isadmin) {
var weburl = amlist[aggregate_urn].weburl +
"/shownode.php3?node_id=" + node_id;
var html = "<a href='" + weburl + "' target=_blank>" +
node_id + "</a>";
clone.find(" [name=node_id]").html(html);
}
else {
clone.find(" [name=node_id]").html(node_id);
}
clone.find(" [name=type]")
.html($(vnode).attr("hardware_type"));
}
// Convenience.
clone.find(" [name=select]").attr("id", node);
if (stype.length &&
$(stype).attr("name") === "emulab-blockstore") {
clone.find(" [name=menu]").text("n/a");
return;
}
if (stype.length &&
$(stype).attr("name") === "firewall") {
isfw = 1;
}
/*
* Find the disk image (if any) for the node and display
* in the listview.
*/
if (vnode.length && $(vnode).attr("disk_image")) {
clone.find(" [name=image]")
.html($(vnode).attr("disk_image"));
}
else if (stype.length) {
var dimage = $(stype).find("disk_image");
if (dimage.length) {
var name = $(dimage).attr("name");
if (name) {
var hrn = sup.ParseURN(name);
if (hrn && hrn.type == "image") {
var id = hrn.project + "/" + hrn.image;
if (hrn.version != null) {
id = id + ":" + hrn.version;
}
clone.find(" [name=image]").html(id);
}
}
}
}
// Insert into the table, we will attach the handlers below.
$('#listview_table > tbody:last').append(clone);
});
}
var slivers = record.slivers;
var manifests = [];
var logfiles = [];
_.each(slivers, function(sliver) {
var manifest = sliver.manifest;
var aggregate_urn = sliver.aggregate_urn;
manifests.push(manifest);
if (sliver.public_url) {
logfiles.push({"urn" : sliver.aggregate_urn,
"url" : sliver.public_url});
}
var xmlDoc = $.parseXML(manifest);
ProcessNodes(aggregate_urn, $(xmlDoc));
});
$("#showtopo_container").removeClass("invisible");
$('#quicktabs_ul a[href="#topology"]').tab('show');
ShowRspec(record.rspec);
ShowViewer('#showtopo_statuspage', manifests);
if (logfiles.length) {
ShowLogfiles(logfiles);
}
}
var jacksInstance;
var jacksInput;
var jacksOutput;
var jacksRspecs;
function ShowViewer(divname, manifests)
{
var first_manifest = _.first(manifests);
var rest = _.rest(manifests);
var multisite = rest.length ? true : false;
if (! jacksInstance)
{
jacksInstance = new window.Jacks({
mode: 'viewer',
source: 'rspec',
multiSite: multisite,
root: divname,
nodeSelect: true,
readyCallback: function (input, output) {
jacksInput = input;
jacksOutput = output;
jacksOutput.on('modified-topology', function (object) {
//console.log("jacksIDs", object, jacksIDs);
ShowManifest(object.rspec);
});
jacksInput.trigger('change-topology',
[{ rspec: first_manifest }]);
if (rest.length) {
_.each(rest, function(manifest) {
jacksInput.trigger('add-topology',
[{ rspec: manifest }]);
});
}
},
canvasOptions: {
"aggregates": [
{
"id": "urn:publicid:IDN+utah.cloudlab.us+authority+cm",
"name": "Cloudlab Utah"
},
{
"id": "urn:publicid:IDN+wisc.cloudlab.us+authority+cm",
"name": "Cloudlab Wisconsin"
},
{
"id": "urn:publicid:IDN+clemson.cloudlab.us+authority+cm",
"name": "Cloudlab Clemson"
},
{
"id": "urn:publicid:IDN+utahddc.geniracks.net+authority+cm",
"name": "IG UtahDDC"
},
{
"id": "urn:publicid:IDN+apt.emulab.net+authority+cm",
"name": "APT Utah"
},
{
"id": "urn:publicid:IDN+emulab.net+authority+cm",
"name": "Emulab"
},
{
"id": "urn:publicid:IDN+wall2.ilabt.iminds.be+authority+cm",
"name": "iMinds Virt Wall 2"
},
{
"id": "urn:publicid:IDN+uky.emulab.net+authority+cm",
"name": "UKY Emulab"
}
]
},
show: {
rspec: false,
tour: false,
version: false,
selectInfo: true,
menu: false
}
});
}
else if (jacksInput)
{
jacksInput.trigger('change-topology',
[{ rspec: first_manifest }]);
if (rest.length) {
_.each(rest, function(manifest) {
jacksInput.trigger('add-topology',
[{ rspec: manifest }]);
});
}
}
}
//
// Show the manifest in the tab, using codemirror.
//
function ShowManifest(manifest)
{
var mode = "text/xml";
$("#manifest_textarea").css("height", "300");
$('#manifest_textarea .CodeMirror').remove();
var myCodeMirror = CodeMirror(function(elt) {
$('#manifest_textarea').prepend(elt);
}, {
value: manifest,
lineNumbers: false,
smartIndent: true,
mode: mode,
readOnly: true,
});
$('#show_manifest_tab').on('shown.bs.tab', function (e) {
myCodeMirror.refresh();
});
}
//
// Show the rspec in the tab, using codemirror.
//
function ShowRspec(rspec)
{
var mode = "text/xml";
$("#rspec_textarea").css("height", "300");
$('#rspec_textarea .CodeMirror').remove();
var myCodeMirror = CodeMirror(function(elt) {
$('#rspec_textarea').prepend(elt);
}, {
value: rspec,
lineNumbers: false,
smartIndent: true,
mode: mode,
readOnly: true,
});
$('#show_rspec_tab').on('shown.bs.tab', function (e) {
myCodeMirror.refresh();
});
}
function ShowLogfiles(logfiles)
{
$('#sliverinfo_dropdown').change(function (event) {
var selected =
$('#sliverinfo_dropdown select option:selected').val();
console.info(selected);
// Find the URL
_.each(logfiles, function(obj) {
var url = obj.url;
var name = amlist[obj.urn].name;
if (name == selected) {
$("#sliverinfo_dropdown a").attr("href", url);
}
});
});
if (logfiles.length == 1) {
$("#sliverinfo_button").attr("href", logfiles[0].url);
$("#sliverinfo_button").removeClass("hidden");
$("#sliverinfo_dropdown").addClass("hidden");
return;
}
// Selection list.
_.each(logfiles, function(obj) {
var url = obj.url;
var name = amlist[obj.urn].name;
$("#sliverinfo_dropdown select").append(
"<option value='" + name + "'>" + name + "</option>");
});
$("#sliverinfo_button").addClass("hidden");
$("#sliverinfo_dropdown").removeClass("hidden");
}
function ShowError(record)
{
var slivers = record.slivers;
var logfiles = [];
_.each(slivers, function(sliver) {
var aggregate_urn = sliver.aggregate_urn;
if (sliver.public_url) {
logfiles.push({"urn" : sliver.aggregate_urn,
"url" : sliver.public_url});
}
});
if (logfiles.length) {
ShowLogfiles(logfiles);
}
$('#error_panel_text').text(record.exitmessage);
$('#error_panel').removeClass("hidden");
}
// Helper.
function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent));
}
$(document).ready(initialize);
});
<?php
#
# Copyright (c) 2000-2018 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/>.
#
# }}}
#
chdir("..");
include_once("webtask.php");
include_once("geni_defs.php");
chdir("apt");
include_once("profile_defs.php");
include_once("instance_defs.php");
#
# Return info about a previous experiment.
#
function Do_HistoryRecord()
{
global $this_user, $ajax_args;
if (!isset($ajax_args["uuid"])) {
SPITAJAX_ERROR(1, "Missing uuid");
return;
}
$uuid = $ajax_args["uuid"];
if (!IsValidUUID($uuid)) {
SPITAJAX_ERROR(1, "Not a valid UUID: $uuid");
return;
}
$record = InstanceHistory::Lookup($uuid);
if (!$record) {
SPITAJAX_ERROR(1, "No such record: $uuid");
return;
}
if (! (ISADMIN() || ISFOREIGN_ADMIN() || $record->CanView($this_user))) {
SPITAJAX_ERROR(1, "You do not have permission to view this page!");
return;
}
$blob = $record->record;
$blob["slivers"] = $record->slivers();
#
# Need to add a few things.
#
$blob["created"] = DateStringGMT($blob["created"]);
$blob["destroyed"] = DateStringGMT($blob["destroyed"]);
$blob["profile_uuid"] = null;
$blob["profile_name"] = null;
if ($profile = Profile::Lookup($blob["profile_id"],
$blob["profile_version"])) {
$blob["profile_name"] = $profile->name() . ":" . $profile->version();
$blob["profile_uuid"] = $profile->uuid();
}
# Need to munge the urls since these slices are history.
foreach ($blob["slivers"] as &$sliver) {
$url = $sliver["public_url"];
if ($url && preg_match("/publicid=\w*/", $url)) {
$url = "https://" . parse_url($url, PHP_URL_HOST) .
"/showslicelogs.php?slice_uuid=" . $record->slice_uuid();
$sliver["public_url"] = $url;
}
}
SPITAJAX_RESPONSE($blob);
}
# Local Variables:
# mode:php
# End:
?>
<?php
#
# Copyright (c) 2000-2018 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/>.
#
# }}}
#
chdir("..");
include("defs.php3");
include_once("geni_defs.php");
chdir("apt");
include("quickvm_sup.php");
$page_title = "Experiment Record";
#
# Get current user.
#
$this_user = CheckLogin($check_status);
if (isset($this_user)) {
CheckLoginOrDie();
}
else {
RedirectLoginPage();
}
#
# We do not set the isfadmin flag if the user has normal permission
# to see this experiment, since that would change what the user sees.
# Okay for real admins, but not for foreign admins.
#
$isfadmin = (ISFOREIGN_ADMIN() ? 1 : 0);
$isadmin = (ISADMIN() ? 1 : 0);
#
# Verify page arguments.
#
$reqargs = OptionalPageArguments("slice_uuid", PAGEARG_UUID,
"uuid", PAGEARG_UUID);
if (! (isset($slice_uuid) || isset($uuid))) {
SPITHEADER(1);
echo "<div class='align-center'>
<p class='lead text-center'>
What experiment record would you like to look at?
</p>
</div>\n";
SPITNULLREQUIRE();
SPITFOOTER();
return;
}
#
# See if the record exists.
#
if (isset($uuid)) {
$record = InstanceHistory::Lookup($uuid);
}
else {
$record = InstanceHistory::LookupbySlice($slice_uuid);
}
if (!$record) {
SPITHEADER(1);
echo "<div class='align-center'>
<p class='lead text-center'>
Experiment record does not exist.
</p>
</div>\n";
SPITNULLREQUIRE();
SPITFOOTER();
return;
}
$uuid = $record->uuid();
if (! (ISADMIN() || ISFOREIGN_ADMIN() || $record->CanView($this_user))) {
PAGEERROR("You do not have permission to look at this experiment!");
}
SPITHEADER(1);
# Place to hang the toplevel template.
echo "<div id='page-body'></div>\n";
#
# Build up a blob of aggregates info used by this experiment.
#
$blob = array();
foreach ($record->slivers as $sliver) {
$aggregate_urn = $sliver["aggregate_urn"];
$aggregate = Aggregate::Lookup($aggregate_urn);
$weburl = $aggregate->weburl();
$blob[$aggregate_urn] = array("weburl" => $weburl,
"name" => $aggregate->name());
}
echo "<script type='text/plain' id='amlist-json'>\n";
echo json_encode($blob, JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_TAG|JSON_HEX_AMP);
echo "</script>\n";
echo "<script type='text/javascript'>\n";
echo " window.uuid = '" . $uuid . "';\n";
echo " window.isadmin = $isadmin;\n";