From ecadb07367d31c0929212618e120130f54af78da Mon Sep 17 00:00:00 2001 From: Justin Choquette Date: Tue, 8 Aug 2023 11:33:57 -0400 Subject: MVP Change-Id: Ib590302f49e7e66f8d04841fb6cb97baf623f51a Signed-off-by: Justin Choquette --- src/account/views.py | 3 +- src/api/utils.py | 12 ++- src/api/views.py | 98 ++++++++++++++++++---- src/booking/urls.py | 4 + src/dashboard/tasks.py | 3 - src/dashboard/views.py | 28 ++++--- src/laas_dashboard/urls.py | 1 + src/resource_inventory/urls.py | 10 +++ src/resource_inventory/views.py | 47 +++++++++++ src/static/js/workflows/book-a-pod.js | 80 +++++++++++++----- src/static/js/workflows/common-models.js | 7 +- src/static/js/workflows/design-a-pod.js | 51 ++++++++--- src/static/js/workflows/workflow.js | 21 ++--- src/templates/base/account/settings.html | 4 +- src/templates/base/base.html | 2 +- src/templates/base/booking/booking_detail.html | 64 +++++++++++--- src/templates/base/booking/booking_table.html | 4 - src/templates/base/dashboard/lab_detail.html | 78 +++++++---------- .../base/resource/hostprofile_detail.html | 60 ++++++++----- src/templates/base/resource/hosts.html | 20 ++++- src/templates/base/workflow/book_a_pod.html | 11 +-- src/templates/base/workflow/design_a_pod.html | 12 +-- 22 files changed, 435 insertions(+), 185 deletions(-) diff --git a/src/account/views.py b/src/account/views.py index a975a2e..a392349 100644 --- a/src/account/views.py +++ b/src/account/views.py @@ -48,7 +48,8 @@ def account_settings_view(request): 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) diff --git a/src/api/utils.py b/src/api/utils.py index c32205e..d6aa68a 100644 --- a/src/api/utils.py +++ b/src/api/utils.py @@ -96,7 +96,8 @@ def get_booking_prereqs_validator(user_profile): return { "form": None, "exists": "false", - "action": "no user" + "action": "no user", + "result": 0 } if (not "ou" in ipa_user) or (ipa_user["ou"] == ""): @@ -104,7 +105,8 @@ def get_booking_prereqs_validator(user_profile): 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"] == []): @@ -112,11 +114,13 @@ def get_booking_prereqs_validator(user_profile): 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 diff --git a/src/api/views.py b/src/api/views.py index 98dd3dc..dbe00e8 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -34,6 +34,7 @@ from api.forms import DowntimeForm 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 @@ -250,18 +251,32 @@ def make_booking(request): 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"], @@ -274,7 +289,8 @@ def make_booking(request): "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"]) @@ -296,10 +312,12 @@ def make_booking(request): # 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, @@ -482,7 +500,6 @@ def liblaas_request(request) -> JsonResponse: 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"] @@ -523,13 +540,13 @@ def liblaas_request(request) -> JsonResponse: ) 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) @@ -544,9 +561,14 @@ def delete_template(request): 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: @@ -556,7 +578,7 @@ def get_booking_status(bookingObject): 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: @@ -646,3 +668,45 @@ def ipa_add_ssh_from_workflow(request): 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 + ) diff --git a/src/booking/urls.py b/src/booking/urls.py index 9784fc5..c6b9d40 100644 --- a/src/booking/urls.py +++ b/src/booking/urls.py @@ -26,6 +26,7 @@ Including another URLconf 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, @@ -34,9 +35,12 @@ from booking.views import ( BookingListView, ) +from api.views import booking_status + app_name = 'booking' urlpatterns = [ url(r'^detail/(?P[0-9]+)/$', booking_detail_view, name='detail'), + url(r'^detail/(?P[0-9]+)/status$', booking_status, name='detail'), url(r'^(?P[0-9]+)/$', booking_detail_view, name='booking_detail'), url(r'^delete/$', BookingDeleteView.as_view(), name='delete_prefix'), url(r'^delete/(?P[0-9]+)/$', BookingDeleteView.as_view(), name='delete'), diff --git a/src/dashboard/tasks.py b/src/dashboard/tasks.py index ac4b006..281db92 100644 --- a/src/dashboard/tasks.py +++ b/src/dashboard/tasks.py @@ -17,9 +17,7 @@ from api.views import liblaas_end_booking # 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): @@ -28,4 +26,3 @@ def end_expired_bookings(): else: print("booking " + str(booking.id) + " has no agg id") booking.save() - print("Finished end_expired_bookings()") diff --git a/src/dashboard/views.py b/src/dashboard/views.py index f250a3c..909b695 100644 --- a/src/dashboard/views.py +++ b/src/dashboard/views.py @@ -8,7 +8,7 @@ # 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 @@ -21,7 +21,7 @@ from api.utils import get_ipa_migration_form, ipa_query_user 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 @@ -40,14 +40,18 @@ def lab_detail_view(request, lab_name): 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, @@ -56,8 +60,8 @@ def lab_detail_view(request, lab_name): 'title': "Lab Overview", 'lab': lab, # 'hostprofiles': ResourceProfile.objects.filter(labs=lab), - 'images': images, - 'hosts': hosts + 'flavors': flavors_list, + 'hosts': host_list } ) diff --git a/src/laas_dashboard/urls.py b/src/laas_dashboard/urls.py index f78e9ff..a7b29fb 100644 --- a/src/laas_dashboard/urls.py +++ b/src/laas_dashboard/urls.py @@ -41,6 +41,7 @@ urlpatterns = [ 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: diff --git a/src/resource_inventory/urls.py b/src/resource_inventory/urls.py index f9bd07e..f230509 100644 --- a/src/resource_inventory/urls.py +++ b/src/resource_inventory/urls.py @@ -7,3 +7,13 @@ # 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/', profile_view), +] \ No newline at end of file diff --git a/src/resource_inventory/views.py b/src/resource_inventory/views.py index f903394..837cc73 100644 --- a/src/resource_inventory/views.py +++ b/src/resource_inventory/views.py @@ -6,3 +6,50 @@ # 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 diff --git a/src/static/js/workflows/book-a-pod.js b/src/static/js/workflows/book-a-pod.js index 3f83849..ddea556 100644 --- a/src/static/js/workflows/book-a-pod.js +++ b/src/static/js/workflows/book-a-pod.js @@ -6,13 +6,13 @@ const steps = { 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() @@ -24,7 +24,12 @@ const steps = { async startWorkflow() { this.userTemplates = await LibLaaSAPI.getTemplatesForUser() // List - GUI.displayTemplates(this.userTemplates); + const flavorsList = await LibLaaSAPI.getLabFlavors("UNH_IOL") + this.labFlavors = new Map(); // Map + 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'}); @@ -240,28 +245,35 @@ const steps = { /** 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'); } } @@ -288,16 +300,45 @@ class GUI { } /** 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= ` @@ -307,9 +348,10 @@ class GUI {

