summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSawyer Bergeron <sawyerbergeron@gmail.com>2019-01-17 11:30:35 -0500
committerSawyer Bergeron <sawyerbergeron@gmail.com>2019-01-18 11:27:53 -0500
commite3842fee2abb084d020acf7b868af745b8a66c18 (patch)
treea65021580b41005e266a18231a4a58a8386cc3ba
parentde2031ad28b3556a65d19151be3a8b459b151c65 (diff)
Add Quick-Booking Workflow
Users can now quickly provision a single-host pod without having to configure unecessary networking. This is intended to be analogous to the workflow used during LaaS 1.0, and to speed up the process of creating a booking for users who do not need more than a single host (for virtual deployments) Change-Id: Ia19cea9a42bbb1df57aad05af8f8ea821395664d Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
-rw-r--r--dashboard/src/account/models.py6
-rw-r--r--dashboard/src/booking/forms.py106
-rw-r--r--dashboard/src/booking/migrations/0003_auto_20190115_1733.py30
-rw-r--r--dashboard/src/booking/models.py26
-rw-r--r--dashboard/src/booking/quick_deployer.py272
-rw-r--r--dashboard/src/booking/urls.py4
-rw-r--r--dashboard/src/booking/views.py64
-rw-r--r--dashboard/src/resource_inventory/migrations/0005_image_os.py19
-rw-r--r--dashboard/src/resource_inventory/models.py7
-rw-r--r--dashboard/src/resource_inventory/resource_manager.py11
-rw-r--r--dashboard/src/templates/booking/quick_deploy.html206
-rw-r--r--dashboard/src/templates/dashboard/landing.html1
-rw-r--r--dashboard/src/templates/dashboard/multiple_select_filter_widget.html73
-rw-r--r--dashboard/src/templates/dashboard/searchable_select_multiple.html7
-rw-r--r--dashboard/src/workflow/forms.py8
15 files changed, 744 insertions, 96 deletions
diff --git a/dashboard/src/account/models.py b/dashboard/src/account/models.py
index bfeead0..0f8154e 100644
--- a/dashboard/src/account/models.py
+++ b/dashboard/src/account/models.py
@@ -94,7 +94,7 @@ class VlanManager(models.Model):
vlan_master_list = json.loads(self.vlans)
try:
iter(vlans)
- except:
+ except Exception:
vlans = [vlans]
for vlan in vlans:
@@ -112,7 +112,7 @@ class VlanManager(models.Model):
try:
iter(vlans)
- except:
+ except Exception:
vlans = [vlans]
for vlan in vlans:
@@ -125,7 +125,7 @@ class VlanManager(models.Model):
try:
iter(vlans)
- except:
+ except Exception:
vlans = [vlans]
vlans = set(vlans)
diff --git a/dashboard/src/booking/forms.py b/dashboard/src/booking/forms.py
new file mode 100644
index 0000000..cb76383
--- /dev/null
+++ b/dashboard/src/booking/forms.py
@@ -0,0 +1,106 @@
+##############################################################################
+# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+import django.forms as forms
+from django.forms.widgets import NumberInput
+from django.db.models import Q
+
+from workflow.forms import (
+ SearchableSelectMultipleWidget,
+ MultipleSelectFilterField,
+ MultipleSelectFilterWidget,
+ FormUtils)
+from account.models import UserProfile
+from resource_inventory.models import Image, Installer, Scenario
+
+
+class QuickBookingForm(forms.Form):
+ purpose = forms.CharField(max_length=1000)
+ project = forms.CharField(max_length=400)
+ image = forms.ModelChoiceField(queryset=Image.objects.all())
+ hostname = forms.CharField(max_length=400)
+
+ installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False)
+ scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False)
+
+ def __init__(self, data=None, *args, user=None, **kwargs):
+ chosen_users = []
+ if "default_user" in kwargs:
+ default_user = kwargs.pop("default_user")
+ else:
+ default_user = "you"
+ self.default_user = default_user
+ if "chosen_users" in kwargs:
+ chosen_users = kwargs.pop("chosen_users")
+ elif data and "users" in data:
+ chosen_users = data.getlist("users")
+
+ if user:
+ self.image = forms.ModelChoiceField(queryset=Image.objects.filter(
+ Q(public=True) | Q(owner=user)), required=False)
+ else:
+ self.image = forms.ModelChoiceField(queryset=Image.objects.all(), required=False)
+
+ super(QuickBookingForm, self).__init__(data=data, **kwargs)
+
+ self.fields['users'] = forms.CharField(
+ widget=SearchableSelectMultipleWidget(
+ attrs=self.build_search_widget_attrs(chosen_users, default_user=default_user)
+ ),
+ required=False
+ )
+ attrs = FormUtils.getLabData(0)
+ attrs['selection_data'] = 'false'
+ self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(attrs=attrs))
+ self.fields['length'] = forms.IntegerField(
+ widget=NumberInput(
+ attrs={
+ "type": "range",
+ 'min': "1",
+ "max": "21",
+ "value": "1"
+ }
+ )
+ )
+
+ def build_user_list(self):
+ """
+ returns a mapping of UserProfile ids to displayable objects expected by
+ searchable multiple select widget
+ """
+ try:
+ users = {}
+ d_qset = UserProfile.objects.select_related('user').all().exclude(user__username=self.default_user)
+ for userprofile in d_qset:
+ user = {
+ 'id': userprofile.user.id,
+ 'expanded_name': userprofile.full_name,
+ 'small_name': userprofile.user.username,
+ 'string': userprofile.email_addr
+ }
+
+ users[userprofile.user.id] = user
+
+ return users
+ except Exception:
+ pass
+
+ def build_search_widget_attrs(self, chosen_users, default_user="you"):
+
+ attrs = {
+ 'set': self.build_user_list(),
+ 'show_from_noentry': "false",
+ 'show_x_results': 10,
+ 'scrollable': "false",
+ 'selectable_limit': -1,
+ 'name': "users",
+ 'placeholder': "username",
+ 'initial': chosen_users,
+ 'edit': False
+ }
+ return attrs
diff --git a/dashboard/src/booking/migrations/0003_auto_20190115_1733.py b/dashboard/src/booking/migrations/0003_auto_20190115_1733.py
new file mode 100644
index 0000000..70eecfe
--- /dev/null
+++ b/dashboard/src/booking/migrations/0003_auto_20190115_1733.py
@@ -0,0 +1,30 @@
+# Generated by Django 2.1 on 2019-01-15 17:33
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0002_booking_pdf'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='installer',
+ name='sup_scenarios',
+ ),
+ migrations.RemoveField(
+ model_name='opsys',
+ name='sup_installers',
+ ),
+ migrations.DeleteModel(
+ name='Installer',
+ ),
+ migrations.DeleteModel(
+ name='Opsys',
+ ),
+ migrations.DeleteModel(
+ name='Scenario',
+ ),
+ ]
diff --git a/dashboard/src/booking/models.py b/dashboard/src/booking/models.py
index 74b766d..0972922 100644
--- a/dashboard/src/booking/models.py
+++ b/dashboard/src/booking/models.py
@@ -16,32 +16,6 @@ from django.db import models
import resource_inventory.resource_manager
-class Scenario(models.Model):
- id = models.AutoField(primary_key=True)
- name = models.CharField(max_length=300)
-
- def __str__(self):
- return self.name
-
-
-class Installer(models.Model):
- id = models.AutoField(primary_key=True)
- name = models.CharField(max_length=30)
- sup_scenarios = models.ManyToManyField(Scenario, blank=True)
-
- def __str__(self):
- return self.name
-
-
-class Opsys(models.Model):
- id = models.AutoField(primary_key=True)
- name = models.CharField(max_length=100)
- sup_installers = models.ManyToManyField(Installer, blank=True)
-
- def __str__(self):
- return self.name
-
-
class Booking(models.Model):
id = models.AutoField(primary_key=True)
owner = models.ForeignKey(User, models.CASCADE, related_name='owner')
diff --git a/dashboard/src/booking/quick_deployer.py b/dashboard/src/booking/quick_deployer.py
new file mode 100644
index 0000000..9bc8c66
--- /dev/null
+++ b/dashboard/src/booking/quick_deployer.py
@@ -0,0 +1,272 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import json
+import uuid
+import re
+from django.db.models import Q
+from django.contrib.auth.models import User
+from datetime import timedelta
+from django.utils import timezone
+from account.models import Lab
+
+from resource_inventory.models import (
+ Installer,
+ Image,
+ GenericResourceBundle,
+ ConfigBundle,
+ Vlan,
+ Host,
+ HostProfile,
+ HostConfiguration,
+ GenericResource,
+ GenericHost,
+ GenericInterface,
+ OPNFVRole,
+ OPNFVConfig
+)
+from resource_inventory.resource_manager import ResourceManager
+from booking.models import Booking
+from dashboard.exceptions import (
+ InvalidHostnameException,
+ ResourceAvailabilityException,
+ ModelValidationException
+)
+from api.models import JobFactory
+
+
+# model validity exceptions
+class IncompatibleInstallerForOS(Exception):
+ pass
+
+
+class IncompatibleScenarioForInstaller(Exception):
+ pass
+
+
+class IncompatibleImageForHost(Exception):
+ pass
+
+
+class ImageOwnershipInvalid(Exception):
+ pass
+
+
+class ImageNotAvailableAtLab(Exception):
+ pass
+
+
+class LabDNE(Exception):
+ pass
+
+
+class HostProfileDNE(Exception):
+ pass
+
+
+class HostNotAvailable(Exception):
+ pass
+
+
+class NoLabSelectedError(Exception):
+ pass
+
+
+class OPNFVRoleDNE(Exception):
+ pass
+
+
+class NoRemainingPublicNetwork(Exception):
+ pass
+
+
+def create_from_form(form, request):
+ quick_booking_id = str(uuid.uuid4())
+
+ host_field = form.cleaned_data['filter_field']
+ host_json = json.loads(host_field)
+ purpose_field = form.cleaned_data['purpose']
+ project_field = form.cleaned_data['project']
+ users_field = form.cleaned_data['users']
+ host_name = form.cleaned_data['hostname']
+ length = form.cleaned_data['length']
+
+ image = form.cleaned_data['image']
+ scenario = form.cleaned_data['scenario']
+ installer = form.cleaned_data['installer']
+
+ # get all initial info we need to validate
+ lab_dict = host_json['labs'][0]
+ lab_id = list(lab_dict.keys())[0]
+ lab_user_id = int(lab_id.split("_")[-1])
+ lab = Lab.objects.get(lab_user__id=lab_user_id)
+
+ host_dict = host_json['hosts'][0]
+ profile_id = list(host_dict.keys())[0]
+ profile_id = int(profile_id.split("_")[-1])
+ profile = HostProfile.objects.get(id=profile_id)
+
+ # check validity of field data before trying to apply to models
+ if not lab:
+ raise LabDNE("Lab with provided ID does not exist")
+ if not profile:
+ raise HostProfileDNE("Host type with provided ID does not exist")
+
+ # check that hostname is valid
+ if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", host_name):
+ raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
+ # check that image os is compatible with installer
+ if installer in image.os.sup_installers.all():
+ #if installer not here, we can omit that and not check for scenario
+ if not scenario:
+ raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
+ if scenario not in installer.sup_scenarios.all():
+ raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
+ if image.from_lab != lab:
+ raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
+ if image.host_type != profile:
+ raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
+ if not image.public and image.owner != request.user:
+ raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
+
+ # check if host type is available
+ #ResourceManager.getInstance().acquireHost(ghost, lab.name)
+ available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
+ if not profile in available_host_types:
+ # TODO: handle deleting generic resource in this instance along with grb
+ raise HostNotAvailable("Could not book selected host due to changed availability. Try again later")
+
+ # check if any hosts with profile at lab are still available
+ hostset = Host.objects.filter(lab=lab, profile=profile).filter(booked=False).filter(working=True)
+ if not hostset.first():
+ raise HostNotAvailable("Couldn't find any matching unbooked hosts")
+
+ # generate GenericResourceBundle
+ if len(host_json['labs']) != 1:
+ raise NoLabSelectedError("No lab was selected")
+
+ grbundle = GenericResourceBundle(owner=request.user)
+ grbundle.lab = lab
+ grbundle.name = "grbundle for quick booking with uid " + quick_booking_id
+ grbundle.description = "grbundle created for quick-deploy booking"
+ grbundle.save()
+
+ # generate GenericResource, GenericHost
+ gresource = GenericResource(bundle=grbundle, name=host_name)
+ gresource.save()
+
+ ghost = GenericHost()
+ ghost.resource = gresource
+ ghost.profile = profile
+ ghost.save()
+
+ # generate config bundle
+ cbundle = ConfigBundle()
+ cbundle.owner = request.user
+ cbundle.name = "configbundle for quick booking with uid " + quick_booking_id
+ cbundle.description = "configbundle created for quick-deploy booking"
+ cbundle.bundle = grbundle
+ cbundle.save()
+
+ # generate OPNFVConfig pointing to cbundle
+ if installer:
+ opnfvconfig = OPNFVConfig()
+ opnfvconfig.scenario = scenario
+ opnfvconfig.installer = installer
+ opnfvconfig.bundle = cbundle
+ opnfvconfig.save()
+
+ # generate HostConfiguration pointing to cbundle
+ hconf = HostConfiguration()
+ hconf.host = ghost
+ hconf.image = image
+ hconf.opnfvRole = OPNFVRole.objects.get(name="Jumphost")
+ if not hconf.opnfvRole:
+ raise OPNFVRoleDNE("No jumphost role was found")
+ hconf.bundle = cbundle
+ hconf.save()
+
+ # construct generic interfaces
+ for interface_profile in profile.interfaceprofile.all():
+ generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
+ generic_interface.save()
+ ghost.save()
+
+ # get vlan, assign to first interface
+ publicnetwork = lab.vlan_manager.get_public_vlan()
+ publicvlan = publicnetwork.vlan
+ if not publicnetwork:
+ raise NoRemainingPublicNetwork("No public networks were available for your pod")
+ lab.vlan_manager.reserve_public_vlan(publicvlan)
+
+ vlan = Vlan.objects.create(vlan_id=publicvlan, tagged=False, public=True)
+ vlan.save()
+ ghost.generic_interfaces.first().vlans.add(vlan)
+ ghost.generic_interfaces.first().save()
+
+ # generate resource bundle
+ try:
+ resource_bundle = ResourceManager.getInstance().convertResourceBundle(grbundle, config=cbundle)
+ except ResourceAvailabilityException:
+ raise ResourceAvailabilityException("Requested resources not available")
+ except ModelValidationException:
+ raise ModelValidationException("Encountered error while saving grbundle")
+
+ # generate booking
+ booking = Booking()
+ booking.purpose = purpose_field
+ booking.project = project_field
+ booking.lab = lab
+ booking.owner = request.user
+ booking.start = timezone.now()
+ booking.end = timezone.now() + timedelta(days=int(length))
+ booking.resource = resource_bundle
+ booking.pdf = ResourceManager().makePDF(booking.resource)
+ booking.save()
+ print("users field:")
+ print(users_field)
+ print(type(users_field))
+ #users_field = json.loads(users_field)
+ users_field = users_field[2:-2]
+ if users_field: #may be empty after split, if no collaborators entered
+ users_field = json.loads(users_field)
+ for collaborator in users_field:
+ user = User.objects.get(id=collaborator['id'])
+ booking.collaborators.add(user)
+ booking.save()
+
+ # generate job
+ JobFactory.makeCompleteJob(booking)
+
+
+def drop_filter(user):
+ installer_filter = {}
+ for image in Image.objects.all():
+ installer_filter[image.id] = {}
+ for installer in image.os.sup_installers.all():
+ installer_filter[image.id][installer.id] = 1
+
+ scenario_filter = {}
+ for installer in Installer.objects.all():
+ scenario_filter[installer.id] = {}
+ for scenario in installer.sup_scenarios.all():
+ scenario_filter[installer.id][scenario.id] = 1
+
+ images = Image.objects.filter(Q(public=True) | Q(owner=user))
+ image_filter = {}
+ for image in images:
+ image_filter[image.id] = {}
+ image_filter[image.id]['lab'] = 'lab_' + str(image.from_lab.lab_user.id)
+ image_filter[image.id]['host_profile'] = 'host_' + str(image.host_type.id)
+ image_filter[image.id]['name'] = image.name
+
+ return {'installer_filter': json.dumps(installer_filter),
+ 'scenario_filter': json.dumps(scenario_filter),
+ 'image_filter': json.dumps(image_filter)}
diff --git a/dashboard/src/booking/urls.py b/dashboard/src/booking/urls.py
index 4d00b7f..c6504e0 100644
--- a/dashboard/src/booking/urls.py
+++ b/dashboard/src/booking/urls.py
@@ -32,7 +32,8 @@ from booking.views import (
bookingDelete,
BookingListView,
booking_stats_view,
- booking_stats_json
+ booking_stats_json,
+ quick_create
)
app_name = "booking"
@@ -50,4 +51,5 @@ urlpatterns = [
url(r'^list/$', BookingListView.as_view(), name='list'),
url(r'^stats/$', booking_stats_view, name='stats'),
url(r'^stats/json$', booking_stats_json, name='stats_json'),
+ url(r'^quick/$', quick_create, name='quick_create'),
]
diff --git a/dashboard/src/booking/views.py b/dashboard/src/booking/views.py
index 29b53e2..bc1d2c9 100644
--- a/dashboard/src/booking/views.py
+++ b/dashboard/src/booking/views.py
@@ -15,28 +15,60 @@ from django.utils import timezone
from django.views import View
from django.views.generic import TemplateView
from django.shortcuts import redirect, render
-import json
-from resource_inventory.models import ResourceBundle
+from account.models import Lab
from resource_inventory.resource_manager import ResourceManager
-from booking.models import Booking, Installer, Opsys
+from resource_inventory.models import ResourceBundle
+from booking.models import Booking
from booking.stats import StatisticsManager
+from workflow.views import login
+from booking.forms import QuickBookingForm
+from booking.quick_deployer import create_from_form, drop_filter
+
+
+def quick_create_clear_fields(request):
+ request.session['quick_create_forminfo'] = None
+
+
+def quick_create(request):
+ if not request.user.is_authenticated:
+ return login(request)
+
+ if request.method == 'GET':
+ context = {}
+
+ r_manager = ResourceManager.getInstance()
+ profiles = {}
+ for lab in Lab.objects.all():
+ profiles[str(lab)] = r_manager.getAvailableHostTypes(lab)
+
+ context['lab_profile_map'] = profiles
+
+ context['form'] = QuickBookingForm(initial={}, chosen_users=[], default_user=request.user.username, user=request.user)
+ context.update(drop_filter(request.user))
-def drop_filter(context):
- installer_filter = {}
- for os in Opsys.objects.all():
- installer_filter[os.id] = []
- for installer in os.sup_installers.all():
- installer_filter[os.id].append(installer.id)
+ return render(request, 'booking/quick_deploy.html', context)
+ if request.method == 'POST':
+ form = QuickBookingForm(request.POST, user=request.user)
+ context = {}
+ context['lab_profile_map'] = {}
+ context['form'] = form
- scenario_filter = {}
- for installer in Installer.objects.all():
- scenario_filter[installer.id] = []
- for scenario in installer.sup_scenarios.all():
- scenario_filter[installer.id].append(scenario.id)
+ if form.is_valid():
+ try:
+ create_from_form(form, request)
+ except Exception as e:
+ messages.error(request, "Whoops, looks like an error occurred. "
+ "Let the admins know that you got the following message: " + str(e))
+ return render(request, 'workflow/exit_redirect.html', context)
- context.update({'installer_filter': json.dumps(installer_filter), 'scenario_filter': json.dumps(scenario_filter)})
+ messages.success(request, "We've processed your request. "
+ "Check Account->My Bookings for the status of your new booking")
+ return render(request, 'workflow/exit_redirect.html', context)
+ else:
+ messages.error(request, "Looks like the form didn't validate. Check that you entered everything correctly")
+ return render(request, 'booking/quick_deploy.html', context)
class BookingView(TemplateView):
@@ -128,6 +160,6 @@ def booking_stats_view(request):
def booking_stats_json(request):
try:
span = int(request.GET.get("days", 14))
- except:
+ except Exception:
span = 14
return JsonResponse(StatisticsManager.getContinuousBookingTimeSeries(span), safe=False)
diff --git a/dashboard/src/resource_inventory/migrations/0005_image_os.py b/dashboard/src/resource_inventory/migrations/0005_image_os.py
new file mode 100644
index 0000000..ede008e
--- /dev/null
+++ b/dashboard/src/resource_inventory/migrations/0005_image_os.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.1 on 2019-01-10 16:18
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0004_auto_20181017_1532'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='image',
+ name='os',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Opsys'),
+ ),
+ ]
diff --git a/dashboard/src/resource_inventory/models.py b/dashboard/src/resource_inventory/models.py
index b56317b..5b07077 100644
--- a/dashboard/src/resource_inventory/models.py
+++ b/dashboard/src/resource_inventory/models.py
@@ -25,7 +25,7 @@ class HostProfile(models.Model):
labs = models.ManyToManyField(Lab, related_name="hostprofiles")
def validate(self):
- validname = re.compile("^[A-Za-z0-9\-\_\.\/\, ]+$")
+ validname = re.compile(r"^[A-Za-z0-9\-\_\.\/\, ]+$")
if not validname.match(self.name):
return "Invalid host profile name given. Name must only use A-Z, a-z, 0-9, hyphens, underscores, dots, commas, or spaces."
else:
@@ -147,7 +147,7 @@ class GenericResourceBundle(models.Model):
class GenericResource(models.Model):
bundle = models.ForeignKey(GenericResourceBundle, related_name='generic_resources', on_delete=models.DO_NOTHING)
- hostname_validchars = RegexValidator(regex='(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))', message="Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)")
+ hostname_validchars = RegexValidator(regex=r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))', message="Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)")
name = models.CharField(max_length=200, validators=[hostname_validchars])
def getHost(self):
@@ -157,7 +157,7 @@ class GenericResource(models.Model):
return self.name
def validate(self):
- validname = re.compile('(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))')
+ validname = re.compile(r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))')
if not validname.match(self.name):
return "Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)"
else:
@@ -265,6 +265,7 @@ class Image(models.Model):
# may need to change host_type.on_delete to models.SET() once images are transferrable between compatible host types
host_type = models.ForeignKey(HostProfile, on_delete=models.CASCADE)
description = models.TextField()
+ os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE)
def __str__(self):
return self.name
diff --git a/dashboard/src/resource_inventory/resource_manager.py b/dashboard/src/resource_inventory/resource_manager.py
index 9282580..812fcd7 100644
--- a/dashboard/src/resource_inventory/resource_manager.py
+++ b/dashboard/src/resource_inventory/resource_manager.py
@@ -17,7 +17,7 @@ from dashboard.exceptions import (
ResourceProvisioningException,
ModelValidationException,
)
-from resource_inventory.models import Host, HostConfiguration, ResourceBundle
+from resource_inventory.models import Host, HostConfiguration, ResourceBundle, HostProfile
class ResourceManager:
@@ -33,6 +33,11 @@ class ResourceManager:
ResourceManager.instance = ResourceManager()
return ResourceManager.instance
+ def getAvailableHostTypes(self, lab):
+ hostset = Host.objects.filter(lab=lab).filter(booked=False).filter(working=True)
+ hostprofileset = HostProfile.objects.filter(host__in=hostset, labs=lab)
+ return set(hostprofileset)
+
# public interface
def deleteResourceBundle(self, resourceBundle):
for host in Host.objects.filter(bundle=resourceBundle):
@@ -70,12 +75,12 @@ class ResourceManager:
physical_hosts.append(physical_host)
self.configureNetworking(physical_host)
- except:
+ except Exception:
self.fail_acquire(physical_hosts)
raise ResourceProvisioningException("Network configuration failed.")
try:
physical_host.save()
- except:
+ except Exception:
self.fail_acquire(physical_hosts)
raise ModelValidationException("Saving hosts failed")
diff --git a/dashboard/src/templates/booking/quick_deploy.html b/dashboard/src/templates/booking/quick_deploy.html
new file mode 100644
index 0000000..3837315
--- /dev/null
+++ b/dashboard/src/templates/booking/quick_deploy.html
@@ -0,0 +1,206 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+{% load bootstrap3 %}
+{% block content %}
+<style>
+ .grid_container {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ padding: 30px;
+ }
+ .grid_element {
+ border-radius: 3px;
+ box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75);
+ margin: 10px;
+ padding: 7px;
+ }
+ .grid_element_wide {
+ grid-column-start: span 12;
+ }
+ .grid_element_half {
+ grid-column-start: span 6;
+ }
+ .grid_element_1third {
+ grid-column-start: span 4;
+ }
+ .grid_element_2third {
+ grid-column-start: span 8;
+ }
+</style>
+{% bootstrap_form_errors form type='non_fields' %}
+<form id="quick_booking_form" action="/booking/quick/" method="POST" class="form">
+{% csrf_token %}
+<div class="grid_container">
+<div class="grid_element host_select_pane grid_element_wide">
+<p>Please select a host type you wish to book. Only available types are shown.</p>
+{% bootstrap_field form.filter_field %}
+</div>
+<div class="grid_element booking_info_pane grid_element_1third">
+ {% bootstrap_field form.purpose %}
+ {% bootstrap_field form.project %}
+ {% bootstrap_field form.length %}
+ <p style="display:inline;">Days: </p><output id="daysout" style="display:inline;">0</output>
+ <script>
+ document.getElementById("id_length").setAttribute("oninput", "daysout.value=this.value");
+ document.getElementById("daysout").value = document.getElementById("id_length").value;
+ </script>
+</div>
+<div class="grid_element collaborator_pane grid_element_1third">
+ <label>Collaborators</label>
+ {{ form.users }}
+</div>
+<div class="grid_element configuration_pane grid_element_1third">
+ {% bootstrap_field form.hostname %}
+ {% bootstrap_field form.image %}
+ {% bootstrap_field form.installer %}
+ {% bootstrap_field form.scenario %}
+</div>
+</div>
+<script type="text/javascript">
+ var normalize = function(data)
+ {
+ //converts the top level keys in data to map to lists
+ var normalized = {}
+ for( var key in data ){
+ normalized[key] = [];
+ for( var subkey in data[key] ){
+ normalized[key].push(data[key][subkey]);
+ }
+ }
+ return normalized;
+ }
+ var update_page_contents = function(response)
+ {
+ document.open();
+ document.write(response);
+ document.close();
+ }
+
+ //form hamdler code
+ submit_form = function()
+ {
+ //altered from initial prototype: form submits automatically,
+ //but needs formatting for multiple select field
+ var data = normalize(result);
+ data = JSON.stringify(data);
+ document.getElementById("filter_field").value = data;
+ }
+
+ var sup_image_dict = {{ image_filter|safe }};
+ var sup_installer_dict = {{ installer_filter|safe }};
+ var sup_scenario_dict = {{ scenario_filter|safe }};
+
+ function imageHider() {
+ var data = normalize(result);
+ var drop = document.getElementById("id_image");
+ for( var i=0; i < drop.length; i++ )
+ {
+ if ( drop.options[i].text == '---------' )
+ {
+ drop.selectedIndex = i;
+ }
+ }
+
+ $('#id_image').children().hide();
+
+ var empty_map = {}
+
+ for ( var i=0; i < drop.childNodes.length; i++ )
+ {
+ var image_object = sup_image_dict[drop.childNodes[i].value];
+ if( image_object ) //weed out empty option
+ {
+ var lab_pk = ""
+ for( var j in data["labs"][0] )
+ {
+ if( j in {} ) { continue; }
+ else { lab_pk = j; break; }
+ }
+ var host_pk = "";
+ for( var j in data["hosts"][0] )
+ {
+ if( j in {} ) { continue; }
+ else { host_pk = j; break; }
+ }
+ if( image_object.host_profile == host_pk && image_object.lab == lab_pk )
+ {
+ drop.childNodes[i].style.display = "inherit";
+ }
+ }
+ }
+ }
+
+ $('#id_image').children().hide();
+ $('#id_installer').children().hide();
+ $('#id_scenario').children().hide();
+
+
+ Array.from(document.getElementsByClassName("grid-item-select-btn")).forEach(function(element) {
+ element.addEventListener('click', imageHider);
+ });
+
+ function installerHider() {
+ dropFilter("id_installer", sup_installer_dict, "id_image");
+ scenarioHider();
+ }
+ document.getElementById('id_image').addEventListener('change', installerHider);
+
+ function scenarioHider() {
+ dropFilter("id_scenario", sup_scenario_dict, "id_installer");
+ }
+ document.getElementById('id_installer').addEventListener('change', scenarioHider);
+
+ function dropFilter(target, target_filter, master) {
+ ob = document.getElementById(target);
+
+ for(var i=0; i<ob.options.length; i++) {
+ if ( ob.options[i].text == '---------' ) {
+ ob.selectedIndex = i;
+ }
+ }
+
+ targ_id = "#" + target;
+ $(targ_id).children().hide();
+ var drop = document.getElementById(master);
+ var opts = target_filter[drop.options[drop.selectedIndex].value];
+ if (!opts) {
+ opts = {};
+ }
+ var emptyMap = {}
+
+ var map = Object.create(null);
+ for (var i = 0; i < opts.length; i++) {
+ var j = opts[i];
+ map[j] = true;
+ }
+
+ for (var i = 0; i < document.getElementById(target).childNodes.length; i++) {
+ if (document.getElementById(target).childNodes[i].value in opts && !(document.getElementById(target).childNodes[i].value in emptyMap) ) {
+ document.getElementById(target).childNodes[i].style.display = "inherit";
+ }
+ }
+ }
+</script>
+ <button onclick="submit_form();" class="btn btn-success">Confirm</button>
+</form>
+<script>
+ //context vars
+ var prefill_host_selection = "{{host_select_field_prefill_data|default:""|safe}}";
+ var prefill_purpose = "{{prefill_purpose|default:""|safe}}";
+ var prefill_project = "{{prefill_project|default:""|safe}}";
+ var prefill_hostname = "{{prefill_hostname|default:""|safe}}";
+
+ //to handle prefill
+ function prefill_host_select_field(data)
+ {
+ //
+ if(data)
+ {
+ make_selection(data);
+ }
+ }
+
+ //call init functions
+ prefill_host_select_field(prefill_host_selection);
+</script>
+{% endblock %}
diff --git a/dashboard/src/templates/dashboard/landing.html b/dashboard/src/templates/dashboard/landing.html
index 40e0146..6bbb25b 100644
--- a/dashboard/src/templates/dashboard/landing.html
+++ b/dashboard/src/templates/dashboard/landing.html
@@ -39,6 +39,7 @@
</style>
{% if not request.user.is_anonymous %}
<div class='wf_create_div'>
+<a class="wf_create btn btn-primary" style="color: #FFF;" href="/booking/quick/">Create a Quick Booking</a>
<button class="wf_create btn btn-primary" onclick="cwf(0)">Create a Booking</button>
<button class="wf_create btn btn-primary" onclick="cwf(1)">Create a Pod</button>
<button class="wf_create btn btn-primary" onclick="cwf(2)">Configure a Pod</button>
diff --git a/dashboard/src/templates/dashboard/multiple_select_filter_widget.html b/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
index 31b8f33..9e33896 100644
--- a/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
+++ b/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
@@ -97,7 +97,7 @@
<script>
var initialized = false;
var mapping = {{ mapping|safe }};
-var items = {{ items|safe }};
+var filter_items = {{ filter_items|safe }};
var result = {};
var selection = {{selection_data|default_if_none:"null"|safe}};
var dropdown_count = 0;
@@ -108,31 +108,32 @@ make_selection({{selection_data|safe}});
function make_selection( selection_data ){
if(!initialized) {
- init();
+ filter_field_init();
}
for(var k in selection_data) {
selected_items = selection_data[k];
- for( var item in selected_items ){
- var node = items[item];
+ for( var selected_item in selected_items ){
+ var node = filter_items[selected_item];
if(!node['multiple']){
- var input_value = selected_items[item];
+ var input_value = selected_items[selected_item];
if( input_value != 'false' ) {
select(node);
markAndSweep(node);
}
- var div = document.getElementById(item)
+ var div = document.getElementById(selected_item)
+ var inputs = div.parentNode.getElementsByTagName("input")
var input = div.parentNode.getElementsByTagName("input")[0]
input.value = input_value;
- updateResult(item);
+ updateResult(selected_item);
} else {
- make_multiple_selection(selected_items, item);
+ make_multiple_selection(selected_items, selected_item);
}
}
}
}
function make_multiple_selection(data, item_class){
- var node = items[item_class];
+ var node = filter_items[item_class];
select(node);
markAndSweep(node);
prepop_data = data[item_class];
@@ -143,8 +144,8 @@ function make_multiple_selection(data, item_class){
}
function markAndSweep(root){
- for(var nodeId in items) {
- node = items[nodeId];
+ for(var nodeId in filter_items) {
+ node = filter_items[nodeId];
node['marked'] = true; //mark all nodes
//clears grey background of everything
}
@@ -164,17 +165,17 @@ function markAndSweep(root){
var neighbors = mapping[mappingId];
for(var neighId in neighbors) {
neighId = neighbors[neighId];
- var neighbor = items[neighId];
+ var neighbor = filter_items[neighId];
toCheck.push(neighbor);
}
}
}
//now remove all nodes still marked
- for(var nodeId in items){
- node = items[nodeId];
+ for(var nodeId in filter_items){
+ node = filter_items[nodeId];
if(node['marked']){
- disable(node);
+ disable_node(node);
}
}
}
@@ -186,8 +187,8 @@ function process(node) {
else {
var selected = []
//remember the currently selected, then reset everything and reselect one at a time
- for(var nodeId in items) {
- node = items[nodeId];
+ for(var nodeId in filter_items) {
+ node = filter_items[nodeId];
if(node['selected']) {
selected.push(node);
}
@@ -205,9 +206,9 @@ function process(node) {
function select(node) {
elem = document.getElementById(node['id']);
node['selected'] = true;
- elem.classList.remove('cleared_node')
- elem.classList.remove('disabled_node')
- elem.classList.add('selected_node')
+ elem.classList.remove('cleared_node');
+ elem.classList.remove('disabled_node');
+ elem.classList.add('selected_node');
var input = elem.parentNode.getElementsByTagName("input")[0];
input.disabled = false;
input.value = true;
@@ -218,27 +219,27 @@ function clear(node) {
node['selected'] = false;
node['selectable'] = true;
elem.classList.add('cleared_node')
- elem.classList.remove('disabled_node')
- elem.classList.remove('selected_node')
+ elem.classList.remove('disabled_node');
+ elem.classList.remove('selected_node');
elem.parentNode.getElementsByTagName("input")[0].disabled = true;
}
-function disable(node) {
+function disable_node(node) {
elem = document.getElementById(node['id']);
node['selected'] = false;
node['selectable'] = false;
- elem.classList.remove('cleared_node')
- elem.classList.add('disabled_node')
- elem.classList.remove('selected_node')
+ elem.classList.remove('cleared_node');
+ elem.classList.add('disabled_node');
+ elem.classList.remove('selected_node');
elem.parentNode.getElementsByTagName("input")[0].disabled = true;
}
function processClick(id, multiple){
if(!initialized){
- init();
+ filter_field_init();
}
var element = document.getElementById(id);
- var node = items[id];
+ var node = filter_items[id];
if(!node['selectable']){
return;
}
@@ -259,11 +260,11 @@ function processClick(id, multiple){
function processClickMultipleObject(node){
select(node);
- add_item(node);
+ add_node(node);
process(node);
}
-function add_item(node){
+function add_node(node){
return add_item_prepopulate(node, {});
}
@@ -364,15 +365,15 @@ function remove_dropdown(id){
}
}
if(deselect_class){
- clear(items[div_class]);
+ clear(filter_items[div_class]);
}
}
function updateResult(nodeId){
if(!initialized){
- init();
+ filter_field_init();
}
- if(!items[nodeId]['multiple']){
+ if(!filter_items[nodeId]['multiple']){
var node = document.getElementById(nodeId);
var value = {}
value[nodeId] = node.parentNode.getElementsByTagName("input")[0].value;
@@ -391,10 +392,10 @@ function updateObjectResult(parentElem){
result[node_type][parentElem.id] = input;
}
-function init() {
- for(nodeId in items) {
+function filter_field_init() {
+ for(nodeId in filter_items) {
element = document.getElementById(nodeId);
- node = items[nodeId];
+ node = filter_items[nodeId];
result[element.parentNode.parentNode.id] = {}
}
initialized = true;
diff --git a/dashboard/src/templates/dashboard/searchable_select_multiple.html b/dashboard/src/templates/dashboard/searchable_select_multiple.html
index ee460dd..c08fbe5 100644
--- a/dashboard/src/templates/dashboard/searchable_select_multiple.html
+++ b/dashboard/src/templates/dashboard/searchable_select_multiple.html
@@ -116,6 +116,7 @@
string_trie.isComplete = false;
var added_items = [];
+ var initial_log = {{ initial|safe }};
var added_template = {{ added_list|default:"{}" }};
@@ -128,7 +129,7 @@
entry_p.innerText = default_entry;
}
- init();
+ search_field_init();
if( show_from_noentry )
{
@@ -149,7 +150,7 @@
}
}
- function init() {
+ function search_field_init() {
build_all_tries(items);
var initial = {{ initial|safe }};
@@ -342,14 +343,12 @@
added_items.push(item);
}
}
-
update_selected_list();
document.getElementById("user_field").focus();
}
function remove_item(item_ref)
{
-
item = Object.values(items)[item_ref];
var index = added_items.indexOf(item);
added_items.splice(index, 1);
diff --git a/dashboard/src/workflow/forms.py b/dashboard/src/workflow/forms.py
index f781663..b8c7f66 100644
--- a/dashboard/src/workflow/forms.py
+++ b/dashboard/src/workflow/forms.py
@@ -330,7 +330,7 @@ class MultipleSelectFilterField(forms.Field):
class FormUtils:
@staticmethod
- def getLabData():
+ def getLabData(multiple_selectable_hosts):
"""
Gets all labs and thier host profiles and returns a serialized version the form can understand.
Should be rewritten with a related query to make it faster
@@ -361,7 +361,7 @@ class FormUtils:
shost['selected'] = 0
shost['selectable'] = 1
shost['follow'] = 0
- shost['multiple'] = 1
+ shost['multiple'] = multiple_selectable_hosts
items[shost['id']] = shost
mapping[slab['id']].append(shost['id'])
if shost['id'] not in mapping:
@@ -374,7 +374,7 @@ class FormUtils:
context = {
'filter_objects': filter_objects,
'mapping': mapping,
- 'items': items
+ 'filter_items': items
}
return context
@@ -384,7 +384,7 @@ class HardwareDefinitionForm(forms.Form):
def __init__(self, *args, **kwargs):
selection_data = kwargs.pop("selection_data", False)
super(HardwareDefinitionForm, self).__init__(*args, **kwargs)
- attrs = FormUtils.getLabData()
+ attrs = FormUtils.getLabData(1)
attrs['selection_data'] = selection_data
self.fields['filter_field'] = MultipleSelectFilterField(
widget=MultipleSelectFilterWidget(