All new accounts created on Gitlab now require administrator approval. If you invite any collaborators, please let Flux staff know so they can approve the accounts.

Commit 36c411bd authored by Leigh B Stoller's avatar Leigh B 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);