` + templateBlob.pod_desc +`

+

` + availability_text + `

diff --git a/src/static/js/workflows/common-models.js b/src/static/js/workflows/common-models.js index 65fedb1..dc479c2 100644 --- a/src/static/js/workflows/common-models.js +++ b/src/static/js/workflows/common-models.js @@ -169,11 +169,16 @@ class FlavorBlob { this.flavor_id = incomingBlob.flavor_id; // UUID (String) this.name = incomingBlob.name; // String this.interfaces = []; // List - // images are added after + this.images = []; // List + this.available_count = incomingBlob.available_count; if (incomingBlob.interfaces) { this.interfaces = incomingBlob.interfaces; } + + if (incomingBlob.images) { + this.images = incomingBlob.images; + } } } diff --git a/src/static/js/workflows/design-a-pod.js b/src/static/js/workflows/design-a-pod.js index 58f8b85..2083f7d 100644 --- a/src/static/js/workflows/design-a-pod.js +++ b/src/static/js/workflows/design-a-pod.js @@ -89,7 +89,6 @@ class DesignWorkflow extends Workflow { this.labImages = new Map(); // Map 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) } @@ -110,18 +109,17 @@ class DesignWorkflow extends Workflow { 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'); } @@ -248,6 +246,7 @@ class DesignWorkflow extends Workflow { 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 @@ -274,6 +273,7 @@ class DesignWorkflow extends Workflow { 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); @@ -282,7 +282,7 @@ class DesignWorkflow extends Workflow { } } - showError("didnt remove"); + showError("didnt remove", -1); } @@ -297,8 +297,7 @@ class DesignWorkflow extends Workflow { 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; } @@ -561,8 +560,7 @@ class DesignWorkflow extends Workflow { 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; } @@ -632,7 +630,7 @@ class GUI { /** 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 = `

Resource

@@ -663,13 +661,39 @@ class GUI { 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= ` @@ -679,6 +703,7 @@ class GUI {

` + templateBlob.pod_desc +`

+

Resources available:` + available_count +`

@@ -61,16 +60,15 @@

Host Profiles

-
- {% for profile in hostprofiles %} + {% for flavor in flavors %} - - - + + + {% endfor %}
{{profile.name}}{{profile.description}}Profile{{flavor.name}}{{flavor.description}}Profile
@@ -81,7 +79,6 @@

Networking Capabilities

-
@@ -96,63 +93,50 @@
-
-
-

Images

- -
-
-
- - - - - - - - {% for image in images %} - - - - - - - {% endfor %} -
NameOwnerFor Host TypeDescription
{{image.name}}{{image.owner}}{{image.host_type}}{{image.description}}
-
-
-

Lab Hosts

-
+ - {% for host in hosts %} - - - - - {% if host.working %} - - {% else %} - - {% endif %} - - - {% endfor %} + + + + + + + + {% endfor %}
NameArchitecture Profile Booked WorkingVendor
{{host.name}}{{host.profile}}{{host.booked|yesno:"Yes,No"}}YesNo{{host.vendor}}
+ {{ host.name }} + + {{ host.arch }} + + {{ host.flavor.name }} + + {% if host.allocation != null %} + Yes + {% else %} + No + {% endif %} + + {% if host.allocation.reason == "maintenance" %} + No + {% else %} + Yes + {% endif %} +
diff --git a/src/templates/base/resource/hostprofile_detail.html b/src/templates/base/resource/hostprofile_detail.html index 0b3262c..1cf2000 100644 --- a/src/templates/base/resource/hostprofile_detail.html +++ b/src/templates/base/resource/hostprofile_detail.html @@ -2,51 +2,46 @@ {% load staticfiles %} {% block content %} +

{{ flavor.name }}

Available at

-
    - {% for lab in hostprofile.labs.all %} -
  • {{lab.name}}
  • - {% endfor %} +
  • UNH IOL

RAM

-
- {{hostprofile.ramprofile.first.amount}}G, - {{hostprofile.ramprofile.first.channels}} channels + {{flavor.ram.value}} {{flavor.ram.unit}}

CPU

-
- + - + - +
Arch:{{hostprofile.cpuprofile.first.architecture}}{{ flavor.arch }}
Cores:{{hostprofile.cpuprofile.first.cores}}{{ flavor.cpu_count }}
Sockets:{{hostprofile.cpuprofile.first.cpus}}{{ flavor.sockets }}
@@ -54,31 +49,29 @@

Disk

-
- - + + - - + + - - + +
Size:{{hostprofile.storageprofile.first.size}} GiBDisk Size:{{flavor.disk_size.value}} {{flavor.disk_size.unit}}
Type:{{hostprofile.storageprofile.first.media_type}}Root Size:{{flavor.root_size.value}} {{flavor.root_size.unit}}
Mount Point:{{hostprofile.storageprofile.first.name}}Swap Size:{{flavor.swap_size.value}} {{flavor.swap_size.unit}}
-
+

Interfaces

-
@@ -89,10 +82,31 @@ - {% for intprof in hostprofile.interfaceprofile.all %} + {% for interface in flavor.interfaces %} + + + + + {% endfor %} + +
{{interface.name}}{{interface.speed.value}} {{interface.speed.unit}}
+
+
+
+
+

Images

+
+
+ + + + + + + + {% for image in flavor.images %} - - + {% endfor %} diff --git a/src/templates/base/resource/hosts.html b/src/templates/base/resource/hosts.html index 9fc50ce..e01916a 100644 --- a/src/templates/base/resource/hosts.html +++ b/src/templates/base/resource/hosts.html @@ -5,25 +5,37 @@ + - {% for host in hosts %} + {% for host in hosts %} + {% endfor %} diff --git a/src/templates/base/workflow/book_a_pod.html b/src/templates/base/workflow/book_a_pod.html index 7448dc5..8a0fb47 100644 --- a/src/templates/base/workflow/book_a_pod.html +++ b/src/templates/base/workflow/book_a_pod.html @@ -24,7 +24,7 @@

Book a Pod

-

Select Host Or Template:

+

Select Host Or Template*

@@ -46,7 +46,7 @@
-

Booking Details

+

Booking Details*

@@ -66,9 +66,6 @@
-
- -

Add Collaborators:

@@ -99,7 +96,7 @@
- +
@@ -144,7 +141,7 @@
diff --git a/src/templates/base/workflow/design_a_pod.html b/src/templates/base/workflow/design_a_pod.html index ab3f11b..32bd332 100644 --- a/src/templates/base/workflow/design_a_pod.html +++ b/src/templates/base/workflow/design_a_pod.html @@ -31,14 +31,14 @@

Design a Pod

-

Select a Lab:

+

Select a Lab*

-

Add Resources:

+

Add Resources*

@@ -51,7 +51,7 @@
-

Add Networks:

+

Add Networks*

@@ -64,14 +64,14 @@
-

Configure Connections:

+

Configure Connections*

-

Pod Details

+

Pod Details*

@@ -208,7 +208,7 @@
-- cgit 1.2.3-korg
Name
{{intprof.name}}{{intprof.speed}}{{image.name}}
NameArchitecture Profile Booked Working
{{ host.name }} - {{ host.profile }} + {{ host.arch }} - {{ host.booked|yesno:"Yes,No" }} + {{ host.flavor.name }} - {{ host.working|yesno:"Yes,No" }} + {% if host.allocation != null %} + Yes + {% else %} + No + {% endif %} + + {% if host.allocation.reason == "maintenance" %} + No + {% else %} + Yes + {% endif %}