aboutsummaryrefslogtreecommitdiffstats
path: root/src/booking
diff options
context:
space:
mode:
authorParker Berberian <pberberian@iol.unh.edu>2019-01-18 16:30:27 +0000
committerGerrit Code Review <gerrit@opnfv.org>2019-01-18 16:30:27 +0000
commit9fc7bfe1b88a720b381ef49bb98cc594924de605 (patch)
treed22b902a67a0cb9c9c6f49d0bfe05e390080e327 /src/booking
parent184dd8ad3a2e2b58f7d25ac6fa1e7ac80c1c5511 (diff)
parentd8e2dbb57cc90ebdffb9ca463b91948b9b634918 (diff)
Merge "Add Quick-Booking Workflow"
Diffstat (limited to 'src/booking')
-rw-r--r--src/booking/forms.py106
-rw-r--r--src/booking/migrations/0003_auto_20190115_1733.py30
-rw-r--r--src/booking/models.py26
-rw-r--r--src/booking/quick_deployer.py272
-rw-r--r--src/booking/urls.py4
-rw-r--r--src/booking/views.py64
6 files changed, 459 insertions, 43 deletions
diff --git a/src/booking/forms.py b/src/booking/forms.py
new file mode 100644
index 0000000..cb76383
--- /dev/null
+++ b/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/src/booking/migrations/0003_auto_20190115_1733.py b/src/booking/migrations/0003_auto_20190115_1733.py
new file mode 100644
index 0000000..70eecfe
--- /dev/null
+++ b/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/src/booking/models.py b/src/booking/models.py
index 74b766d..0972922 100644
--- a/src/booking/models.py
+++ b/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/src/booking/quick_deployer.py b/src/booking/quick_deployer.py
new file mode 100644
index 0000000..9bc8c66
--- /dev/null
+++ b/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/src/booking/urls.py b/src/booking/urls.py
index 4d00b7f..c6504e0 100644
--- a/src/booking/urls.py
+++ b/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/src/booking/views.py b/src/booking/views.py
index 29b53e2..bc1d2c9 100644
--- a/src/booking/views.py
+++ b/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)