aboutsummaryrefslogtreecommitdiffstats
path: root/src/booking
diff options
context:
space:
mode:
Diffstat (limited to 'src/booking')
-rw-r--r--src/booking/__init__.py8
-rw-r--r--src/booking/admin.py16
-rw-r--r--src/booking/apps.py15
-rw-r--r--src/booking/forms.py104
-rw-r--r--src/booking/lib.py36
-rw-r--r--src/booking/migrations/0001_initial.py68
-rw-r--r--src/booking/migrations/0002_booking_pdf.py18
-rw-r--r--src/booking/migrations/0003_auto_20190115_1733.py30
-rw-r--r--src/booking/migrations/0004_auto_20190124_1700.py20
-rw-r--r--src/booking/migrations/0005_booking_idf.py18
-rw-r--r--src/booking/migrations/0006_booking_opnfv_config.py20
-rw-r--r--src/booking/migrations/__init__.py0
-rw-r--r--src/booking/models.py65
-rw-r--r--src/booking/quick_deployer.py355
-rw-r--r--src/booking/stats.py58
-rw-r--r--src/booking/tests/__init__.py8
-rw-r--r--src/booking/tests/test_models.py232
-rw-r--r--src/booking/tests/test_quick_booking.py150
-rw-r--r--src/booking/urls.py54
-rw-r--r--src/booking/views.py201
20 files changed, 1476 insertions, 0 deletions
diff --git a/src/booking/__init__.py b/src/booking/__init__.py
new file mode 100644
index 0000000..b6fef6c
--- /dev/null
+++ b/src/booking/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt 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
+##############################################################################
diff --git a/src/booking/admin.py b/src/booking/admin.py
new file mode 100644
index 0000000..162777e
--- /dev/null
+++ b/src/booking/admin.py
@@ -0,0 +1,16 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+# 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
+##############################################################################
+
+
+from django.contrib import admin
+
+from booking.models import Booking
+
+admin.site.register(Booking)
diff --git a/src/booking/apps.py b/src/booking/apps.py
new file mode 100644
index 0000000..99bf115
--- /dev/null
+++ b/src/booking/apps.py
@@ -0,0 +1,15 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt 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
+##############################################################################
+
+
+from django.apps import AppConfig
+
+
+class BookingConfig(AppConfig):
+ name = 'booking'
diff --git a/src/booking/forms.py b/src/booking/forms.py
new file mode 100644
index 0000000..df88cc6
--- /dev/null
+++ b/src/booking/forms.py
@@ -0,0 +1,104 @@
+##############################################################################
+# 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 django.forms as forms
+from django.forms.widgets import NumberInput
+
+from workflow.forms import (
+ MultipleSelectFilterField,
+ MultipleSelectFilterWidget,
+ FormUtils)
+from account.models import UserProfile
+from resource_inventory.models import Image, Installer, Scenario
+from workflow.forms import SearchableSelectMultipleField
+from booking.lib import get_user_items, get_user_field_opts
+
+
+class QuickBookingForm(forms.Form):
+ purpose = forms.CharField(max_length=1000)
+ project = forms.CharField(max_length=400)
+ 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, user=None, *args, **kwargs):
+ if "default_user" in kwargs:
+ default_user = kwargs.pop("default_user")
+ else:
+ default_user = "you"
+ self.default_user = default_user
+
+ super(QuickBookingForm, self).__init__(data=data, **kwargs)
+
+ self.fields["image"] = forms.ModelChoiceField(
+ Image.objects.filter(public=True) | Image.objects.filter(owner=user)
+ )
+
+ self.fields['users'] = SearchableSelectMultipleField(
+ queryset=UserProfile.objects.select_related('user').exclude(user=user),
+ items=get_user_items(exclude=user),
+ required=False,
+ **get_user_field_opts()
+ )
+
+ attrs = FormUtils.getLabData(0)
+ self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**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
+
+
+class HostReImageForm(forms.Form):
+
+ image_id = forms.IntegerField()
+ host_id = forms.IntegerField()
diff --git a/src/booking/lib.py b/src/booking/lib.py
new file mode 100644
index 0000000..8132c75
--- /dev/null
+++ b/src/booking/lib.py
@@ -0,0 +1,36 @@
+##############################################################################
+# Copyright (c) 2019 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
+##############################################################################
+
+from account.models import UserProfile
+
+
+def get_user_field_opts():
+ return {
+ 'show_from_noentry': False,
+ 'show_x_results': 5,
+ 'results_scrollable': True,
+ 'selectable_limit': -1,
+ 'placeholder': 'Search for other users',
+ 'name': 'users',
+ 'disabled': False
+ }
+
+
+def get_user_items(exclude=None):
+ qs = UserProfile.objects.select_related('user').exclude(user=exclude)
+ items = {}
+ for up in qs:
+ item = {
+ 'id': up.id,
+ 'expanded_name': up.full_name,
+ 'small_name': up.user.username,
+ 'string': up.email_addr
+ }
+ items[up.id] = item
+ return items
diff --git a/src/booking/migrations/0001_initial.py b/src/booking/migrations/0001_initial.py
new file mode 100644
index 0000000..20415fe
--- /dev/null
+++ b/src/booking/migrations/0001_initial.py
@@ -0,0 +1,68 @@
+# Generated by Django 2.1 on 2018-09-14 14:48
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('account', '0001_initial'),
+ ('resource_inventory', '__first__'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Booking',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('start', models.DateTimeField()),
+ ('end', models.DateTimeField()),
+ ('reset', models.BooleanField(default=False)),
+ ('jira_issue_id', models.IntegerField(blank=True, null=True)),
+ ('jira_issue_status', models.CharField(blank=True, max_length=50)),
+ ('purpose', models.CharField(max_length=300)),
+ ('ext_count', models.IntegerField(default=2)),
+ ('project', models.CharField(blank=True, default='', max_length=100, null=True)),
+ ('collaborators', models.ManyToManyField(related_name='collaborators', to=settings.AUTH_USER_MODEL)),
+ ('config_bundle', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ConfigBundle')),
+ ('lab', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='account.Lab')),
+ ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL)),
+ ('resource', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceBundle')),
+ ],
+ options={
+ 'db_table': 'booking',
+ },
+ ),
+ migrations.CreateModel(
+ name='Installer',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=30)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Opsys',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=100)),
+ ('sup_installers', models.ManyToManyField(blank=True, to='booking.Installer')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Scenario',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=300)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='installer',
+ name='sup_scenarios',
+ field=models.ManyToManyField(blank=True, to='booking.Scenario'),
+ ),
+ ]
diff --git a/src/booking/migrations/0002_booking_pdf.py b/src/booking/migrations/0002_booking_pdf.py
new file mode 100644
index 0000000..53232c9
--- /dev/null
+++ b/src/booking/migrations/0002_booking_pdf.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2018-11-09 16:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='booking',
+ name='pdf',
+ field=models.TextField(blank=True, default=''),
+ ),
+ ]
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/migrations/0004_auto_20190124_1700.py b/src/booking/migrations/0004_auto_20190124_1700.py
new file mode 100644
index 0000000..baa32d2
--- /dev/null
+++ b/src/booking/migrations/0004_auto_20190124_1700.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1 on 2019-01-24 17:00
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0003_auto_20190115_1733'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='booking',
+ name='owner',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='owner', to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/src/booking/migrations/0005_booking_idf.py b/src/booking/migrations/0005_booking_idf.py
new file mode 100644
index 0000000..31e9170
--- /dev/null
+++ b/src/booking/migrations/0005_booking_idf.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2019-04-12 19:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0004_auto_20190124_1700'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='booking',
+ name='idf',
+ field=models.TextField(blank=True, default=''),
+ ),
+ ]
diff --git a/src/booking/migrations/0006_booking_opnfv_config.py b/src/booking/migrations/0006_booking_opnfv_config.py
new file mode 100644
index 0000000..e5ffc71
--- /dev/null
+++ b/src/booking/migrations/0006_booking_opnfv_config.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1 on 2019-05-01 18:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0010_auto_20190430_1405'),
+ ('booking', '0005_booking_idf'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='booking',
+ name='opnfv_config',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.OPNFVConfig'),
+ ),
+ ]
diff --git a/src/booking/migrations/__init__.py b/src/booking/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/booking/migrations/__init__.py
diff --git a/src/booking/models.py b/src/booking/models.py
new file mode 100644
index 0000000..9836730
--- /dev/null
+++ b/src/booking/models.py
@@ -0,0 +1,65 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+# 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
+##############################################################################
+
+
+from resource_inventory.models import ResourceBundle, ConfigBundle, OPNFVConfig
+from account.models import Lab
+from django.contrib.auth.models import User
+from django.db import models
+import resource_inventory.resource_manager
+
+
+class Booking(models.Model):
+ id = models.AutoField(primary_key=True)
+ owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='owner')
+ collaborators = models.ManyToManyField(User, related_name='collaborators')
+ start = models.DateTimeField()
+ end = models.DateTimeField()
+ reset = models.BooleanField(default=False)
+ jira_issue_id = models.IntegerField(null=True, blank=True)
+ jira_issue_status = models.CharField(max_length=50, blank=True)
+ purpose = models.CharField(max_length=300, blank=False)
+ ext_count = models.IntegerField(default=2)
+ resource = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True)
+ config_bundle = models.ForeignKey(ConfigBundle, on_delete=models.SET_NULL, null=True)
+ opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.SET_NULL, null=True)
+ project = models.CharField(max_length=100, default="", blank=True, null=True)
+ lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL)
+ pdf = models.TextField(blank=True, default="")
+ idf = models.TextField(blank=True, default="")
+
+ class Meta:
+ db_table = 'booking'
+
+ def save(self, *args, **kwargs):
+ """
+ Save the booking if self.user is authorized and there is no overlapping booking.
+ Raise PermissionError if the user is not authorized
+ Raise ValueError if there is an overlapping booking
+ """
+ if self.start >= self.end:
+ raise ValueError('Start date is after end date')
+ # conflicts end after booking starts, and start before booking ends
+ conflicting_dates = Booking.objects.filter(resource=self.resource).exclude(id=self.id)
+ conflicting_dates = conflicting_dates.filter(end__gt=self.start)
+ conflicting_dates = conflicting_dates.filter(start__lt=self.end)
+ if conflicting_dates.count() > 0:
+ raise ValueError('This booking overlaps with another booking')
+ return super(Booking, self).save(*args, **kwargs)
+
+ def delete(self, *args, **kwargs):
+ res = self.resource
+ self.resource = None
+ self.save()
+ resource_inventory.resource_manager.ResourceManager.getInstance().deleteResourceBundle(res)
+ return super(self.__class__, self).delete(*args, **kwargs)
+
+ def __str__(self):
+ return str(self.purpose) + ' from ' + str(self.start) + ' until ' + str(self.end)
diff --git a/src/booking/quick_deployer.py b/src/booking/quick_deployer.py
new file mode 100644
index 0000000..0e0cc5a
--- /dev/null
+++ b/src/booking/quick_deployer.py
@@ -0,0 +1,355 @@
+##############################################################################
+# 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 datetime import timedelta
+from django.utils import timezone
+from account.models import Lab
+
+from resource_inventory.models import (
+ Installer,
+ Image,
+ GenericResourceBundle,
+ ConfigBundle,
+ Host,
+ HostProfile,
+ HostConfiguration,
+ GenericResource,
+ GenericHost,
+ GenericInterface,
+ OPNFVRole,
+ OPNFVConfig,
+ Network,
+ NetworkConnection,
+ NetworkRole,
+ HostOPNFVConfig,
+)
+from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
+from notifier.manager import NotificationHandler
+from booking.models import Booking
+from dashboard.exceptions import (
+ InvalidHostnameException,
+ ResourceAvailabilityException,
+ ModelValidationException,
+ BookingLengthException
+)
+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
+
+
+class BookingPermissionException(Exception):
+ pass
+
+
+def parse_host_field(host_json):
+ lab, profile = (None, None)
+ lab_dict = host_json['lab']
+ for lab_info in lab_dict.values():
+ if lab_info['selected']:
+ lab = Lab.objects.get(lab_user__id=lab_info['id'])
+
+ host_dict = host_json['host']
+ for host_info in host_dict.values():
+ if host_info['selected']:
+ profile = HostProfile.objects.get(pk=host_info['id'])
+
+ if lab is None:
+ raise NoLabSelectedError("No lab was selected")
+ if profile is None:
+ raise HostProfileDNE("No Host was selected")
+
+ return lab, profile
+
+
+def check_available_matching_host(lab, hostprofile):
+ available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
+ if hostprofile not in available_host_types:
+ # TODO: handle deleting generic resource in this instance along with grb
+ raise HostNotAvailable('Requested host type is not available. Please try again later. Host availability can be viewed in the "Hosts" tab to the left.')
+
+ hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
+ if not hostset.exists():
+ raise HostNotAvailable("Couldn't find any matching unbooked hosts")
+
+ return True
+
+
+def generate_grb(owner, lab, common_id):
+ grbundle = GenericResourceBundle(owner=owner)
+ grbundle.lab = lab
+ grbundle.name = "grbundle for quick booking with uid " + common_id
+ grbundle.description = "grbundle created for quick-deploy booking"
+ grbundle.save()
+
+ return grbundle
+
+
+def generate_gresource(bundle, hostname):
+ if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
+ raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
+ gresource = GenericResource(bundle=bundle, name=hostname)
+ gresource.save()
+
+ return gresource
+
+
+def generate_ghost(generic_resource, host_profile):
+ ghost = GenericHost()
+ ghost.resource = generic_resource
+ ghost.profile = host_profile
+ ghost.save()
+
+ return ghost
+
+
+def generate_config_bundle(owner, common_id, grbundle):
+ cbundle = ConfigBundle()
+ cbundle.owner = owner
+ cbundle.name = "configbundle for quick booking with uid " + common_id
+ cbundle.description = "configbundle created for quick-deploy booking"
+ cbundle.bundle = grbundle
+ cbundle.save()
+
+ return cbundle
+
+
+def generate_opnfvconfig(scenario, installer, config_bundle):
+ opnfvconfig = OPNFVConfig()
+ opnfvconfig.scenario = scenario
+ opnfvconfig.installer = installer
+ opnfvconfig.bundle = config_bundle
+ opnfvconfig.save()
+
+ return opnfvconfig
+
+
+def generate_hostconfig(generic_host, image, config_bundle):
+ hconf = HostConfiguration()
+ hconf.host = generic_host
+ hconf.image = image
+ hconf.bundle = config_bundle
+ hconf.is_head_node = True
+ hconf.save()
+
+ return hconf
+
+
+def generate_hostopnfv(hostconfig, opnfvconfig):
+ config = HostOPNFVConfig()
+ role = None
+ try:
+ role = OPNFVRole.objects.get(name="Jumphost")
+ except Exception:
+ role = OPNFVRole.objects.create(
+ name="Jumphost",
+ description="Single server jumphost role"
+ )
+ config.role = role
+ config.host_config = hostconfig
+ config.opnfv_config = opnfvconfig
+ config.save()
+ return config
+
+
+def generate_resource_bundle(generic_resource_bundle, config_bundle): # warning: requires cleanup
+ try:
+ resource_manager = ResourceManager.getInstance()
+ resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
+ return resource_bundle
+ except ResourceAvailabilityException:
+ raise ResourceAvailabilityException("Requested resources not available")
+ except ModelValidationException:
+ raise ModelValidationException("Encountered error while saving grbundle")
+
+
+def check_invariants(request, **kwargs):
+ installer = kwargs['installer']
+ image = kwargs['image']
+ scenario = kwargs['scenario']
+ lab = kwargs['lab']
+ host_profile = kwargs['host_profile']
+ length = kwargs['length']
+ # 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 != host_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")
+ if length < 1 or length > 21:
+ raise BookingLengthException("Booking must be between 1 and 21 days long")
+
+
+def configure_networking(grb, config):
+ # create network
+ net = Network.objects.create(name="public", bundle=grb, is_public=True)
+ # connect network to generic host
+ grb.getHosts()[0].generic_interfaces.first().connections.add(
+ NetworkConnection.objects.create(network=net, vlan_is_tagged=False)
+ )
+ # asign network role
+ role = NetworkRole.objects.create(name="public", network=net)
+ opnfv_config = config.opnfv_config.first()
+ if opnfv_config:
+ opnfv_config.networks.add(role)
+
+
+def create_from_form(form, request):
+ quick_booking_id = str(uuid.uuid4())
+
+ host_field = form.cleaned_data['filter_field']
+ purpose_field = form.cleaned_data['purpose']
+ project_field = form.cleaned_data['project']
+ users_field = form.cleaned_data['users']
+ hostname = form.cleaned_data['hostname']
+ length = form.cleaned_data['length']
+
+ image = form.cleaned_data['image']
+ scenario = form.cleaned_data['scenario']
+ installer = form.cleaned_data['installer']
+
+ lab, host_profile = parse_host_field(host_field)
+ data = form.cleaned_data
+ data['lab'] = lab
+ data['host_profile'] = host_profile
+ check_invariants(request, **data)
+
+ # check booking privileges
+ if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
+ raise BookingPermissionException("You do not have permission to have more than 3 bookings at a time.")
+
+ check_available_matching_host(lab, host_profile) # requires cleanup if failure after this point
+
+ grbundle = generate_grb(request.user, lab, quick_booking_id)
+ gresource = generate_gresource(grbundle, hostname)
+ ghost = generate_ghost(gresource, host_profile)
+ cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
+ hconf = generate_hostconfig(ghost, image, cbundle)
+
+ # if no installer provided, just create blank host
+ opnfv_config = None
+ if installer:
+ opnfv_config = generate_opnfvconfig(scenario, installer, cbundle)
+ generate_hostopnfv(hconf, opnfv_config)
+
+ # construct generic interfaces
+ for interface_profile in host_profile.interfaceprofile.all():
+ generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
+ generic_interface.save()
+
+ configure_networking(grbundle, cbundle)
+
+ # generate resource bundle
+ resource_bundle = generate_resource_bundle(grbundle, cbundle)
+
+ # generate booking
+ booking = Booking.objects.create(
+ purpose=purpose_field,
+ project=project_field,
+ lab=lab,
+ owner=request.user,
+ start=timezone.now(),
+ end=timezone.now() + timedelta(days=int(length)),
+ resource=resource_bundle,
+ config_bundle=cbundle,
+ opnfv_config=opnfv_config
+ )
+ booking.pdf = PDFTemplater.makePDF(booking)
+
+ for collaborator in users_field: # list of UserProfiles
+ booking.collaborators.add(collaborator.user)
+
+ booking.save()
+
+ # generate job
+ JobFactory.makeCompleteJob(booking)
+ NotificationHandler.notify_new_booking(booking)
+
+ return 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/stats.py b/src/booking/stats.py
new file mode 100644
index 0000000..383723a
--- /dev/null
+++ b/src/booking/stats.py
@@ -0,0 +1,58 @@
+##############################################################################
+# 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
+##############################################################################
+from booking.models import Booking
+import datetime
+import pytz
+
+
+class StatisticsManager(object):
+
+ @staticmethod
+ def getContinuousBookingTimeSeries(span=28):
+ """
+ Will return a dictionary of names and 2-D array of x and y data points.
+ e.g. {"plot1": [["x1", "x2", "x3"],["y1", "y2", "y3]]}
+ x values will be dates in string
+ every change (booking start / end) will be reflected,
+ instead of one data point per day
+ y values are the integer number of bookings/users active at
+ some point in the given date span is the number of days to plot.
+ The last x value will always be the current time
+ """
+ data = []
+ x = []
+ y = []
+ users = []
+ now = datetime.datetime.now(pytz.utc)
+ delta = datetime.timedelta(days=span)
+ end = now - delta
+ bookings = Booking.objects.filter(start__lte=now, end__gte=end).prefetch_related("collaborators")
+ for booking in bookings: # collect data from each booking
+ user_list = [u.pk for u in booking.collaborators.all()]
+ user_list.append(booking.owner.pk)
+ data.append((booking.start, 1, user_list))
+ data.append((booking.end, -1, user_list))
+
+ # sort based on time
+ data.sort(key=lambda i: i[0])
+
+ # collect data
+ count = 0
+ active_users = {}
+ for datum in data:
+ x.append(str(datum[0])) # time
+ count += datum[1] # booking count
+ y.append(count)
+ for pk in datum[2]: # maintain count of each user's active bookings
+ active_users[pk] = active_users.setdefault(pk, 0) + datum[1]
+ if active_users[pk] == 0:
+ del active_users[pk]
+ users.append(len([x for x in active_users.values() if x > 0]))
+
+ return {"booking": [x, y], "user": [x, users]}
diff --git a/src/booking/tests/__init__.py b/src/booking/tests/__init__.py
new file mode 100644
index 0000000..b6fef6c
--- /dev/null
+++ b/src/booking/tests/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt 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
+##############################################################################
diff --git a/src/booking/tests/test_models.py b/src/booking/tests/test_models.py
new file mode 100644
index 0000000..6170295
--- /dev/null
+++ b/src/booking/tests/test_models.py
@@ -0,0 +1,232 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+# 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
+##############################################################################
+
+
+from datetime import timedelta
+
+from django.contrib.auth.models import Permission, User
+from django.test import TestCase
+from django.utils import timezone
+
+# from booking.models import *
+from booking.models import Booking
+from resource_inventory.models import ResourceBundle, GenericResourceBundle, ConfigBundle
+
+
+class BookingModelTestCase(TestCase):
+
+ count = 0
+
+ def setUp(self):
+ self.owner = User.objects.create(username='owner')
+
+ self.res1 = ResourceBundle.objects.create(
+ template=GenericResourceBundle.objects.create(
+ name="gbundle" + str(self.count)
+ )
+ )
+ self.count += 1
+ self.res2 = ResourceBundle.objects.create(
+ template=GenericResourceBundle.objects.create(
+ name="gbundle2" + str(self.count)
+ )
+ )
+ self.count += 1
+ self.user1 = User.objects.create(username='user1')
+
+ self.add_booking_perm = Permission.objects.get(codename='add_booking')
+ self.user1.user_permissions.add(self.add_booking_perm)
+
+ self.user1 = User.objects.get(pk=self.user1.id)
+ self.config_bundle = ConfigBundle.objects.create(
+ owner=self.user1,
+ name="test config"
+ )
+
+ def test_start_end(self):
+ """
+ if the start of a booking is greater or equal then the end,
+ saving should raise a ValueException
+ """
+ start = timezone.now()
+ end = start - timedelta(weeks=1)
+ self.assertRaises(
+ ValueError,
+ Booking.objects.create,
+ start=start,
+ end=end,
+ resource=self.res1,
+ owner=self.user1,
+ config_bundle=self.config_bundle
+ )
+ end = start
+ self.assertRaises(
+ ValueError,
+ Booking.objects.create,
+ start=start,
+ end=end,
+ resource=self.res1,
+ owner=self.user1,
+ config_bundle=self.config_bundle
+ )
+
+ def test_conflicts(self):
+ """
+ saving an overlapping booking on the same resource
+ should raise a ValueException
+ saving for different resources should succeed
+ """
+ start = timezone.now()
+ end = start + timedelta(weeks=1)
+ self.assertTrue(
+ Booking.objects.create(
+ start=start,
+ end=end,
+ owner=self.user1,
+ resource=self.res1,
+ config_bundle=self.config_bundle
+ )
+ )
+
+ self.assertRaises(
+ ValueError,
+ Booking.objects.create,
+ start=start,
+ end=end,
+ resource=self.res1,
+ owner=self.user1,
+ config_bundle=self.config_bundle
+ )
+
+ self.assertRaises(
+ ValueError,
+ Booking.objects.create,
+ start=start + timedelta(days=1),
+ end=end - timedelta(days=1),
+ resource=self.res1,
+ owner=self.user1,
+ config_bundle=self.config_bundle
+ )
+
+ self.assertRaises(
+ ValueError,
+ Booking.objects.create,
+ start=start - timedelta(days=1),
+ end=end,
+ resource=self.res1,
+ owner=self.user1,
+ config_bundle=self.config_bundle
+ )
+
+ self.assertRaises(
+ ValueError,
+ Booking.objects.create,
+ start=start - timedelta(days=1),
+ end=end - timedelta(days=1),
+ resource=self.res1,
+ owner=self.user1,
+ config_bundle=self.config_bundle
+ )
+
+ self.assertRaises(
+ ValueError,
+ Booking.objects.create,
+ start=start,
+ end=end + timedelta(days=1),
+ resource=self.res1,
+ owner=self.user1,
+ config_bundle=self.config_bundle
+ )
+
+ self.assertRaises(
+ ValueError,
+ Booking.objects.create,
+ start=start + timedelta(days=1),
+ end=end + timedelta(days=1),
+ resource=self.res1,
+ owner=self.user1,
+ config_bundle=self.config_bundle
+ )
+
+ self.assertTrue(
+ Booking.objects.create(
+ start=start - timedelta(days=1),
+ end=start,
+ owner=self.user1,
+ resource=self.res1,
+ config_bundle=self.config_bundle
+ )
+ )
+
+ self.assertTrue(
+ Booking.objects.create(
+ start=end,
+ end=end + timedelta(days=1),
+ owner=self.user1,
+ resource=self.res1,
+ config_bundle=self.config_bundle
+ )
+ )
+
+ self.assertTrue(
+ Booking.objects.create(
+ start=start - timedelta(days=2),
+ end=start - timedelta(days=1),
+ owner=self.user1,
+ resource=self.res1,
+ config_bundle=self.config_bundle
+ )
+ )
+
+ self.assertTrue(
+ Booking.objects.create(
+ start=end + timedelta(days=1),
+ end=end + timedelta(days=2),
+ owner=self.user1,
+ resource=self.res1,
+ config_bundle=self.config_bundle
+ )
+ )
+
+ self.assertTrue(
+ Booking.objects.create(
+ start=start,
+ end=end,
+ owner=self.user1,
+ resource=self.res2,
+ config_bundle=self.config_bundle
+ )
+ )
+
+ def test_extensions(self):
+ """
+ saving a booking with an extended end time is allows to happen twice,
+ and each extension must be a maximum of one week long
+ """
+ start = timezone.now()
+ end = start + timedelta(weeks=1)
+ self.assertTrue(
+ Booking.objects.create(
+ start=start,
+ end=end,
+ owner=self.user1,
+ resource=self.res1,
+ config_bundle=self.config_bundle
+ )
+ )
+
+ booking = Booking.objects.all().first() # should be only thing in db
+
+ self.assertEquals(booking.ext_count, 2)
+ booking.end = booking.end + timedelta(days=3)
+ try:
+ booking.save()
+ except Exception:
+ self.fail("save() threw an exception")
diff --git a/src/booking/tests/test_quick_booking.py b/src/booking/tests/test_quick_booking.py
new file mode 100644
index 0000000..e445860
--- /dev/null
+++ b/src/booking/tests/test_quick_booking.py
@@ -0,0 +1,150 @@
+##############################################################################
+# 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 datetime
+
+from django.test import TestCase, Client
+
+from booking.models import Booking
+from dashboard.testing_utils import (
+ make_host,
+ make_user,
+ make_user_profile,
+ make_lab,
+ make_installer,
+ make_image,
+ make_scenario,
+ make_os,
+ make_complete_host_profile,
+ make_opnfv_role,
+ make_public_net,
+)
+
+
+class QuickBookingValidFormTestCase(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.user = make_user(False, username="newtestuser", password="testpassword")
+ make_user_profile(cls.user, True)
+
+ lab_user = make_user(True)
+ cls.lab = make_lab(lab_user)
+
+ cls.host_profile = make_complete_host_profile(cls.lab)
+ cls.scenario = make_scenario()
+ cls.installer = make_installer([cls.scenario])
+ os = make_os([cls.installer])
+ cls.image = make_image(cls.lab, 1, cls.user, os, cls.host_profile)
+ cls.host = make_host(cls.host_profile, cls.lab)
+ cls.role = make_opnfv_role()
+ cls.pubnet = make_public_net(10, cls.lab)
+
+ cls.post_data = cls.build_post_data()
+ cls.client = Client()
+
+ @classmethod
+ def build_post_data(cls):
+ return {
+ 'filter_field': '{"hosts":[{"host_' + str(cls.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(cls.lab.lab_user.id) + '":"true"}]}',
+ 'purpose': 'my_purpose',
+ 'project': 'my_project',
+ 'length': '3',
+ 'ignore_this': 1,
+ 'users': '',
+ 'hostname': 'my_host',
+ 'image': str(cls.image.id),
+ 'installer': str(cls.installer.id),
+ 'scenario': str(cls.scenario.id)
+ }
+
+ def post(self, changed_fields={}):
+ payload = self.post_data.copy()
+ payload.update(changed_fields)
+ response = self.client.post('/booking/quick/', payload)
+ return response
+
+ def setUp(self):
+ self.client.login(username=self.user.username, password="testpassword")
+
+ def assertValidBooking(self, booking):
+ self.assertEqual(booking.owner, self.user)
+ self.assertEqual(booking.purpose, 'my_purpose')
+ self.assertEqual(booking.project, 'my_project')
+ delta = booking.end - booking.start
+ delta -= datetime.timedelta(days=3)
+ self.assertLess(delta, datetime.timedelta(minutes=1))
+
+ resource_bundle = booking.resource
+ config_bundle = booking.config_bundle
+
+ opnfv_config = config_bundle.opnfv_config.first()
+ self.assertEqual(self.installer, opnfv_config.installer)
+ self.assertEqual(self.scenario, opnfv_config.scenario)
+
+ host = resource_bundle.hosts.first()
+ self.assertEqual(host.profile, self.host_profile)
+ self.assertEqual(host.template.resource.name, 'my_host')
+
+ def test_with_too_long_length(self):
+ response = self.post({'length': '22'})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_negative_length(self):
+ response = self.post({'length': '-1'})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_installer(self):
+ response = self.post({'installer': str(self.installer.id + 100)})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_scenario(self):
+ response = self.post({'scenario': str(self.scenario.id + 100)})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_host_id(self):
+ response = self.post({'filter_field': '{"hosts":[{"host_' + str(self.host_profile.id + 100) + '":"true"}], "labs": [{"lab_' + str(self.lab.lab_user.id) + '":"true"}]}'})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_lab_id(self):
+ response = self.post({'filter_field': '{"hosts":[{"host_' + str(self.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(self.lab.lab_user.id + 100) + '":"true"}]}'})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_empty_filter_field(self):
+ response = self.post({'filter_field': ''})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_garbage_users_field(self): # expected behavior: treat as though field is empty if it has garbage data
+ response = self.post({'users': 'X�]QP�槰DP�+m���h�U�_�yJA:.rDi��QN|.��C��n�P��F!��D�����5ȅj�9�LV��'}) # output from /dev/urandom
+
+ self.assertEqual(response.status_code, 200)
+ booking = Booking.objects.first()
+ self.assertIsNotNone(booking)
+ self.assertValidBooking(booking)
+
+ def test_with_valid_form(self):
+ response = self.post()
+
+ self.assertEqual(response.status_code, 200)
+ booking = Booking.objects.first()
+ self.assertIsNotNone(booking)
+ self.assertValidBooking(booking)
diff --git a/src/booking/urls.py b/src/booking/urls.py
new file mode 100644
index 0000000..310aaa7
--- /dev/null
+++ b/src/booking/urls.py
@@ -0,0 +1,54 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+# 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
+##############################################################################
+
+
+"""pharos_dashboard URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf.urls import url
+
+from booking.views import (
+ booking_detail_view,
+ BookingDeleteView,
+ bookingDelete,
+ BookingListView,
+ booking_stats_view,
+ booking_stats_json,
+ quick_create,
+ booking_modify_image
+)
+
+app_name = "booking"
+urlpatterns = [
+
+
+ url(r'^detail/(?P<booking_id>[0-9]+)/$', booking_detail_view, name='detail'),
+ url(r'^(?P<booking_id>[0-9]+)/$', booking_detail_view, name='booking_detail'),
+ url(r'^delete/$', BookingDeleteView.as_view(), name='delete_prefix'),
+ url(r'^delete/(?P<booking_id>[0-9]+)/$', BookingDeleteView.as_view(), name='delete'),
+ url(r'^delete/(?P<booking_id>[0-9]+)/confirm/$', bookingDelete, name='delete_booking'),
+ url(r'^modify/(?P<booking_id>[0-9]+)/image/$', booking_modify_image, name='modify_booking_image'),
+ 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
new file mode 100644
index 0000000..bad7dc9
--- /dev/null
+++ b/src/booking/views.py
@@ -0,0 +1,201 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+# 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
+##############################################################################
+
+from django.contrib import messages
+from django.shortcuts import get_object_or_404
+from django.http import JsonResponse, HttpResponse
+from django.utils import timezone
+from django.views import View
+from django.views.generic import TemplateView
+from django.shortcuts import redirect, render
+from django.db.models import Q
+from django.urls import reverse
+
+from resource_inventory.models import ResourceBundle, HostProfile, Image, Host
+from resource_inventory.resource_manager import ResourceManager
+from account.models import Lab
+from booking.models import Booking
+from booking.stats import StatisticsManager
+from booking.forms import HostReImageForm
+from api.models import JobFactory
+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(default_user=request.user.username, user=request.user)
+
+ context.update(drop_filter(request.user))
+
+ 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
+
+ if form.is_valid():
+ try:
+ booking = create_from_form(form, request)
+ messages.success(request, "We've processed your request. "
+ "Check Account->My Bookings for the status of your new booking")
+ return redirect(reverse('booking:booking_detail', kwargs={'booking_id': booking.id}))
+ except Exception as e:
+ messages.error(request, "Whoops, an error occurred: " + str(e))
+ return render(request, 'booking/quick_deploy.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):
+ template_name = "booking/booking_detail.html"
+
+ def get_context_data(self, **kwargs):
+ booking = get_object_or_404(Booking, id=self.kwargs['booking_id'])
+ title = 'Booking Details'
+ context = super(BookingView, self).get_context_data(**kwargs)
+ context.update({'title': title, 'booking': booking})
+ return context
+
+
+class BookingDeleteView(TemplateView):
+ template_name = "booking/booking_delete.html"
+
+ def get_context_data(self, **kwargs):
+ booking = get_object_or_404(Booking, id=self.kwargs['booking_id'])
+ title = 'Delete Booking'
+ context = super(BookingDeleteView, self).get_context_data(**kwargs)
+ context.update({'title': title, 'booking': booking})
+ return context
+
+
+def bookingDelete(request, booking_id):
+ booking = get_object_or_404(Booking, id=booking_id)
+ booking.delete()
+ messages.add_message(request, messages.SUCCESS, 'Booking deleted')
+ return redirect('../../../../')
+
+
+class BookingListView(TemplateView):
+ template_name = "booking/booking_list.html"
+
+ def get_context_data(self, **kwargs):
+ bookings = Booking.objects.filter(end__gte=timezone.now())
+ title = 'Search Booking'
+ context = super(BookingListView, self).get_context_data(**kwargs)
+ context.update({'title': title, 'bookings': bookings})
+ return context
+
+
+class ResourceBookingsJSON(View):
+ def get(self, request, *args, **kwargs):
+ resource = get_object_or_404(ResourceBundle, id=self.kwargs['resource_id'])
+ bookings = resource.booking_set.get_queryset().values(
+ 'id',
+ 'start',
+ 'end',
+ 'purpose',
+ 'jira_issue_status',
+ 'config_bundle__name'
+ )
+ return JsonResponse({'bookings': list(bookings)})
+
+
+def build_image_mapping(lab, user):
+ mapping = {}
+ for profile in HostProfile.objects.filter(labs=lab):
+ images = Image.objects.filter(
+ from_lab=lab,
+ host_type=profile
+ ).filter(
+ Q(public=True) | Q(owner=user)
+ )
+ mapping[profile.name] = [{"name": image.name, "value": image.id} for image in images]
+ return mapping
+
+
+def booking_detail_view(request, booking_id):
+ user = None
+ if request.user.is_authenticated:
+ user = request.user
+ else:
+ return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
+
+ booking = get_object_or_404(Booking, id=booking_id)
+ allowed_users = set(list(booking.collaborators.all()))
+ allowed_users.add(booking.owner)
+ if user not in allowed_users:
+ return render(request, "dashboard/login.html", {'title': 'This page is private'})
+
+ context = {
+ 'title': 'Booking Details',
+ 'booking': booking,
+ 'pdf': booking.pdf,
+ 'user_id': user.id,
+ 'image_mapping': build_image_mapping(booking.lab, user)
+ }
+
+ return render(
+ request,
+ "booking/booking_detail.html",
+ context
+ )
+
+
+def booking_modify_image(request, booking_id):
+ form = HostReImageForm(request.POST)
+ if form.is_valid():
+ booking = Booking.objects.get(id=booking_id)
+ if request.user != booking.owner:
+ return HttpResponse("unauthorized")
+ if timezone.now() > booking.end:
+ return HttpResponse("unauthorized")
+ new_image = Image.objects.get(id=form.cleaned_data['image_id'])
+ host = Host.objects.get(id=form.cleaned_data['host_id'])
+ host.config.image = new_image
+ host.config.save()
+ JobFactory.reimageHost(new_image, booking, host)
+ return HttpResponse(new_image.name)
+ return HttpResponse("error")
+
+
+def booking_stats_view(request):
+ return render(
+ request,
+ "booking/stats.html",
+ context={"data": StatisticsManager.getContinuousBookingTimeSeries(), "title": "Booking Statistics"}
+ )
+
+
+def booking_stats_json(request):
+ try:
+ span = int(request.GET.get("days", 14))
+ except Exception:
+ span = 14
+ return JsonResponse(StatisticsManager.getContinuousBookingTimeSeries(span), safe=False)