Commit 36c411bd authored by Leigh Stoller's avatar Leigh Stoller

Checkpoint new reservations UI.

parent 5fd70e57
......@@ -32,13 +32,13 @@ SUBDIRS =
BIN_SCRIPTS = manage_profile manage_instance manage_dataset \
create_instance rungenilib ns2rspec nsgenilib.py \
rspec2genilib ns2genilib
rspec2genilib ns2genilib manage_reservations
SBIN_SCRIPTS = apt_daemon aptevent_daemon portal_xmlrpc apt_checkup
LIB_SCRIPTS = APT_Profile.pm APT_Instance.pm APT_Dataset.pm APT_Geni.pm \
APT_Aggregate.pm APT_Utility.pm
WEB_BIN_SCRIPTS = webmanage_profile webmanage_instance webmanage_dataset \
webcreate_instance webrungenilib webns2rspec webns2genilib \
webrspec2genilib
webrspec2genilib webmanage_reservations
WEB_SBIN_SCRIPTS= webportal_xmlrpc
LIBEXEC_SCRIPTS = $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS)
USERLIBEXEC = rungenilib.proxy genilib-jail genilib-iocage
......
This diff is collapsed.
This diff is collapsed.
......@@ -246,6 +246,9 @@ ProtoGeniDefs::AddModule("cluster",
"SliceOpenstackData" => \&GeniCluster::SliceOpenstackData,
"SliceCheckReservation"=> \&GeniCluster::SliceCheckReservation,
"SliceMaxExtension" => \&GeniCluster::SliceMaxExtension,
"Reserve" => \&GeniCluster::Reserve,
"Reservations" => \&GeniCluster::Reservations,
"DeleteReservation" => \&GeniCluster::DeleteReservation,
}},
});
......
......@@ -51,6 +51,9 @@ $GENI_METHODS = {
"SliceOpenstackData" => \&GeniCluster::SliceOpenstackData,
"SliceCheckReservation"=> \&GeniCluster::SliceCheckReservation,
"SliceMaxExtension" => \&GeniCluster::SliceMaxExtension,
"Reserve" => \&GeniCluster::Reserve,
"Reservations" => \&GeniCluster::Reservations,
"DeleteReservation" => \&GeniCluster::DeleteReservation,
};
1;
......
......@@ -23,9 +23,13 @@
#
#
#
# This needs to go into the DB.
#
class Aggregate
{
var $aggregate;
var $typeinfo;
#
# Constructor by lookup by urn
......@@ -41,6 +45,19 @@ class Aggregate
return;
}
$this->aggregate = mysql_fetch_array($query_result);
$this->typeinfo = array();
#
# Get the type info
#
$query_result =
DBQueryWarn("select * from apt_aggregate_nodetypes ".
"where urn='$safe_urn'");
while ($row = mysql_fetch_array($query_result)) {
$type = $row["type"];
$this->typeinfo[$type] = array("count" => $row["count"],
"free" => $row["free"]);
}
}
# accessors
function field($name) {
......@@ -52,6 +69,7 @@ class Aggregate
function abbreviation() { return $this->field('abbreviation'); }
function weburl() { return $this->field('weburl'); }
function has_datasets() { return $this->field('has_datasets'); }
function reservations() { return $this->field('reservations'); }
function isfederate() { return $this->field('isfederate'); }
function portals() { return $this->field('portals'); }
......@@ -70,6 +88,22 @@ class Aggregate
return null;
}
function LookupByNickname($nickname) {
$safe_nickname = addslashes($nickname);
$query_result =
DBQueryWarn("select urn from apt_aggregates ".
"where nickname='$safe_nickname'");
if (!$query_result || !mysql_num_rows($query_result)) {
return null;
}
$row = mysql_fetch_array($query_result);
$urn = $row['urn'];
return Aggregate::Lookup($urn);
}
#
# Generate the free nodes URL from the web url.
#
......@@ -99,6 +133,30 @@ class Aggregate
return $result;
}
#
# Return a list of aggregates supporting reservations,
#
function SupportsReservations() {
$result = array();
global $PORTAL_GENESIS;
$query_result =
DBQueryFatal("select urn from apt_aggregates ".
"where disabled=0 and reservations=1 and ".
" FIND_IN_SET('$PORTAL_GENESIS', portals)");
while ($row = mysql_fetch_array($query_result)) {
$urn = $row["urn"];
if (! ($aggregate = Aggregate::Lookup($urn))) {
TBERROR("Aggregate::SupportsReservations: ".
"Could not load aggregate $urn!", 1);
}
$result[] = $aggregate;
}
return $result;
}
#
# Return the list of allowed aggregates based on the portal in use.
#
......
......@@ -53,12 +53,16 @@ $(function () {
if (item.dataset) {
var key = item.dataset['key'];
var margin = 15;
var colsize = 12;
var colsize = null;
// Squeeze vertical space for this field.
if (_.has(item.dataset, "compact")) {
margin = 5;
}
// Column size per row,
if (_.has(item.dataset, "colsize")) {
colsize = item.dataset['colsize'];;
}
/*
* Wrap in a div we can name. We assume the form
......@@ -94,7 +98,9 @@ $(function () {
}
label_text = label_text + "</label>";
wrapper.append($(label_text));
colsize = (wide ? 9 : 6);
if (!colsize) {
colsize = (wide ? 9 : 6);
}
}
var innerdiv =
$("<div class='col-sm-" + colsize + "'></div>");
......@@ -148,20 +154,20 @@ $(function () {
* the page. Only allows a single form, but that would be easy
* to change if we needed it.
*/
var form_modified = false;
function EnableUnsavedWarning(form, modified_callback) {
var modified = false;
$(form + ' :input').change(function () {
console.info("changed");
//console.info("changed");
if (modified_callback) {
modified_callback();
}
modified = true;
form_modified = true;
});
// Warn user if they have not saved changes.
window.onbeforeunload = function() {
if (! modified)
if (! form_modified)
return null;
return "You have unsaved changes!";
}
......@@ -169,6 +175,9 @@ $(function () {
function DisableUnsavedWarning(form) {
window.onbeforeunload = null;
}
function MarkFormUnsaved() {
form_modified = true;
}
function ClearFormErrors(form) {
$(form).find(".format-me").each(function () {
......@@ -183,6 +192,19 @@ $(function () {
}
}
});
$('#general_error').html("");
}
/*
* Update a form contents from an array.
*/
function UpdateForm(form, formfields) {
_.each(formfields, function(value, name) {
$(form).find("[name=" + name + "]").each(function () {
console.log(this, this.type);
$(this).val(value);
});
});
}
/*
......@@ -202,7 +224,7 @@ $(function () {
ClearFormErrors(form);
var checkonly_callback = function(json) {
console.info(json);
console.info("CheckForm", json);
/*
* We deal with these errors, the caller handles other errors.
......@@ -236,8 +258,8 @@ $(function () {
formfields[field.name] = field.value;
});
var submit_callback = function(json) {
console.info(json);
sup.HideModal("#waitwait-modal");
console.info("SubmitForm", json);
sup.HideWaitWait();
DisableUnsavedWarning(form);
callback(json);
};
......@@ -260,6 +282,8 @@ $(function () {
"GenerateFormErrors" : GenerateFormErrors,
"EnableUnsavedWarning" : EnableUnsavedWarning,
"DisableUnsavedWarning" : DisableUnsavedWarning,
"MarkFormUnsaved" : MarkFormUnsaved,
"UpdateForm" : UpdateForm,
};
}
)();
......
$(function ()
{
'use strict';
var template_list = ["reservation-list", "oops-modal", "confirm-modal",
"waitwait-modal"];
var templates = APT_OPTIONS.fetchTemplateList(template_list);
var listTemplate = _.template(templates["reservation-list"]);
var confirmString = templates["confirm-modal"];
var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"];
var amlist = null;
function initialize()
{
window.APT_OPTIONS.initialize(sup);
amlist = decodejson('#amlist-json');
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
$('#confirm_div').html(confirmString);
LoadData();
}
/*
* Load reservations from each am in the list and generate a table.
*/
function LoadData()
{
var count = Object.keys(amlist).length;
_.each(amlist, function(urn, name) {
var callback = function(json) {
console.log(json);
// Kill the spinner.
count--;
if (count <= 0) {
$('#spinner').addClass("hidden");
}
if (json.code) {
console.log("Could not get reservation data for " +
name + ": " + json.value);
return;
}
var reservations = json.value;
if (reservations.length == 0)
return;
// Generate the main template.
var html = listTemplate({
"reservations" : reservations,
"showidx" : true,
"showproject" : true,
"showuser" : true,
"name" : name,
});
html =
"<div class='row' id='" + name + "'>" +
" <div class='col-xs-12 col-xs-offset-0'>" + html +
" </div>" +
"</div>";
$('#main-body').prepend(html);
// Format dates with moment before display.
$('#' + name + ' .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("lll"));
}
});
$('#' + name + ' .tablesorter')
.tablesorter({
theme : 'green',
// initialize zebra
widgets: ["zebra"],
});
// Bind a delete handler.
$('#' + name + ' .delete-button').click(function() {
DeleteReservation($(this).closest('tr'));
return false;
});
}
var xmlthing = sup.CallServerMethod(null, "reserve",
"ListReservations",
{"cluster" : name});
xmlthing.done(callback);
});
}
/*
* Delete a reservation. When complete, delete the table row.
*/
function DeleteReservation(row) {
// This is what we are deleting.
var idx = $(row).attr('data-idx');
var pid = $(row).attr('data-pid');
var cluster = $(row).attr('data-cluster');
var table = $(row).closest("table");
// Callback for the delete request.
var callback = function (json) {
sup.HideModal('#waitwait-modal');
console.log("delete", json);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
$(row).remove();
table.trigger('update');
};
// Bind the confirm button in the modal. Do the deletion.
$('#confirm_modal #confirm_delete').click(function () {
sup.HideModal('#confirm_modal');
sup.ShowModal('#waitwait-modal');
var xmlthing = sup.CallServerMethod(null, "reserve",
"Delete",
{"idx" : idx,
"pid" : pid,
"cluster" : cluster});
xmlthing.done(callback);
});
// Handler so we know the user closed the modal. We need to
// clear the confirm button handler.
$('#confirm_modal').on('hidden.bs.modal', function (e) {
$('#confirm_modal #confirm_delete').unbind("click");
$('#confirm_modal').off('hidden.bs.modal');
})
sup.ShowModal("#confirm_modal");
}
// Helper.
function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent));
}
$(document).ready(initialize);
});
$(function ()
{
'use strict';
var template_list = ["reserve-request", "reservation-list",
"oops-modal", "waitwait-modal"];
var templates = APT_OPTIONS.fetchTemplateList(template_list);
var mainString = templates["reserve-request"];
var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"];
var mainTemplate = _.template(mainString);
var listTemplate = _.template(templates["reservation-list"]);
var fields = null;
var projlist = null;
var amlist = null;
var isadmin = false;
var editing = false;
var buttonstate = "check";
function initialize()
{
window.APT_OPTIONS.initialize(sup);
isadmin = window.ISADMIN;
editing = window.EDITING;
fields = JSON.parse(_.unescape($('#form-json')[0].textContent));
projlist = JSON.parse(_.unescape($('#projects-json')[0].textContent));
amlist = JSON.parse(_.unescape($('#amlist-json')[0].textContent));
GeneratePageBody(fields);
// Now we can do this.
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
/*
* In edit mode, we ask for the reservation details from the
* backend cluster and then update the form.
*/
if (editing) {
PopulateReservation();
}
}
//
// Moved into a separate function since we want to regen the form
// after each submit, which happens via ajax on this page.
//
function GeneratePageBody(formfields)
{
// Generate the template.
var html = mainTemplate({
formfields: formfields,
projects: projlist,
amlist: amlist,
isadmin: isadmin,
editing: false,
});
html = aptforms.FormatFormFieldsHorizontal(html);
$('#main-body').html(html);
// This activates the popover subsystem.
$('[data-toggle="popover"]').popover({
trigger: 'hover',
container: 'body'
});
// Handler for cluster change to show the type list.
$('#reserve-request-form #cluster').change(function (event) {
$("#reserve-request-form #cluster option:selected").
each(function() {
HandleClusterChange($(this).val());
return;
});
});
// Handle submit button.
$('#reserve-submit-button').click(function (event) {
event.preventDefault();
if (buttonstate == "check") {
CheckForm();
}
else {
Reserve();
}
});
// Handle modal submit button.
$('#confirm-reservation #commit-reservation').click(function (event) {
if (buttonstate == "submit") {
Reserve();
}
});
// Insert datepickers after html inserted.
$("#reserve-request-form #start_day").datepicker({
showButtonPanel: true,
});
$("#reserve-request-form #end_day").datepicker({
showButtonPanel: true,
});
/*
* Callback when something changes so that we can toggle the
* button from Submit to Check.
*/
var modified_callback = function () {
ToggleSubmit(true, "check");
};
aptforms.EnableUnsavedWarning('#reserve-request-form',
modified_callback);
LoadReservations();
}
//
// Check form validity. This does not check whether the reservation
// is valid.
//
function CheckForm()
{
var checkonly_callback = function(json) {
if (json.code) {
if (json.code != 2) {
sup.SpitOops("oops", json.value);
}
return;
}
// Now check the actual reservation validity.
ValidateReservation();
}
/*
* Before we submit, set the start/end fields to UTC time.
*/
var start_day = $('#reserve-request-form [name=start_day]').val();
var start_hour = $('#reserve-request-form [name=start_hour]').val();
if (start_day && start_hour) {
var start = moment(start_day, "MM/DD/YYYY");
start.hour(start_hour);
console.log("start", start);
$('#reserve-request-form [name=start]').val(start.format());
}
var end_day = $('#reserve-request-form [name=end_day]').val();
var end_hour = $('#reserve-request-form [name=end_hour]').val();
if (end_day && end_hour) {
var end = moment(end_day, "MM/DD/YYYY");
end.hour(end_hour);
console.log("end", end);
$('#reserve-request-form [name=end]').val(end.format());
}
aptforms.CheckForm('#reserve-request-form', "reserve",
"Validate", checkonly_callback);
}
/*
* Load anonymized reservations from each am in the list and generate tables.
*/
function LoadReservations()
{
var count = Object.keys(amlist).length;
_.each(amlist, function(details, urn) {
var callback = function(json) {
//console.log(json);
// Kill the spinner.
count--;
if (count <= 0) {
$('#spinner').addClass("hidden");
}
if (json.code) {
console.log("Could not get reservation data for " +
details.name + ": " + json.value);
return;
}
var reservations = json.value;
if (reservations.length == 0)
return;
// Generate the main template.
var html = listTemplate({
"reservations" : reservations,
"showidx" : false,
"showproject" : false,
"showuser" : false,
"name" : details.name,
});
html =
"<div class='row' id='" + details.nickname + "'>" +
" <div class='col-xs-12 col-xs-offset-0'>" + html +
" </div>" +
"</div>";
$('#reservation-lists').prepend(html);
// Format dates with moment before display.
$('#' + details.nickname + ' .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("lll"));
}
});
$('#' + details.nickname + ' .tablesorter')
.tablesorter({
theme : 'green',
// initialize zebra
widgets: ["zebra"],
});
}
var xmlthing = sup.CallServerMethod(null, "reserve",
"ListReservations",
{"cluster" : details.nickname,
"anonymous" : 1});
xmlthing.done(callback);
});
}
//
// Validate the reservation.
//
function ValidateReservation()
{
var callback = function(json) {
if (json.code) {
if (json.code != 2) {
sup.SpitOops("oops", json.value);
}
return;
}
// User can submit.
ToggleSubmit(true, "submit");
// Make sure we still warn about an unsaved form.
aptforms.MarkFormUnsaved();
sup.ShowModal('#confirm-reservation');
};
aptforms.SubmitForm('#reserve-request-form', "reserve",
"Validate", callback,
"Checking to see if your request can be "+
"accommodated");
}
/*
* And do it.
*/
function Reserve()
{
var reserve_callback = function(json) {
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
window.location.replace(json.value);
};
aptforms.SubmitForm('#reserve-request-form', "reserve",
"Reserve", reserve_callback,
"Submitting your reservation request; "+
"patience please");
}
function PopulateReservation()
{
var callback = function(json) {
console.log(json);
sup.HideWaitWait();
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
// Messy.
var details = json.value;
$('#reserve-request-form [name=pid]').val(details.pid);
$('#reserve-request-form [name=count]').val(details.count);
$('#reserve-request-form [name=cluster]').val(details.cluster);
HandleClusterChange(details.cluster);
$('#reserve-request-form [name=type]').val(details.type);
$('#reserve-request-form [name=reason]')
.val(_.escape(details.notes));
var start = moment(details.start);
var end = moment(details.end);
$('#reserve-request-form [name=start_day]')
.val(start.format("MM/DD/YYYY"));
$('#reserve-request-form [name=start_hour]')
.val(start.format("H"));
$('#reserve-request-form [name=end_day]')
.val(end.format("MM/DD/YYYY"));
$('#reserve-request-form [name=end_hour]')
.val(end.format("H"));
console.log(start, end);
};
sup.ShowWaitWait();
var xmlthing = sup.CallServerMethod(null, "reserve",
"GetReservation",
{"cluster" : window.CLUSTER,
"idx" : window.IDX});
xmlthing.done(callback);
}
function HandleClusterChange(selected_cluster)
{
/*
* Build up selection list of types on the selected cluster
*/
var options = "";
var typelist = amlist[selected_cluster].typeinfo;
_.each(typelist, function(details, type) {
var count = details.count;
options = options +
"<option value='" + type + "' >" +
type + " (" + count + " nodes)</option>";
});
$("#reserve-request-form #type")
.html("<option value=''>Please Select</option>" + options);
}
// Toggle the button between check and submit.
function ToggleSubmit(enable, which) {
if (which == "submit") {
$('#reserve-submit-button').text("Reserve");
$('#reserve-submit-button').addClass("btn-success");
$('#reserve-submit-button').removeClass("btn-primary");
}
else if (which == "check") {
$('#reserve-submit-button').text("Check");
$('#reserve-submit-button').removeClass("btn-success");
$('#reserve-submit-button').addClass("btn-primary");
}
if (enable) {
$('#reserve-submit-button').removeAttr("disabled");
}
else {
$('#reserve-submit-button').attr("disabled", "disabled");
}
buttonstate = which;
}
$(document).ready(initialize);
});