Add Quick-Booking Workflow 63/66463/9
authorSawyer Bergeron <sawyerbergeron@gmail.com>
Thu, 17 Jan 2019 16:30:35 +0000 (11:30 -0500)
committerSawyer Bergeron <sawyerbergeron@gmail.com>
Fri, 18 Jan 2019 16:27:53 +0000 (11:27 -0500)
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>
15 files changed:
dashboard/src/account/models.py
dashboard/src/booking/forms.py [new file with mode: 0644]
dashboard/src/booking/migrations/0003_auto_20190115_1733.py [new file with mode: 0644]
dashboard/src/booking/models.py
dashboard/src/booking/quick_deployer.py [new file with mode: 0644]
dashboard/src/booking/urls.py
dashboard/src/booking/views.py
dashboard/src/resource_inventory/migrations/0005_image_os.py [new file with mode: 0644]
dashboard/src/resource_inventory/models.py
dashboard/src/resource_inventory/resource_manager.py
dashboard/src/templates/booking/quick_deploy.html [new file with mode: 0644]
dashboard/src/templates/dashboard/landing.html
dashboard/src/templates/dashboard/multiple_select_filter_widget.html
dashboard/src/templates/dashboard/searchable_select_multiple.html
dashboard/src/workflow/forms.py

index bfeead0..0f8154e 100644 (file)
@@ -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 (file)
index 0000000..cb76383
--- /dev/null
@@ -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 (file)
index 0000000..70eecfe
--- /dev/null
@@ -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',
+        ),
+    ]
index 74b766d..0972922 100644 (file)
@@ -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 (file)
index 0000000..9bc8c66
--- /dev/null
@@ -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)}
index 4d00b7f..c6504e0 100644 (file)
@@ -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'),
 ]
index 29b53e2..bc1d2c9 100644 (file)
@@ -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 (file)
index 0000000..ede008e
--- /dev/null
@@ -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'),
+        ),
+    ]
index b56317b..5b07077 100644 (file)
@@ -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
index 9282580..812fcd7 100644 (file)
@@ -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 (file)
index 0000000..3837315
--- /dev/null
@@ -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 %}
index 40e0146..6bbb25b 100644 (file)
@@ -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>
index 31b8f33..9e33896 100644 (file)
@@ -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;
index ee460dd..c08fbe5 100644 (file)
     string_trie.isComplete = false;
 
     var added_items = [];
+    var initial_log = {{ initial|safe }};
 
     var added_template = {{ added_list|default:"{}" }};
 
         entry_p.innerText = default_entry;
     }
 
-    init();
+    search_field_init();
 
     if( show_from_noentry )
     {
         }
     }
 
-    function init() {
+    function search_field_init() {
         build_all_tries(items);
 
         var initial = {{ initial|safe }};
                 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);
index f781663..b8c7f66 100644 (file)
@@ -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(