Commit f4a9a9df authored by Leigh B Stoller's avatar Leigh B Stoller

User visible view of the Cluster Status page, which has been improved

upon to handle pre-reserves and reservations in a reasonable manner.
parent e7fad93e
<?php
#
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
# Copyright (c) 2000-2017 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -31,21 +31,26 @@ include_once("instance_defs.php");
#
function Do_GetStatus()
{
global $ajax_args, $geni_response_codes, $urn_mapping, $TBBASE;
global $TBSUEXEC_PATH;
global $ajax_args, $this_user, $geni_response_codes, $urn_mapping;
global $TBSUEXEC_PATH, $TBBASE, $TB_PROJECT_CREATEEXPT, $OURDOMAIN;
global $TB_PROJECT_READINFO, $DEFAULT_AGGREGATE_URN;
$isadmin = 0;
$autoextend_maxage = TBGetSiteVar("aptui/autoextend_maxage");
if (! (ISADMIN() || ISFOREIGN_ADMIN())) {
SPITAJAX_ERROR(1, "Not enough permission");
return;
# Admins get much more info.
if (ISADMIN() || ISFOREIGN_ADMIN()) {
$isadmin = 1;
}
if (Instance::ValidURN($ajax_args["cluster"])) {
$cluster = $ajax_args["cluster"];
}
else {
$cluster = $ajax_args["cluster"] . ".cm";
SPITAJAX_ERROR(1, "Not a valid cluster URN");
return;
}
$inuse = array();
$isus = ($cluster == $DEFAULT_AGGREGATE_URN ? 1 : 0);
$cluster = escapeshellarg($cluster);
$fp = popen("$TBSUEXEC_PATH elabman tbadmin ".
"webportal_xmlrpc -a $cluster InUse", "r");
$string = "";
......@@ -53,10 +58,25 @@ function Do_GetStatus()
$string .= fgets($fp, 1024);
}
pclose($fp);
$inuse = json_decode($string);
if (gettype($inuse) == "object") {
$inuse = $inuse->details;
$results = json_decode($string);
if (gettype($results) == "object") {
$inuse = $results->details;
if (property_exists($results, "typeinfo")) {
$typeinfo = $results->typeinfo;
}
}
else {
# Backwards compat?
$inuse = $results;
}
#
# Grab local project list for mere users, so we can use it to determine
# if they are in the same project as a reserved_pid, which means it is
# available to the user.
#
$projlist = $this_user->ProjectAccessList($TB_PROJECT_CREATEEXPT);
#
# We want to convert slice_urns to local instances so that we
......@@ -65,15 +85,23 @@ function Do_GetStatus()
#
$instances = array();
$maxttls = array();
$projects = array();
$typecounts = array();
$nodeinfo = array();
foreach ($inuse as $details) {
$info = array();
$type = $details->type;
if (!array_key_exists($type, $typecounts)) {
$typecounts[$type] = array("inuse" => 0,
"preres" => 0,
"free" => 0,
"free_preres" => 0);
if (isset($typeinfo) &&
property_exists($typeinfo, $type)) {
$typecounts[$type]["free_reserved"] =
$typeinfo->{$type}->freecount;
}
}
if ($details->reserved_pid != "") {
$typecounts[$type]["preres"] += 1;
......@@ -89,6 +117,14 @@ function Do_GetStatus()
$typecounts[$type]["free"] += 1;
}
}
$info["type"] = $type;
$info["node_id"] = $details->node_id;
#
# Find local instance if it exists.
#
$instance = null;
$maxttl = $details->maxttl;
if (property_exists($details, "slice_uuid") &&
$details->slice_uuid != "") {
......@@ -99,55 +135,149 @@ function Do_GetStatus()
}
else {
$instance = Instance::LookupBySlice($details->slice_uuid);
if (!$instance) {
continue;
if (! $instance) {
# Avoid not finding more then once.
$instance = null;
}
$instances[$details->slice_uuid] = $instance;
# Need the slice for this.
$slice = GeniSlice::Lookup("sa", $details->slice_uuid);
#
# We can generate the maxttl here, ignoring whatever the
# cluster might think. We need the slice though.
#
$maxttl = "";
if ($slice &&
!($instance->admin_lockdown() ||
$instance->user_lockdown())) {
$slice_expires = strtotime($slice->expires());
$slice_created = strtotime($slice->created());
$diff = $slice_expires - time();
$cdiff = time() - $slice_created;
if ($instance->extension_lockout() ||
($cdiff > (3600 * 24 * $autoextend_maxage))) {
$maxttl = $slice_expires - time();
}
else {
#
# This is the amount of free time the user can
# get, plus we add two days, since a denied extension
# automatically grants two days to avoid termination
# before we get a chance to look at it.
#
$maxttl = ($slice_created + (3600 * 24 * 2) +
(3600 * 24 * $autoextend_maxage)) - time();
if ($instance && $slice) {
#
# We can generate the maxttl here, ignoring whatever the
# cluster might think. We need the slice though.
#
if ($slice &&
!($instance->admin_lockdown() ||
$instance->user_lockdown())) {
$slice_expires = strtotime($slice->expires());
$slice_created = strtotime($slice->created());
$diff = $slice_expires - time();
$cdiff = time() - $slice_created;
if ($instance->extension_lockout() ||
($cdiff > (3600 * 24 * $autoextend_maxage))) {
$maxttl = $slice_expires - time();
}
else {
#
# This is the amount of free time the user can
# get, plus we add two days, since a denied
# extension automatically grants two days to
# avoid termination before we get a chance to look
# at it.
#
$maxttl = ($slice_created + (3600 * 24 * 2) +
(3600 * 24 * $autoextend_maxage)) - time();
}
}
}
else {
$maxttl = $details->maxttl;
}
$maxttls[$details->slice_uuid] = $maxttl;
}
}
#
# Find local project for a pre-reserve.
#
$reserved_project = null;
$lpid = null;
if (property_exists($details, "reserved_urn") &&
$details->reserved_urn != "" &&
Instance::ValidURN($details->reserved_urn)) {
list ($auth,$type,$id) = Instance::ParseURN($details->reserved_urn);
#
# Subdomain is project id.
#
list ($domain,$pid) = preg_split('/:/', $auth);
if ($domain == $OURDOMAIN && $pid && $pid != "") {
$lpid = $pid;
}
}
elseif ($details->reserved_pid != "" && $isus) {
# Local (classic) project.
$lpid = $details->reserved_pid;
}
if ($lpid) {
if (array_key_exists($lpid, $projects)) {
$reserved_project = $projects[$lpid];
}
else {
$reserved_project = Project::Lookup($lpid);
if ($reserved_project) {
$projects[$lpid] = $reserved_project;
}
else {
# Avoid repeated failed lookups.
$projects[$lpid] = $reserved_project = null;
}
}
}
if ($isadmin) {
if ($instance) {
$details->pid = $instance->pid();
$details->instance_uuid = $instance->uuid();
$details->instance_name = $instance->name();
$details->uid = $instance->creator();
$details->maxttl = $maxttl;
$info["pid"] = $instance->pid();
$info["uid"] = $instance->creator();
$info["instance_uuid"] = $instance->uuid();
$info["instance_name"] = $instance->name();
}
else {
$info["pid"] = $details->pid;
$info["uid"] = $details->uid;
}
$info["eid"] = $details->eid;
$info["ttl"] = $details->ttl;
$info["maxttl"] = $maxttl;
$info["reserved_pid"] = $details->reserved_pid;
}
else {
$available = 1;
if ($details->eid != "") {
$available = 0;
}
elseif ($details->reserved_pid != "" || $reserved_project) {
#
# Free but a pre-reserve. See if the user is a member of
# local project and thus can use the node.
#
if ($reserved_project) {
if (!$reserved_project->AccessCheck($this_user,
$TB_PROJECT_READINFO)) {
$available = 0;
}
}
else {
# Do not know anything, so not available;
$available = 0;
}
}
$info["available"] = $available;
#
# Adjust free counts for this type, see above.
#
if ($available &&
$details->pid == "" && $details->reserved_pid != "") {
$typecounts[$type]["free"] += 1;
# Reservation system ignores prereserved nodes.
$typecounts[$type]["free_reserved"] += 1;
}
}
$nodeinfo[] = $info;
}
$dashboard["totals"] = $typecounts;
$dashboard["inuse"] = $inuse;
SPITAJAX_RESPONSE($dashboard);
#
# If the reservation system says that the number of free nodes is
# different then what we calculated, put that in parens.
#
$blob = array();
$blob["totals"] = $typecounts;
$blob["inuse"] = $nodeinfo;
SPITAJAX_RESPONSE($blob);
}
function Do_GetPreReservations()
......
<?php
#
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
# Copyright (c) 2000-2017 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -38,16 +38,13 @@ $this_user = CheckLoginOrRedirect();
$isadmin = (ISADMIN() ? 1 : 0);
$isfadmin = (ISFOREIGN_ADMIN() ? 1 : 0);
if (! (ISADMIN() || ISFOREIGN_ADMIN())) {
SPITUSERERROR("You do not have permission to view this page!");
}
SPITHEADER(1);
#
# The apt_aggregates table should tell us what clusters, but for
# now it is always the local cluster
#
if ($TBMAINSITE && !$ISEMULAB) {
if ($TBMAINSITE && $ISCLOUD) {
$aggregates =
array("Emulab" => "urn:publicid:IDN+emulab.net+authority+cm",
"APT" => "urn:publicid:IDN+apt.emulab.net+authority+cm",
......
......@@ -14,11 +14,12 @@ $(function ()
function initialize()
{
window.APT_OPTIONS.initialize(sup);
isadmin = window.ISADMIN;
isadmin = window.ISADMIN || window.ISFADMIN;
amlist = JSON.parse(_.unescape($('#agglist-json')[0].textContent));
var html = mainTemplate({
amlist: amlist,
"amlist" : amlist,
"isadmin" : isadmin,
});
$('#page-body').html(html);
......@@ -36,7 +37,7 @@ $(function ()
{
_.each(amlist, function(urn, name) {
var callback = function(json) {
//console.log(json);
console.log(json);
if (json.code) {
console.log("Could not get cluster data: " + json.value);
return;
......@@ -44,57 +45,88 @@ $(function ()
var inuse = json.value.inuse;
var html = "";
inuse.forEach(function(value, index) {
_.each(inuse, function(value, name) {
var type = "";
if (_.has(value, "type")) {
type = value.type;
}
var expires = "";
if (_.has(value, "ttl")) {
var ttl = value.ttl;
if (ttl != "") {
expires = moment().add(ttl, 'seconds').fromNow();
html = html + "<tr>" +
"<td>" + value.node_id + "</td>" +
"<td>" + type + "</td>";
if (isadmin) {
var expires = "";
if (_.has(value, "ttl")) {
var ttl = value.ttl;
if (ttl != "") {
expires = moment()
.add(ttl, 'seconds').fromNow();
}
}
}
var allowed = "";
if (_.has(value, "maxttl")) {
var maxttl = value.maxttl;
if (maxttl != "") {
allowed = moment().add(maxttl, 'seconds').fromNow();
var allowed = "";
if (_.has(value, "maxttl")) {
var maxttl = value.maxttl;
if (maxttl != "") {
allowed = moment()
.add(maxttl, 'seconds').fromNow();
}
}
var uid = "";
if (_.has(value, "uid")) {
uid = value.uid;
}
var eid = "";
if (_.has(value, "eid")) {
eid = value.eid;
if (_.has(value, "instance_uuid")) {
var uuid = value.instance_uuid;
eid = "<a href='status.php?uuid=" + uuid +
"' target=_blank>" +
value.instance_name + "</a>";
}
}
html = html +
"<td>" + value.pid + "</td>" +
"<td>" + eid + "</td>" +
"<td>" + uid + "</td>" +
"<td>" + expires + "</td>" +
"<td>" + allowed + "</td>" +
"<td>" + value.reserved_pid + "</td>";
}
var uid = "";
if (_.has(value, "uid")) {
uid = value.uid;
}
var eid = value.eid;
if (_.has(value, "instance_uuid")) {
var uuid = value.instance_uuid;
eid = "<a href='status.php?uuid=" + uuid +
"' target=_blank>" + value.instance_name + "</a>";
else {
if (value.available) {
html = html + "<td>Yes</td>";
}
else {
html = html + "<td>No</td>";
}
}
html = html + "<tr>" +
"<td>" + value.node_id + "</td>" +
"<td>" + type + "</td>" +
"<td>" + value.pid + "</td>" +
"<td>" + eid + "</td>" +
"<td>" + uid + "</td>" +
"<td>" + expires + "</td>" +
"<td>" + allowed + "</td>" +
"<td>" + value.reserved_pid + "</td>" + "</tr>";
html = html + "</tr>";
});
$('#' + name + '-tbody').html(html);
InitTable(name);
// These are the totals.
html = countsTemplate({"totals" : json.value.totals});
html = countsTemplate({"totals" : json.value.totals,
"isadmin": isadmin});
$('#counts-panel-' + name).html(html);
// This activates the tooltip subsystem.
$('#counts-panel-' + name + ' ' +
'[data-toggle="tooltip"]').tooltip({
delay: {"hide" : 500, "show" : 150},
placement: 'auto',
});
// We reference the totals table in InitTable();
InitTable(name);
}
var xmlthing = sup.CallServerMethod(null, "cluster-status",
"GetStatus",
{"cluster" : urn});
xmlthing.done(callback);
});
if (!isadmin) {
return;
}
_.each(amlist, function(urn, name) {
var callback = function(json) {
console.log(json);
......@@ -195,13 +227,16 @@ $(function ()
// Allows using filter_liveSearch or delayed search &
// pressing escape to cancel the search
$.tablesorter.filter.bindSearch(table, $(searchname));
$.tablesorter.filter.bindSearch(table, $('#inuse-search-all'));
$(tablename).removeClass("hidden");
if (window.ISCLOUD) {
$.tablesorter.filter.bindSearch(table, $('#inuse-search-all'));
}
/*
* This is the expand/collapse button for an individual table.
*/
$('#inuse-collapse-button-' + name).click(function () {
$('#inuse-collapse-button-' + name).click(function (event) {
event.preventDefault();
if ($(panelname).data("status") == "minimized") {
$(panelname).removeClass("inuse-panel");
$('#counts-panel-' + name).removeClass("counts-panel");
......@@ -217,6 +252,20 @@ $(function ()
$(this).addClass("glyphicon-chevron-right");
}
})
// Only one, expand immediately.
if (Object.keys(amlist).length == 1) {
$('#inuse-collapse-button-' + name).click();
}
$(tablename).removeClass("hidden");
// Bind type column in the counts table to initiating search
$('#counts-panel-' + name + ' .counts-type').click(function(event) {
event.preventDefault();
$(tablename +
' input.tablesorter-filter.form-control[data-column="1"]')
.val($(event.target).text());
table.trigger('search', false);
});
}
$(document).ready(initialize);
......
......@@ -377,7 +377,8 @@ echo "
<li><a href='instantiate.php'>Start Experiment</a></li>
<li><a href='manage_profile.php'>Create Experiment Profile</a></li>
<li><a href='reserve.php'>Reserve Nodes</a></li>
";
<li><a href='cluster-status.php'>Cluster Status</a></li>
";
echo " <li class='divider'></li>
<li><a href='user-dashboard.php#experiments'>
My Experiments</a></li>
......
......@@ -5,18 +5,48 @@
<tr>
<th>Type</th>
<th>Inuse</th>
<th>Pre</th>
<th>Free</th>
<% if (isadmin) { %>
<th>Pre
<a href='#' class='btn btn-xs'
data-toggle='tooltip'
data-container="body"
data-trigger="hover"
title='Number preserved AND free in parens'
style="margin-bottom: 3px;
padding-left: 0px;
padding-right: 0px;">
<span class='glyphicon glyphicon-question-sign'></span></a></th>
<% } %>
<th>Free
<a href='#' class='btn btn-xs'
data-toggle='tooltip'
data-container="body"
data-trigger="hover"
title='If the reservation system determines that some free
nodes will soon be unavailble, the adjusted number is
shown in parens.'
style="margin-bottom: 3px;
padding-left: 0px;
padding-right: 0px;">
<span class='glyphicon glyphicon-question-sign'></span></a></th>
</th>
</tr>
</thead>
<tbody>
<% _.each(totals, function(value, type) { %>
<tr>
<td><%- type %></td>
<td><a href="#" class="counts-type"><%- type %></a></td>
<td><%- value.inuse %></td>
<td><%- value.preres %></td>
<td><%- value.free %>
<% if (isadmin) { %>
<td><%- value.preres %>
<% if (value.free_preres) { %>(<%- value.free_preres %>)<% } %>
</td>
<% } %>
<td><%- value.free %>
<% if (_.has(value, "free_reserved") &&
value.free != value.free_reserved) {%>
(<%- value.free_reserved %>)
<% } %>
</td>
</tr>
<% }); %>
......
......@@ -36,19 +36,31 @@
padding: 0 !important;
}
</style>
<div class='row' style="margin-bottom: 5px">
<div class='col-sm-8 col-sm-offset-3'>
<input class='form-control search'
type='search' data-column='all'
id='inuse-search-all' placeholder='Global Search'>
<% if (window.ISCLOUD) { %>
<div class='row' style="margin-bottom: 5px">
<div
<% if (isadmin) { %>
class='col-sm-8 col-sm-offset-3'
<% } else { %>
class='col-sm-5 col-sm-offset-5'
<% } %> >
<input class='form-control search'
type='search' data-column='all'
id='inuse-search-all' placeholder='Global Search'>
</div>
</div>
</div>
<% } %>
<% _.each(amlist, function(urn, name) { %>
<div class='row'>
<div class='col-sm-3'>
<div
<% if (isadmin) { %>
class='col-sm-3'
<% } else { %>
class='col-sm-3 col-sm-offset-2'
<% } %> >
<div class='panel panel-default'>