context = {
"preference_form": AccountPreferencesForm(instance=profile),
"company_form": SetCompanyForm(initial={'company': ipa_user['ou']}),
- "existing_keys": ipa_user['ipasshpubkey'] if 'ipasshpubkey' in ipa_user else []
+ "existing_keys": ipa_user['ipasshpubkey'] if 'ipasshpubkey' in ipa_user else [],
+ "ipa_username": profile.ipa_username
}
return render(request, template, context)
return {
"form": None,
"exists": "false",
- "action": "no user"
+ "action": "no user",
+ "result": 0
}
if (not "ou" in ipa_user) or (ipa_user["ou"] == ""):
return {
"form": SetCompanyForm(),
"exists": "true",
- "action": "/api/ipa/workflow-company"
+ "action": "/api/ipa/workflow-company",
+ "result": 1
}
if (not "ipasshpubkey" in ipa_user) or (ipa_user["ipasshpubkey"] == []):
return {
"form": SetSSHForm(),
"exists": "true",
- "action": "/api/ipa/workflow-ssh"
+ "action": "/api/ipa/workflow-ssh",
+ "result": 2,
}
return {
"form": None,
"exists": "false",
- "action": ""
+ "action": "",
+ "result": -1
}
\ No newline at end of file
from account.models import UserProfile, Lab
from booking.models import Booking
from api.models import LabManagerTracker,AutomationAPIManager, APILog
+from api.utils import get_booking_prereqs_validator
import yaml
import uuid
print("incoming data is ", data)
# todo - test this
- ipa_users = list(UserProfile.objects.get(user=request.user).ipa_username) # add owner's ipa username to list of allowed users to be sent to liblaas
+ ipa_users = []
+ ipa_users.append(UserProfile.objects.get(user=request.user).ipa_username) # add owner's ipa username to list of allowed users to be sent to liblaas
- for user in list(data["allowed_users"]):
+ for user in data["allowed_users"]:
collab_profile = UserProfile.objects.get(user=User.objects.get(username=user))
- if (collab_profile.ipa_username == "" or collab_profile.ipa_username == None):
- return JsonResponse(
- data={},
- status=406, # Not good practice but a quick solution until blob validation is fully supported within django instead of the frontend
- safe=False
- )
- else:
+ prereq_validator = get_booking_prereqs_validator(collab_profile)
+
+ if prereq_validator["result"] == -1:
+ # All good
ipa_users.append(collab_profile.ipa_username)
+ else:
+ message = "There is an issue with one of your chosen collaborators."
+ if prereq_validator["result"] == 0:
+ # No IPA username
+ message = str(collab_profile) + " has not linked their IPA account yet. Please ask them to log into the LaaS dashboard, or remove them from the booking to continue."
+ elif prereq_validator["result"] == 1:
+ # No Company
+ message = str(collab_profile) + " has not set their company yet. Please ask them to log into the LaaS dashboard, go to the settings page and add it. Otherwise, remove them from the booking to continue."
+ elif prereq_validator["result"] == 2:
+ # No SSH
+ message = str(collab_profile) + " has not added an SSH public key yet. Please ask them to log into the LaaS dashboard, go to the settings page and add it. Otherwise, remove them from the booking to continue."
+ return JsonResponse(
+ data={"message": message, "error": True},
+ status=200,
+ safe=False
+ )
bookingBlob = {
"template_id": data["template_id"],
"purpose": data["metadata"]["purpose"],
"project": data["metadata"]["project"],
"length": int(data["metadata"]["length"])
- }
+ },
+ "origin": "anuket" if os.environ.get("TEMPLATE_OVERRIDE_DIR") == 'laas' else "lfedge"
}
print("allowed users are ", bookingBlob["allowed_users"])
# Now create it in liblaas
bookingBlob["metadata"]["booking_id"] = str(booking.id)
- liblaas_endpoint = os.environ.get("LIBLAAS_BASE_URL") + 'booking/create'
+ liblaas_endpoint = liblaas_base_url + 'booking/create'
liblaas_response = requests.post(liblaas_endpoint, data=json.dumps(bookingBlob), headers={'Content-Type': 'application/json'})
+ print("response from liblaas is", vars(liblaas_response))
if liblaas_response.status_code != 200:
- print("received non success from liblaas")
+ print("received non success from liblaas, deleting booking from dashboard")
+ booking.delete()
return JsonResponse(
data={},
status=500,
if request.method != 'POST':
return JsonResponse({"error" : "405 Method not allowed"})
- liblaas_base_url = os.environ.get("LIBLAAS_BASE_URL")
post_data = json.loads(request.body)
print("post data is " + str(post_data))
http_method = post_data["method"]
)
def liblaas_templates(request):
- liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + "template/list/" + UserProfile.objects.get(user=request.user).ipa_username
+ liblaas_url = liblaas_base_url + "template/list/" + UserProfile.objects.get(user=request.user).ipa_username
print("api call to " + liblaas_url)
return requests.get(liblaas_url)
def delete_template(request):
endpoint = json.loads(request.body)["endpoint"]
- liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + endpoint
+ liblaas_url = liblaas_base_url + endpoint
print("api call to ", liblaas_url)
try:
response = requests.delete(liblaas_url)
status=500,
safe=False
)
+
+def booking_status(request, booking_id):
+ print("booking id is", booking_id)
+ statuses = get_booking_status(Booking.objects.get(id=booking_id))
+ return HttpResponse(json.dumps(statuses))
def get_booking_status(bookingObject):
- liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + "booking/" + bookingObject.aggregateId + "/status"
+ liblaas_url = liblaas_base_url + "booking/" + bookingObject.aggregateId + "/status"
print("Getting booking status at: ", liblaas_url)
response = requests.get(liblaas_url)
try:
return []
def liblaas_end_booking(aggregateId):
- liblaas_url = os.environ.get('LIBLAAS_BASE_URL') + "booking/" + str(aggregateId) + "/end"
+ liblaas_url = liblaas_base_url + "booking/" + str(aggregateId) + "/end"
print("Ending booking at ", liblaas_url)
response = requests.delete(liblaas_url)
try:
key_as_list.append(request.POST["ssh_public_key"])
ipa_set_ssh(profile, key_as_list)
return redirect("workflow:book_a_pod")
+
+def list_hosts(request):
+ if request.method != "GET":
+ return HttpResponse(status=405)
+
+ dashboard = 'lfedge' if liblaas_base_url == 'lfedge' else 'anuket'
+
+ liblaas_url = os.environ.get('LIBLAAS_BASE_URL') + "flavor/hosts/" + dashboard
+ print("Listing hosts at ", liblaas_url)
+ response = requests.get(liblaas_url)
+ try:
+ return JsonResponse(
+ data = json.loads(response.content),
+ status=200,
+ safe=False
+ )
+ except Exception as e:
+ print("Failed to list hosts!", e)
+ return JsonResponse(
+ data = {},
+ status=500,
+ safe=False
+ )
+
+def list_flavors(request):
+ if (request.method) != "GET":
+ return HttpResponse(status=405)
+
+ response = requests.get(liblaas_base_url + "flavor")
+ try:
+ return JsonResponse(
+ data = json.loads(response.content),
+ status=200,
+ safe=False
+ )
+ except Exception as e:
+ print("Failed to list flavors!", e)
+ return JsonResponse(
+ data = {},
+ status=500,
+ safe=False
+ )
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url
+from django.urls import path
from booking.views import (
booking_detail_view,
BookingListView,
)
+from api.views import booking_status
+
app_name = 'booking'
urlpatterns = [
url(r'^detail/(?P<booking_id>[0-9]+)/$', booking_detail_view, name='detail'),
+ url(r'^detail/(?P<booking_id>[0-9]+)/status$', booking_status, name='detail'),
url(r'^(?P<booking_id>[0-9]+)/$', booking_detail_view, name='booking_detail'),
url(r'^delete/$', BookingDeleteView.as_view(), name='delete_prefix'),
url(r'^delete/(?P<booking_id>[0-9]+)/$', BookingDeleteView.as_view(), name='delete'),
# todo - make a task to check for expired bookings
@shared_task
def end_expired_bookings():
- print("Celery task for end_expired_bookings() has been triggered")
cleanup_set = Booking.objects.filter(end__lte=timezone.now(), ).filter(complete=False)
- print("Newly expired bookings: ", cleanup_set)
for booking in cleanup_set:
booking.complete = True
if (booking.aggregateId):
else:
print("booking " + str(booking.id) + " has no agg id")
booking.save()
- print("Finished end_expired_bookings()")
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################
-
+import json
from django.shortcuts import get_object_or_404
from django.views.generic import TemplateView
from django.shortcuts import render
from api.views import ipa_conflict_account
from booking.models import Booking
from dashboard.forms import *
-
+from api.views import list_flavors, list_hosts
from laas_dashboard import settings
user = request.user
lab = get_object_or_404(Lab, name=lab_name)
-
- # images = Image.objects.filter(from_lab=lab).filter(public=True)
- images = []
- # if user:
- # images = images | Image.objects.filter(from_lab=lab).filter(owner=user)
-
- # hosts = ResourceQuery.filter(lab=lab)
- hosts = []
+ flavors_list = json.loads(list_flavors(request).content)
+ host_list = json.loads(list_hosts(request).content)
+ flavor_map = {}
+ for flavor in flavors_list:
+ flavor_map[flavor['flavor_id']] = flavor['name']
+
+
+ # Apparently Django Templating lacks many features that regular Jinja offers, so I need to get creative
+ for host in host_list:
+ id = host["flavor"]
+ name = flavor_map[id]
+ host["flavor"] = {"id": id, "name": name}
return render(
request,
'title': "Lab Overview",
'lab': lab,
# 'hostprofiles': ResourceProfile.objects.filter(labs=lab),
- 'images': images,
- 'hosts': hosts
+ 'flavors': flavors_list,
+ 'hosts': host_list
}
)
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^api/', include('api.urls')),
url(r'^oidc/', include('mozilla_django_oidc.urls')),
+ url(r'^resource/', include('resource_inventory.urls', namespace='resource')),
]
if settings.DEBUG is True:
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################
+
+from django.conf.urls import url
+from django.urls import path
+
+from resource_inventory.views import host_list_view, profile_view
+app_name = 'resource'
+urlpatterns = [
+ url(r'^list/$', host_list_view, name='host-list'),
+ path('profile/<str:resource_id>', profile_view),
+]
\ No newline at end of file
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################
+
+import json
+from django.shortcuts import render
+from django.http import HttpResponse
+from api.views import list_hosts, list_flavors
+
+
+def host_list_view(request):
+ if request.method == "GET":
+ host_list = json.loads(list_hosts(request).content)
+ flavor_list = json.loads(list_flavors(request).content)
+ flavor_map = {}
+ for flavor in flavor_list:
+ flavor_map[flavor['flavor_id']] = flavor['name']
+
+ # Apparently Django Templating lacks many features that regular Jinja offers, so I need to get creative
+ for host in host_list:
+ id = host["flavor"]
+ name = flavor_map[id]
+ host["flavor"] = {"id": id, "name": name}
+
+ template = "resource/hosts.html"
+ context = {
+ "hosts": host_list,
+ "flavor_map": flavor_map
+ }
+ return render(request, template, context)
+
+ return HttpResponse(status=405)
+
+
+def profile_view(request, resource_id):
+ if request.method == "GET":
+ flavor_list = json.loads(list_flavors(request).content)
+ selected_flavor = {}
+ for flavor in flavor_list:
+ if flavor["flavor_id"] == resource_id:
+ selected_flavor = flavor
+ break
+
+ template = "resource/hostprofile_detail.html"
+ context = {
+ "flavor": selected_flavor
+ }
+ return render(request, template, context)
+
+ return HttpResponse(status=405)
\ No newline at end of file
SELECT_TEMPLATE: 0,
CLOUD_INIT: 1,
BOOKING_DETAILS: 2,
- ADD_COLLABS: 3,
- BOOKING_SUMMARY: 4
+ ADD_COLLABS: 2,
+ BOOKING_SUMMARY: 3
}
class BookingWorkflow extends Workflow {
constructor(savedBookingBlob) {
- super(["select_template", "cloud_init", "booking_details" ,"add_collabs", "booking_summary"])
+ super(["select_template", "cloud_init", "booking_details" , "booking_summary"])
// if (savedBookingBlob) {
// this.resume_workflow()
async startWorkflow() {
this.userTemplates = await LibLaaSAPI.getTemplatesForUser() // List<TemplateBlob>
- GUI.displayTemplates(this.userTemplates);
+ const flavorsList = await LibLaaSAPI.getLabFlavors("UNH_IOL")
+ this.labFlavors = new Map(); // Map<UUID, FlavorBlob>
+ for (const fblob of flavorsList) {
+ this.labFlavors.set(fblob.flavor_id, fblob);
+ }
+ GUI.displayTemplates(this.userTemplates, this.labFlavors);
GUI.modifyCollabWidget();
this.setEventListeners();
document.getElementById(this.sections[0]).scrollIntoView({behavior: 'smooth'});
/** Async / await is more infectious than I thought, so all functions that rely on an API call will need to be async */
async onclickConfirm() {
+ // disable button
+ const button = document.getElementById("booking-confirm-button");
+ $("html").css("cursor", "wait");
+ button.setAttribute('disabled', 'true');
const complete = this.isCompleteBookingInfo();
if (!complete[0]) {
- showError(complete[1]);
- this.step = complete[2]
- document.getElementById(this.sections[complete[2]]).scrollIntoView({behavior: 'smooth'});
+ showError(complete[1], complete[2]);
+ $("html").css("cursor", "default");
+ button.removeAttribute('disabled');
return
}
const response = await LibLaaSAPI.makeBooking(this.bookingBlob);
+ if (!response) {
+ showError("The selected resources for this booking are unavailable at this time. Please select a different resource or try again later.", -1)
+ }
if (response.bookingId) {
- showError("The booking has been successfully created.")
- window.location.href = "../../";
+ showError("The booking has been successfully created.", -1)
+ window.location.href = "../../booking/detail/" + response.bookingId + "/"; // todo
+ return;
} else {
- if (response.status == 406) {
- showError("One or more collaborators is missing SSH keys or has not configured their IPA account.")
+ if (response.error == true) {
+ showError(response.message, -1)
} else {
- showError("The booking could not be created at this time.")
+ showError("The booking could not be created at this time.", -1)
}
}
- // if (confirm("Are you sure you would like to create this booking?")) {
-
- // }
+ $("html").css("cursor", "default");
+ button.removeAttribute('disabled');
}
}
}
/** Takes a list of templateBlobs and creates a selectable card for each of them */
- static displayTemplates(templates) {
+ static displayTemplates(templates, flavor_map) {
const templates_list = document.getElementById("default_templates_list");
for (const t of templates) {
- const newCard = this.makeTemplateCard(t);
+ const newCard = this.makeTemplateCard(t, this.calculateAvailability(t, flavor_map));
templates_list.appendChild(newCard);
}
}
- static makeTemplateCard(templateBlob) {
+ static calculateAvailability(templateBlob, flavor_map) {
+ const local_map = new Map()
+
+ // Map flavor uuid to amount in template
+ for (const host of templateBlob.host_list) {
+ const existing_count = local_map.get(host.flavor)
+ if (existing_count) {
+ local_map.set(host.flavor, existing_count + 1)
+ } else {
+ local_map.set(host.flavor, 1)
+ }
+ }
+
+ let lowest_count = Number.POSITIVE_INFINITY;
+ for (const [key, val] of local_map) {
+ const curr_count = Math.floor(flavor_map.get(key).available_count / val)
+ if (curr_count < lowest_count) {
+ lowest_count = curr_count;
+ }
+ }
+
+ return lowest_count;
+ }
+
+ static makeTemplateCard(templateBlob, available_count) {
+ const isAvailable = available_count > 0;
+ let availability_text = isAvailable ? 'Resources Available' : 'Resources Unavailable';
+ let color = isAvailable ? 'text-success' : 'text-danger';
+ let disabled = !isAvailable ? 'disabled = "true"' : '';
+
const col = document.createElement('div');
col.classList.add('col-3', 'my-1');
col.innerHTML= `
</div>
<div class="card-body">
<p class="grid-item-description">` + templateBlob.pod_desc +`</p>
+ <p class="grid-item-description ` + color + `">` + availability_text + `</p>
</div>
<div class="card-footer">
- <button type="button" class="btn btn-success grid-item-select-btn w-100 stretched-link"
+ <button type="button"` + disabled + ` class="btn btn-success grid-item-select-btn w-100 stretched-link"
onclick="workflow.onclickSelectTemplate(this.parentNode.parentNode, '` + templateBlob.id +`')">Select</button>
</div>
</div>
this.flavor_id = incomingBlob.flavor_id; // UUID (String)
this.name = incomingBlob.name; // String
this.interfaces = []; // List<String>
- // images are added after
+ this.images = []; // List<ImageBlob>
+ this.available_count = incomingBlob.available_count;
if (incomingBlob.interfaces) {
this.interfaces = incomingBlob.interfaces;
}
+
+ if (incomingBlob.images) {
+ this.images = incomingBlob.images;
+ }
}
}
this.labImages = new Map(); // Map<UUID, ImageBlob>
for (const fblob of flavorsList) {
- fblob.images = await LibLaaSAPI.getImagesForFlavor(fblob.flavor_id);
for (const iblob of fblob.images) {
this.labImages.set(iblob.image_id, iblob)
}
this.step = steps.ADD_RESOURCES;
if (this.templateBlob.lab_name == null) {
- showError("Please select a lab before adding resources.");
- this.goTo(steps.SELECT_LAB);
+ showError("Please select a lab before adding resources.", steps.SELECT_LAB);
return;
}
if (this.templateBlob.host_list.length >= 8) {
- showError("You may not add more than 8 hosts to a single pod.")
+ showError("You may not add more than 8 hosts to a single pod.", -1)
return;
}
this.resourceBuilder = null;
- GUI.refreshAddHostModal(this.userTemplates);
+ GUI.refreshAddHostModal(this.userTemplates, this.labFlavors);
$("#resource_modal").modal('toggle');
}
for (const [index, host] of this.resourceBuilder.user_configs.entries()) {
const new_host = new HostConfigBlob(host);
this.templateBlob.host_list.push(new_host);
+ this.labFlavors.get(host.flavor).available_count--
}
// Add networks
for (let existing_host of this.templateBlob.host_list) {
if (hostname == existing_host.hostname) {
this.removeHostFromTemplateBlob(existing_host);
+ this.labFlavors.get(existing_host.flavor).available_count++;
GUI.refreshHostStep(this.templateBlob.host_list, this.labFlavors, this.labImages);
GUI.refreshNetworkStep(this.templateBlob.networks);
GUI.refreshConnectionStep(this.templateBlob.host_list);
}
}
- showError("didnt remove");
+ showError("didnt remove", -1);
}
this.step = steps.ADD_NETWORKS;
if (this.templateBlob.lab_name == null) {
- showError("Please select a lab before adding networks.");
- this.goTo(steps.SELECT_LAB);
+ showError("Please select a lab before adding networks.", steps.SELECT_LAB);
return;
}
this.step = steps.POD_SUMMARY;
const simpleValidation = this.simpleStepValidation();
if (!simpleValidation[0]) {
- showError(simpleValidation[1])
- this.goTo(simpleValidation[2]);
+ showError(simpleValidation[1], simpleValidation[2])
return;
}
/** Resets the host modal inner html
* Takes a list of templateBlobs
*/
- static refreshAddHostModal(template_list) {
+ static refreshAddHostModal(template_list, flavor_map) {
document.getElementById('add_resource_modal_body').innerHTML = `
<h2>Resource</h2>
<div id="template-cards" class="row align-items-center justify-content-start">
const template_cards = document.getElementById('template-cards');
for (let template of template_list) {
- template_cards.appendChild(this.makeTemplateCard(template));
+ template_cards.appendChild(this.makeTemplateCard(template, this.calculateAvailability(template, flavor_map)));
}
}
+ static calculateAvailability(templateBlob, flavor_map) {
+ const local_map = new Map()
+
+ // Map flavor uuid to amount in template
+ for (const host of templateBlob.host_list) {
+ const existing_count = local_map.get(host.flavor)
+ if (existing_count) {
+ local_map.set(host.flavor, existing_count + 1)
+ } else {
+ local_map.set(host.flavor, 1)
+ }
+ }
+
+ let lowest_count = Number.POSITIVE_INFINITY;
+ for (const [key, val] of local_map) {
+ const curr_count = Math.floor(flavor_map.get(key).available_count / val)
+ if (curr_count < lowest_count) {
+ lowest_count = curr_count;
+ }
+ }
+
+ return lowest_count;
+ }
+
/** Makes a card to be displayed in the add resource modal for a given templateBlob */
- static makeTemplateCard(templateBlob) {
+ static makeTemplateCard(templateBlob, available_count) {
+ let color = available_count > 0 ? 'text-success' : 'text-danger';
+ // let disabled = available_count == 0 ? 'disabled = "true"' : '';
const col = document.createElement('div');
col.classList.add('col-12', 'col-md-6', 'col-xl-3', 'my-3');
col.innerHTML= `
</div>
<div class="card-body">
<p class="grid-item-description">` + templateBlob.pod_desc +`</p>
+ <p class="grid-item-description ` + color + `">Resources available:` + available_count +`</p>
</div>
<div class="card-footer">
<button type="button" class="btn btn-success grid-item-select-btn w-100 stretched-link"
/** POSTs to dashboard, which then auths and logs the requests, makes the request to LibLaaS, and passes the result back to here.
Treat this as a private function. Only use the async functions when outside of this class */
static makeRequest(method, endpoint, workflow_data) {
- console.log("Making request: %s, %s, %s", method, endpoint, workflow_data.toString())
const token = document.getElementsByName('csrfmiddlewaretoken')[0].value
return new Promise((resolve, reject) => {// -> HttpResponse
$.ajax(
static async makeTemplate(templateBlob) { // -> UUID or null
templateBlob.owner = user;
- console.log(JSON.stringify(templateBlob))
return await this.handleResponse(this.makeRequest(HTTP.POST, endpoint.MAKE_TEMPLATE, templateBlob));
}
}
goTo(step_number) {
- while (step_number > this.step) {
- this.goNext();
- }
-
- while (step_number < this.step) {
- this.goPrev();
+ if (step_number < 0) return;
+ this.step = step_number
+ document.getElementById(this.sections[this.step]).scrollIntoView({behavior: 'smooth'});
+ if (this.step == this.sections.length - 1) {
+ document.getElementById('next').setAttribute('disabled', '');
+ } else if (this.step == 1) {
+ document.getElementById('prev').removeAttribute('disabled');
}
}
showError("Unable to fetch " + info +". Please try again later or contact support.")
}
-function showError(message) {
+// global variable needed for a scrollintoview bug affecting chrome
+let alert_destination = -1;
+function showError(message, destination) {
+ alert_destination = destination;
const text = document.getElementById('alert_modal_message');
-
text.innerText = message;
$("#alert_modal").modal('show');
}
{% csrf_token %}
<input id="hidden_key_list" type="hidden" name="ssh_key_list" value="">
<div class="form-group">
+ <label>Your IPA Username</label>
+ <input type="text" class="form-control" disabled="true" style="width: 300px;" value="{{ ipa_username }}">
+ <p>To change your password and access advanced account management, please go <a href="http://os-passwd.iol.unh.edu">here</a></p>
{{ company_form }}
{{ preference_form }}
<br>
key_list.push('{{key}}')
{% endfor %}
update_json_list()
- console.log(key_list)
});
Lab Info <i class="fas fa-angle-down rotate"></i>
</a>
<div class="collapse" id="labInfo">
- <a href="" class="list-group-item list-group-item-action nav-bg">
+ <a href="{% url 'resource:host-list' %}" class="list-group-item list-group-item-action nav-bg">
Hosts
</a>
<a href="{% url 'booking:list' %}" class="list-group-item list-group-item-action nav-bg">
<div class="card mb-3">
<div class="card-header d-flex">
<h4 class="d-inline">Deployment Progress</h4>
- <p>These are the different tasks that have to be completed before your deployment is ready.
- If this is taking a really long time, let us know <a href="mailto:{{contact_email}}">here!</a></p>
+ <p class="mx-3">Your resources are being prepared. If this is taking a really long time, please contact us <a href="mailto:{{contact_email}}">here!</a></p>
<!-- <button data-toggle="collapse" data-target="#panel_tasks" class="btn btn-outline-secondary ml-auto">Expand</button> -->
</div>
<div class="collapse show" id="panel_tasks">
{% for host in statuses %}
<tr>
<td>
- <!-- Success,
- Reimage,
- InProgress,
- Failure,
- Import, -->
- {% if host.status is 'Success' %}
+ {% if 'Success' in host.status %}
<div class="rounded-circle bg-success square-20"></div>
- {% elif host.status is 'InProgress' %}
- <div class="spinner-border text-primary square-20"></div>
+ {% elif 'Fail' in host.status %}
+ <div class="rounded-circle bg-danger square-20"></div>
{% else %}
- <div class="rounded-circle bg-secondary square-20"></div>
+ <div class="spinner-border text-primary square-20"></div>
{% endif %}
</td>
<td>
{{ host.hostname }}
</td>
- <td>
+ <td id="{{host.instance_id}}">
{{ host.status }}
</td>
</tr>
</div>
</div>
+<div class="row">
+ <div class="col-5">
+ <div class="card mb-3">
+ <div class="card-header d-flex">
+ Diagnostic Information
+ </div>
+ <div>
+ <table class="table m-0">
+ <tr>
+ <th></th>
+ <th>Aggregate ID</th>
+ <td>{{booking.aggregateId}}</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script>
+setInterval(function(){
+ fetchBookingStatus();
+}, 5000);
+
+
+async function fetchBookingStatus() {
+ req = new XMLHttpRequest();
+ var url = "status";
+ req.open("GET", url, true);
+ req.onerror = function() { alert("oops"); }
+ req.onreadystatechange = function() {
+ if(req.readyState === 4) {
+ statuses = JSON.parse(req.responseText)
+ updateStatuses(statuses)
+ }
+ }
+ req.send();
+}
+
+async function updateStatuses(statuses) {
+ for (const s of statuses) {
+ document.getElementById(s.instance_id).innerText = s.status
+ }
+}
+</script>
+
{% endblock content %}
<th>Project</th>
<th>Start</th>
<th>End</th>
- <th>Operating System</th>
</tr>
</thead>
<tbody>
<td>
{{ booking.end }}
</td>
- <td>
- {{ booking.resource.get_head_node.config.image.os.name }}
- </td>
</tr>
{% endfor %}
</tbody>
<div class="card mb-3">
<div class="card-header d-flex">
<h4>Lab Profile</h4>
- <button class="btn btn-outline-secondary ml-auto" data-toggle="collapse" data-target="#panel_overview">Expand</button>
</div>
<div class="collapse show" id="panel_overview">
<div class="overflow-auto">
<div class="card my-3">
<div class="card-header d-flex">
<h4 class="d-inline-block">Host Profiles</h4>
- <button data-toggle="collapse" data-target="#profile_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
<div class="collapse show" id="profile_panel">
<div class="overflow-auto">
<table class="table m-0">
- {% for profile in hostprofiles %}
+ {% for flavor in flavors %}
<tr>
- <td>{{profile.name}}</td>
- <td>{{profile.description}}</td>
- <td><a href="/resource/profiles/{{ profile.id }}" class="btn btn-info">Profile</a></td>
+ <td>{{flavor.name}}</td>
+ <td>{{flavor.description}}</td>
+ <td><a href="/resource/profile/{{ flavor.flavor_id }}" class="btn btn-info">Profile</a></td>
</tr>
{% endfor %}
</table>
<div class="card my-3">
<div class="card-header d-flex">
<h4 class="d-inline">Networking Capabilities</h4>
- <button data-toggle="collapse" data-target="#network_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
<div class="collapse show" id="network_panel">
</table>
</div>
</div>
- <div class="card my-3">
- <div class="card-header d-flex">
- <h4>Images</h4>
- <button data-toggle="collapse" data-target="#image_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
- </div>
- <div class="collapse show" id="image_panel">
- <div class="overflow-auto">
- <table class="table m-0">
- <tr>
- <th>Name</th>
- <th>Owner</th>
- <th>For Host Type</th>
- <th>Description</th>
- </tr>
- {% for image in images %}
- <tr>
- <td>{{image.name}}</td>
- <td>{{image.owner}}</td>
- <td>{{image.host_type}}</td>
- <td>{{image.description}}</td>
- </tr>
- {% endfor %}
- </table>
- </div>
- </div>
- </div>
</div>
<div class="col-lg-8">
<div class="card mb-3">
<div class="card-header d-flex">
<h4>Lab Hosts</h4>
- <button data-toggle="collapse" data-target="#lab_hosts_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
<div class="collapse show" id="lab_hosts_panel">
<table class="table m-0">
<tr>
<th>Name</th>
+ <th>Architecture</th>
<th>Profile</th>
<th>Booked</th>
<th>Working</th>
- <th>Vendor</th>
</tr>
{% for host in hosts %}
- <tr>
- <td>{{host.name}}</td>
- <td>{{host.profile}}</td>
- <td>{{host.booked|yesno:"Yes,No"}}</td>
- {% if host.working %}
- <td class="bg-success text-white">Yes</td>
- {% else %}
- <td class="bg-danger text-white">No</td>
- {% endif %}
- <td>{{host.vendor}}</td>
- </tr>
- {% endfor %}
+ <tr>
+ <td>
+ {{ host.name }}
+ </td>
+ <td>
+ {{ host.arch }}
+ </td>
+ <td>
+ <a href="../../resource/profile/{{ host.flavor.id }}">{{ host.flavor.name }}</a>
+ </td>
+ <td>
+ {% if host.allocation != null %}
+ Yes
+ {% else %}
+ No
+ {% endif %}
+ </td>
+ <td>
+ {% if host.allocation.reason == "maintenance" %}
+ No
+ {% else %}
+ Yes
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
</table>
</div>
</div>
{% load staticfiles %}
{% block content %}
+<h1>{{ flavor.name }}</h1>
<div class="row">
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header d-flex">
<h4 class="d-inline">Available at</h4>
- <button data-toggle="collapse" data-target="#availableAt" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
<div class="collapse show" id="availableAt">
<ul class="list-group list-group-flush">
- {% for lab in hostprofile.labs.all %}
- <li class="list-group-item">{{lab.name}}</li>
- {% endfor %}
+ <li class="list-group-item">UNH IOL</li>
</ul>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex">
<h4 class="d-inline">RAM</h4>
- <button data-toggle="collapse" data-target="#ramPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
<div id="ramPanel" class="collapse show">
<div class="card-body">
- {{hostprofile.ramprofile.first.amount}}G,
- {{hostprofile.ramprofile.first.channels}} channels
+ {{flavor.ram.value}} {{flavor.ram.unit}}
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex">
<h4 class="d-inline">CPU</h4>
- <button data-toggle="collapse" data-target="#cpuPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
<div class="collapse show" id="cpuPanel">
<table class="table">
<tr>
<td>Arch:</td>
- <td>{{hostprofile.cpuprofile.first.architecture}}</td>
+ <td>{{ flavor.arch }}</td>
</tr>
<tr>
<td>Cores:</td>
- <td>{{hostprofile.cpuprofile.first.cores}}</td>
+ <td>{{ flavor.cpu_count }}</td>
</tr>
<tr>
<td>Sockets:</td>
- <td>{{hostprofile.cpuprofile.first.cpus}}</td>
+ <td>{{ flavor.sockets }}</td>
</tr>
</table>
</div>
<div class="card mb-4">
<div class="card-header d-flex">
<h4 class="d-inline">Disk</h4>
- <button data-toggle="collapse" data-target="#diskPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
<div class="collapse show" id="diskPanel">
<table class="table">
<tr>
- <td>Size:</td>
- <td>{{hostprofile.storageprofile.first.size}} GiB</td>
+ <td>Disk Size:</td>
+ <td>{{flavor.disk_size.value}} {{flavor.disk_size.unit}}</td>
</tr>
<tr>
- <td>Type:</td>
- <td>{{hostprofile.storageprofile.first.media_type}}</td>
+ <td>Root Size:</td>
+ <td>{{flavor.root_size.value}} {{flavor.root_size.unit}}</td>
</tr>
<tr>
- <td>Mount Point:</td>
- <td>{{hostprofile.storageprofile.first.name}}</td>
+ <td>Swap Size:</td>
+ <td>{{flavor.swap_size.value}} {{flavor.swap_size.unit}}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-lg-6">
- <div class="card">
+ <div class="card mb-4">
<div class="card-header d-flex">
<h4 class="d-inline">Interfaces</h4>
- <button data-toggle="collapse" data-target="#interfacePanel" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
<div class="collapse show" id="interfacePanel">
<table class="table">
</tr>
</thead>
<tbody>
- {% for intprof in hostprofile.interfaceprofile.all %}
+ {% for interface in flavor.interfaces %}
+ <tr>
+ <td>{{interface.name}}</td>
+ <td>{{interface.speed.value}} {{interface.speed.unit}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="card">
+ <div class="card-header d-flex">
+ <h4 class="d-inline">Images</h4>
+ </div>
+ <div class="collapse show" id="interfacePanel">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for image in flavor.images %}
<tr>
- <td>{{intprof.name}}</td>
- <td>{{intprof.speed}}</td>
+ <td>{{image.name}}</td>
</tr>
{% endfor %}
</tbody>
<thead>
<tr>
<th>Name</th>
+ <th>Architecture</th>
<th>Profile</th>
<th>Booked</th>
<th>Working</th>
</tr>
</thead>
<tbody>
- {% for host in hosts %}
+ {% for host in hosts %}
<tr>
<td>
{{ host.name }}
</td>
<td>
- <a href="profiles/{{ host.profile.id }}">{{ host.profile }}</a>
+ {{ host.arch }}
</td>
<td>
- {{ host.booked|yesno:"Yes,No" }}
+ <a href="../profile/{{ host.flavor.id }}">{{ host.flavor.name }}</a>
</td>
<td>
- {{ host.working|yesno:"Yes,No" }}
+ {% if host.allocation != null %}
+ Yes
+ {% else %}
+ No
+ {% endif %}
+ </td>
+ <td>
+ {% if host.allocation.reason == "maintenance" %}
+ No
+ {% else %}
+ Yes
+ {% endif %}
</td>
</tr>
{% endfor %}
<div class="scroll-container w-100 h-100 p-0">
<div class="scroll-area pt-5 mx-5" id="select_template">
<h1 class="mt-4"><u>Book a Pod</u></h1>
- <h2 class="mt-4 mb-3">Select Host Or Template:</h2>
+ <h2 class="mt-4 mb-3">Select Host Or Template<span class="text-danger">*</span></h2>
<div class="card-deck align-items-center">
<div class="col-12" id="template_list">
</div>
<div class="scroll-area pt-5 mx-5" id="booking_details">
- <h2 class="mt-4 mb-3">Booking Details</h2>
+ <h2 class="mt-4 mb-3">Booking Details<span class="text-danger">*</span></h2>
<div class="form-group mb-0">
<div class="row align-items-center my-4">
<div class="col-xl-6 col-md-8 col-11">
<input id="input_length" type="range" min="1" max="21" value="1" class="form-control form-control-lg col-xl-5 col-9 p-0" placeholder="Length" oninput="workflow.onchangeDays()">
</div>
</div>
- </div>
-
- <div class="scroll-area pt-5 mx-5" id="add_collabs">
<h2 class="mt-4 mb-3">Add Collaborators:</h2>
<div class="row">
<div class="col-xl-6 col-md-8 col-11 p-0 border border-dark">
</div>
<div class="row align-items-center mt-5">
<!-- <button class="btn btn-danger cancel-book-button p-0 mr-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickCancel()">Cancel</button> -->
- <button class="btn btn-success cancel-book-button p-0 ml-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickConfirm()">Book</button>
+ <button id="booking-confirm-button" class="btn btn-success cancel-book-button p-0 ml-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickConfirm()">Book</button>
</div>
</div>
<h5 id="alert_modal_message"></h5>
</div>
<div class="modal-footer d-flex justify-content-center">
- <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="">Confirm</button>
+ <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="workflow.goTo(alert_destination)">Confirm</button>
</div>
</div>
</div>
<div class="scroll-area pt-5 mx-5" id="select_lab">
<!-- Ideally the 'Design a Pod' header would be anchored to the top of the page below the arrow -->
<h1 class="mt-4"><u>Design a Pod</u></h1>
- <h2 class="mt-4 mb-3">Select a Lab:</h2>
+ <h2 class="mt-4 mb-3">Select a Lab<span class="text-danger">*</span></h2>
<div class="row card-deck" id="lab_cards">
</div>
</div>
<!-- Add Resources -->
<div class="scroll-area pt-5 mx-5" id="add_resources">
- <h2 class="mt-4 mb-3">Add Resources:</h2>
+ <h2 class="mt-4 mb-3">Add Resources<span class="text-danger">*</span></h2>
<div class="row card-deck align-items-center" id="host_cards">
<div class="col-xl-3 col-md-6 col-12" id="add_resource_plus_card">
<div class="card align-items-center border-0">
<!-- Add Networks -->
<div class="scroll-area pt-5 mx-5" id="add_networks">
- <h2 class="mt-4 mb-3">Add Networks:</h2>
+ <h2 class="mt-4 mb-3">Add Networks<span class="text-danger">*</span></h2>
<div class="row card-deck align-items-center" id="network_card_deck">
<div class="col-xl-3 col-md-6 col-12" id="add_network_plus_card">
<div class="card align-items-center border-0">
<!-- Configure Connections-->
<div class="scroll-area pt-5 mx-5" id="configure_connections">
- <h2 class="mt-4 mb-3">Configure Connections:</h2>
+ <h2 class="mt-4 mb-3">Configure Connections<span class="text-danger">*</span></h2>
<div class="row card-deck align-items-center" id="connection_cards">
</div>
</div>
<!-- Pod Details-->
<div class="scroll-area pt-5 mx-5" id="pod_details">
- <h2 class="mt-4 mb-3">Pod Details</h2>
+ <h2 class="mt-4 mb-3">Pod Details<span class="text-danger">*</span></h2>
<div class="form-group">
<div class="row align-items-center my-4">
<div class="col-xl-6 col-md-8 col-11">
<h5 id="alert_modal_message"></h5>
</div>
<div class="modal-footer d-flex justify-content-center">
- <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="">Confirm</button>
+ <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="workflow.goTo(alert_destination)">Confirm</button>
</div>
</div>
</div>