aboutsummaryrefslogtreecommitdiffstats
path: root/src/booking
diff options
context:
space:
mode:
Diffstat (limited to 'src/booking')
-rw-r--r--src/booking/forms.py114
-rw-r--r--src/booking/migrations/0010_auto_20230608_1913.py29
-rw-r--r--src/booking/migrations/0011_booking_aggregateid.py19
-rw-r--r--src/booking/models.py33
-rw-r--r--src/booking/quick_deployer.py343
-rw-r--r--src/booking/stats.py109
-rw-r--r--src/booking/tests/__init__.py8
-rw-r--r--src/booking/tests/test_models.py210
-rw-r--r--src/booking/tests/test_quick_booking.py180
-rw-r--r--src/booking/tests/test_stats.py59
-rw-r--r--src/booking/urls.py8
-rw-r--r--src/booking/views.py123
12 files changed, 56 insertions, 1179 deletions
diff --git a/src/booking/forms.py b/src/booking/forms.py
index 9c9b053..c7169bb 100644
--- a/src/booking/forms.py
+++ b/src/booking/forms.py
@@ -7,117 +7,3 @@
# 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)
-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):
- # Django Form class for Express Booking
- purpose = forms.CharField(max_length=1000)
- project = forms.CharField(max_length=400)
- hostname = forms.CharField(required=False, max_length=400)
- global_cloud_config = forms.CharField(widget=forms.Textarea, required=False)
-
- 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, lab_data=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)
-
- image_help_text = 'Image can be set only for single-node bookings. For multi-node bookings set image through Design a POD.'
- self.fields["image"] = forms.ModelChoiceField(
- Image.objects.filter(public=True) | Image.objects.filter(owner=user), required=False
- )
-
- self.fields['image'].widget.attrs.update({
- 'class': 'has-popover',
- 'data-content': image_help_text,
- 'data-placement': 'bottom',
- 'data-container': 'body'
- })
-
- self.fields['users'] = SearchableSelectMultipleField(
- queryset=UserProfile.objects.filter(public_user=True).select_related('user').exclude(user=user),
- items=get_user_items(exclude=user),
- required=False,
- **get_user_field_opts()
- )
-
- self.fields['length'] = forms.IntegerField(
- widget=NumberInput(
- attrs={
- "type": "range",
- 'min': "1",
- "max": "21",
- "value": "1"
- }
- )
- )
-
- self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**lab_data))
-
- hostname_help_text = 'Hostname can be set only for single-node bookings. For multi-node bookings set hostname through Design a POD.'
- self.fields['hostname'].widget.attrs.update({
- 'class': 'has-popover',
- 'data-content': hostname_help_text,
- 'data-placement': 'top',
- 'data-container': 'body'
- })
-
- def build_user_list(self):
- """
- Build list of UserProfiles.
-
- 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/migrations/0010_auto_20230608_1913.py b/src/booking/migrations/0010_auto_20230608_1913.py
new file mode 100644
index 0000000..66bf63b
--- /dev/null
+++ b/src/booking/migrations/0010_auto_20230608_1913.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.2 on 2023-06-08 19:13
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0009_booking_complete'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='booking',
+ name='jira_issue_id',
+ ),
+ migrations.RemoveField(
+ model_name='booking',
+ name='jira_issue_status',
+ ),
+ migrations.RemoveField(
+ model_name='booking',
+ name='opnfv_config',
+ ),
+ migrations.RemoveField(
+ model_name='booking',
+ name='resource',
+ ),
+ ]
diff --git a/src/booking/migrations/0011_booking_aggregateid.py b/src/booking/migrations/0011_booking_aggregateid.py
new file mode 100644
index 0000000..111b36b
--- /dev/null
+++ b/src/booking/migrations/0011_booking_aggregateid.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2 on 2023-07-17 15:25
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0010_auto_20230608_1913'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='booking',
+ name='aggregateId',
+ field=models.CharField(blank=True, max_length=36, validators=[django.core.validators.RegexValidator(code='nomatch', message='aggregate_id must be a valid UUID', regex='^[0-9a-fA-F]{8}\x08-[0-9a-fA-F]{4}\x08-[0-9a-fA-F]{4}\x08-[0-9a-fA-F]{4}\x08-[0-9a-fA-F]{12}$')]),
+ ),
+ ]
diff --git a/src/booking/models.py b/src/booking/models.py
index 966f1c2..09244d3 100644
--- a/src/booking/models.py
+++ b/src/booking/models.py
@@ -9,11 +9,10 @@
##############################################################################
-from resource_inventory.models import ResourceBundle, OPNFVConfig
from account.models import Lab
from django.contrib.auth.models import User
from django.db import models
-import resource_inventory.resource_manager
+from django.core.validators import RegexValidator
class Booking(models.Model):
@@ -26,47 +25,21 @@ class Booking(models.Model):
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)
# bookings can be extended a limited number of times
ext_count = models.IntegerField(default=2)
# the hardware that the user has booked
- resource = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True, blank=True)
- opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.SET_NULL, null=True, blank=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="")
+ # Associated LibLaaS aggregate
+ aggregateId = models.CharField(blank=True, max_length=36, validators=[RegexValidator(regex='^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$', message='aggregate_id must be a valid UUID', code='nomatch')])
complete = models.BooleanField(default=False)
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
deleted file mode 100644
index 4b85d76..0000000
--- a/src/booking/quick_deployer.py
+++ /dev/null
@@ -1,343 +0,0 @@
-##############################################################################
-# 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 yaml
-from django.db.models import Q
-from django.db import transaction
-from datetime import timedelta
-from django.utils import timezone
-from django.core.exceptions import ValidationError
-from account.models import Lab, UserProfile
-
-from resource_inventory.models import (
- ResourceTemplate,
- Image,
- OPNFVRole,
- OPNFVConfig,
- ResourceOPNFVConfig,
- ResourceConfiguration,
- NetworkConnection,
- InterfaceConfiguration,
- Network,
- CloudInitFile,
-)
-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 BookingLengthException
-from api.models import JobFactory
-
-
-def parse_resource_field(resource_json):
- """
- Parse the json from the frontend.
-
- returns a reference to the selected Lab and ResourceTemplate objects
- """
- lab, template = (None, None)
- lab_dict = resource_json['lab']
- for lab_info in lab_dict.values():
- if lab_info['selected']:
- lab = Lab.objects.get(lab_user__id=lab_info['id'])
-
- resource_dict = resource_json['resource']
- for resource_info in resource_dict.values():
- if resource_info['selected']:
- template = ResourceTemplate.objects.get(pk=resource_info['id'])
-
- if lab is None:
- raise ValidationError("No lab was selected")
- if template is None:
- raise ValidationError("No Host was selected")
-
- return lab, template
-
-
-def update_template(old_template, image, hostname, user, global_cloud_config=None):
- """
- Duplicate a template to the users account and update configured fields.
-
- The dashboard presents users with preconfigured resource templates,
- but the user might want to make small modifications, e.g hostname and
- linux distro. So we copy the public template and create a private version
- to the user's profile, and mark it temporary. When the booking ends the
- new template is deleted
- """
- name = user.username + "'s Copy of '" + old_template.name + "'"
- num_copies = ResourceTemplate.objects.filter(name__startswith=name).count()
- template = ResourceTemplate.objects.create(
- name=name if num_copies == 0 else name + " (" + str(num_copies) + ")",
- xml=old_template.xml,
- owner=user,
- lab=old_template.lab,
- description=old_template.description,
- public=False,
- temporary=True,
- private_vlan_pool=old_template.private_vlan_pool,
- public_vlan_pool=old_template.public_vlan_pool,
- copy_of=old_template
- )
-
- for old_network in old_template.networks.all():
- Network.objects.create(
- name=old_network.name,
- bundle=template,
- is_public=old_network.is_public
- )
- # We are assuming there is only one opnfv config per public resource template
- old_opnfv = template.opnfv_config.first()
- if old_opnfv:
- opnfv_config = OPNFVConfig.objects.create(
- installer=old_opnfv.installer,
- scenario=old_opnfv.installer,
- template=template,
- name=old_opnfv.installer,
- )
- # I am explicitly leaving opnfv_config.networks empty to avoid
- # problems with duplicated / shared networks. In the quick deploy,
- # there is never multiple networks anyway. This may have to change in the future
-
- for old_config in old_template.getConfigs():
- image_to_set = image
- if not image:
- image_to_set = old_config.image
-
- config = ResourceConfiguration.objects.create(
- profile=old_config.profile,
- image=image_to_set,
- template=template,
- is_head_node=old_config.is_head_node,
- name=hostname if len(old_template.getConfigs()) == 1 else old_config.name,
- # cloud_init_files=old_config.cloud_init_files.set()
- )
-
- for file in old_config.cloud_init_files.all():
- config.cloud_init_files.add(file)
-
- if global_cloud_config:
- config.cloud_init_files.add(global_cloud_config)
- config.save()
-
- for old_iface_config in old_config.interface_configs.all():
- iface_config = InterfaceConfiguration.objects.create(
- profile=old_iface_config.profile,
- resource_config=config
- )
-
- for old_connection in old_iface_config.connections.all():
- iface_config.connections.add(NetworkConnection.objects.create(
- network=template.networks.get(name=old_connection.network.name),
- vlan_is_tagged=old_connection.vlan_is_tagged
- ))
-
- for old_res_opnfv in old_config.resource_opnfv_config.all():
- if old_opnfv:
- ResourceOPNFVConfig.objects.create(
- role=old_opnfv.role,
- resource_config=config,
- opnfv_config=opnfv_config
- )
- return template
-
-
-def generate_opnfvconfig(scenario, installer, template):
- return OPNFVConfig.objects.create(
- scenario=scenario,
- installer=installer,
- template=template
- )
-
-
-def generate_hostopnfv(hostconfig, opnfvconfig):
- role = None
- try:
- role = OPNFVRole.objects.get(name="Jumphost")
- except Exception:
- role = OPNFVRole.objects.create(
- name="Jumphost",
- description="Single server jumphost role"
- )
- return ResourceOPNFVConfig.objects.create(
- role=role,
- host_config=hostconfig,
- opnfv_config=opnfvconfig
- )
-
-
-def generate_resource_bundle(template):
- resource_manager = ResourceManager.getInstance()
- resource_bundle = resource_manager.instantiateTemplate(template)
- return resource_bundle
-
-
-def check_invariants(**kwargs):
- # TODO: This should really happen in the BookingForm validation methods
- image = kwargs['image']
- lab = kwargs['lab']
- length = kwargs['length']
- # check that image os is compatible with installer
- if image:
- if image.from_lab != lab:
- raise ValidationError("The chosen image is not available at the chosen hosting lab")
- # TODO
- # if image.host_type != host_profile:
- # raise ValidationError("The chosen image is not available for the chosen host type")
- if not image.public and image.owner != kwargs['owner']:
- raise ValidationError("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 create_from_form(form, request):
- """
- Parse data from QuickBookingForm to create booking
- """
- resource_field = form.cleaned_data['filter_field']
- # users_field = form.cleaned_data['users']
- hostname = 'opnfv_host' if not form.cleaned_data['hostname'] else form.cleaned_data['hostname']
-
- global_cloud_config = None if not form.cleaned_data['global_cloud_config'] else form.cleaned_data['global_cloud_config']
-
- if global_cloud_config:
- form.cleaned_data['global_cloud_config'] = create_ci_file(global_cloud_config)
-
- # image = form.cleaned_data['image']
- # scenario = form.cleaned_data['scenario']
- # installer = form.cleaned_data['installer']
-
- lab, resource_template = parse_resource_field(resource_field)
- data = form.cleaned_data
- data['hostname'] = hostname
- data['lab'] = lab
- data['resource_template'] = resource_template
- data['owner'] = request.user
-
- return _create_booking(data)
-
-
-def create_from_API(body, user):
- """
- Parse data from Automation API to create booking
- """
- booking_info = json.loads(body.decode('utf-8'))
-
- data = {}
- data['purpose'] = booking_info['purpose']
- data['project'] = booking_info['project']
- data['users'] = [UserProfile.objects.get(user__username=username)
- for username in booking_info['collaborators']]
- data['hostname'] = booking_info['hostname']
- data['length'] = booking_info['length']
- data['installer'] = None
- data['scenario'] = None
-
- data['image'] = Image.objects.get(pk=booking_info['imageLabID'])
-
- data['resource_template'] = ResourceTemplate.objects.get(pk=booking_info['templateID'])
- data['lab'] = data['resource_template'].lab
- data['owner'] = user
-
- if 'global_cloud_config' in data.keys():
- data['global_cloud_config'] = CloudInitFile.objects.get(id=data['global_cloud_config'])
-
- return _create_booking(data)
-
-
-def create_ci_file(data: str) -> CloudInitFile:
- try:
- d = yaml.load(data)
- if not (type(d) is dict):
- raise Exception("CI file was valid yaml but was not a dict")
- except Exception:
- raise ValidationError("The provided Cloud Config is not valid yaml, please refer to the Cloud Init documentation for expected structure")
- print("about to create global cloud config")
- config = CloudInitFile.create(text=data, priority=CloudInitFile.objects.count())
- print("made global cloud config")
-
- return config
-
-
-@transaction.atomic
-def _create_booking(data):
- check_invariants(**data)
-
- # check booking privileges
- # TODO: use the canonical booking_allowed method because now template might have multiple
- # machines
- if Booking.objects.filter(owner=data['owner'], end__gt=timezone.now()).count() >= 3 and not data['owner'].userprofile.booking_privledge:
- raise PermissionError("You do not have permission to have more than 3 bookings at a time.")
-
- ResourceManager.getInstance().templateIsReservable(data['resource_template'])
-
- resource_template = update_template(data['resource_template'], data['image'], data['hostname'], data['owner'], global_cloud_config=data['global_cloud_config'])
-
- # generate resource bundle
- resource_bundle = generate_resource_bundle(resource_template)
-
- # generate booking
- booking = Booking.objects.create(
- purpose=data['purpose'],
- project=data['project'],
- lab=data['lab'],
- owner=data['owner'],
- start=timezone.now(),
- end=timezone.now() + timedelta(days=int(data['length'])),
- resource=resource_bundle,
- opnfv_config=None
- )
-
- booking.pdf = PDFTemplater.makePDF(booking)
-
- for collaborator in data['users']: # list of Users (not UserProfile)
- booking.collaborators.add(collaborator.user)
-
- booking.save()
-
- # generate job
- JobFactory.makeCompleteJob(booking)
- NotificationHandler.notify_new_booking(booking)
-
- return booking
-
-
-def drop_filter(user):
- """
- Return a dictionary that contains filters.
-
- Only certain installlers are supported on certain images, etc
- so the image filter indexed at [imageid][installerid] is truthy if
- that installer is supported on that image
- """
- installer_filter = {}
- scenario_filter = {}
-
- images = Image.objects.filter(Q(public=True) | Q(owner=user))
- image_filter = {}
- for image in images:
- image_filter[image.id] = {
- 'lab': 'lab_' + str(image.from_lab.lab_user.id),
- 'architecture': str(image.architecture),
- 'name': image.name
- }
-
- resource_filter = {}
- templates = ResourceTemplate.objects.filter(Q(public=True) | Q(owner=user))
- for rt in templates:
- profiles = [conf.profile for conf in rt.getConfigs()]
- resource_filter["resource_" + str(rt.id)] = [str(p.architecture) for p in profiles]
-
- return {
- 'installer_filter': json.dumps(installer_filter),
- 'scenario_filter': json.dumps(scenario_filter),
- 'image_filter': json.dumps(image_filter),
- 'resource_profile_map': json.dumps(resource_filter),
- }
diff --git a/src/booking/stats.py b/src/booking/stats.py
deleted file mode 100644
index 5a59d32..0000000
--- a/src/booking/stats.py
+++ /dev/null
@@ -1,109 +0,0 @@
-##############################################################################
-# Copyright (c) 2020 Parker Berberian, Sawyer Bergeron, Sean Smith 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 os
-from booking.models import Booking
-from resource_inventory.models import ResourceQuery, ResourceProfile
-from datetime import datetime, timedelta
-from collections import Counter
-import pytz
-
-
-class StatisticsManager(object):
-
- @staticmethod
- def getContinuousBookingTimeSeries(span=28):
- """
- Calculate Booking usage data points.
-
- Gathers all active bookings that fall in interval [(now - span), (now + 1 week)].
- x data points are every 12 hours
- y values are the integer number of bookings/users active at time
- """
-
- anuket_colors = [
- '#6BDAD5', # Turquoise
- '#E36386', # Pale Violet Red
- '#F5B335', # Sandy Brown
- '#007473', # Teal
- '#BCE194', # Gainsboro
- '#00CE7C', # Sea Green
- ]
-
- lfedge_colors = [
- '#0049B0',
- '#B481A5',
- '#6CAFE4',
- '#D33668',
- '#28245A'
- ]
-
- x = []
- y = []
- users = []
- projects = []
- profiles = {str(profile): [] for profile in ResourceProfile.objects.all()}
-
- now = datetime.now(pytz.utc)
- delta = timedelta(days=span)
- start = now - delta
- end = now + timedelta(weeks=1)
-
- bookings = Booking.objects.filter(
- start__lte=end,
- end__gte=start
- ).prefetch_related("collaborators")
-
- # get data
- while start <= end:
- active_users = 0
-
- books = bookings.filter(
- start__lte=start,
- end__gte=start
- ).prefetch_related("collaborators")
-
- for booking in books:
- active_users += booking.collaborators.all().count() + 1
-
- x.append(str(start.month) + '-' + str(start.day))
- y.append(books.count())
-
- step_profiles = Counter([
- str(config.profile)
- for book in books
- for config in book.resource.template.getConfigs()
- ])
-
- for profile in ResourceProfile.objects.all():
- profiles[str(profile)].append(step_profiles[str(profile)])
- users.append(active_users)
-
- start += timedelta(hours=12)
-
- in_use = len(ResourceQuery.filter(working=True, booked=True))
- not_in_use = len(ResourceQuery.filter(working=True, booked=False))
- maintenance = len(ResourceQuery.filter(working=False))
-
- projects = [x.project for x in bookings]
- proj_count = sorted(Counter(projects).items(), key=lambda x: x[1])
-
- project_keys = [proj[0] for proj in proj_count[-5:]]
- project_keys = ['None' if x is None else x for x in project_keys]
- project_counts = [proj[1] for proj in proj_count[-5:]]
-
- resources = {key: [x, value] for key, value in profiles.items()}
-
- return {
- "resources": resources,
- "booking": [x, y],
- "user": [x, users],
- "utils": [in_use, not_in_use, maintenance],
- "projects": [project_keys, project_counts],
- "colors": anuket_colors if os.environ.get('TEMPLATE_OVERRIDE_DIR') == 'laas' else lfedge_colors
- }
diff --git a/src/booking/tests/__init__.py b/src/booking/tests/__init__.py
deleted file mode 100644
index b6fef6c..0000000
--- a/src/booking/tests/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-##############################################################################
-# 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
deleted file mode 100644
index 37eb655..0000000
--- a/src/booking/tests/test_models.py
+++ /dev/null
@@ -1,210 +0,0 @@
-##############################################################################
-# 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 User
-from django.test import TestCase
-from django.utils import timezone
-
-from booking.models import Booking
-from dashboard.testing_utils import make_resource_template, make_user
-
-
-class BookingModelTestCase(TestCase):
- """
- Test the Booking model.
-
- Creates all the scafolding needed and tests the Booking model
- """
-
- def setUp(self):
- """
- Prepare for Booking model tests.
-
- Creates all the needed models, such as users, resources, and configurations
- """
- self.owner = User.objects.create(username='owner')
- self.res1 = make_resource_template(name="Test template 1")
- self.res2 = make_resource_template(name="Test template 2")
- self.user1 = make_user(username='user1')
-
- def test_start_end(self):
- """
- Verify the start and end fields.
-
- 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,
- )
- end = start
- self.assertRaises(
- ValueError,
- Booking.objects.create,
- start=start,
- end=end,
- resource=self.res1,
- owner=self.user1,
- )
-
- def test_conflicts(self):
- """
- Verify conflicting dates are dealt with.
-
- 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,
- )
- )
-
- self.assertRaises(
- ValueError,
- Booking.objects.create,
- start=start,
- end=end,
- resource=self.res1,
- owner=self.user1,
- )
-
- self.assertRaises(
- ValueError,
- Booking.objects.create,
- start=start + timedelta(days=1),
- end=end - timedelta(days=1),
- resource=self.res1,
- owner=self.user1,
- )
-
- self.assertRaises(
- ValueError,
- Booking.objects.create,
- start=start - timedelta(days=1),
- end=end,
- resource=self.res1,
- owner=self.user1,
- )
-
- self.assertRaises(
- ValueError,
- Booking.objects.create,
- start=start - timedelta(days=1),
- end=end - timedelta(days=1),
- resource=self.res1,
- owner=self.user1,
- )
-
- self.assertRaises(
- ValueError,
- Booking.objects.create,
- start=start,
- end=end + timedelta(days=1),
- resource=self.res1,
- owner=self.user1,
- )
-
- self.assertRaises(
- ValueError,
- Booking.objects.create,
- start=start + timedelta(days=1),
- end=end + timedelta(days=1),
- resource=self.res1,
- owner=self.user1,
- )
-
- self.assertTrue(
- Booking.objects.create(
- start=start - timedelta(days=1),
- end=start,
- owner=self.user1,
- resource=self.res1,
- )
- )
-
- self.assertTrue(
- Booking.objects.create(
- start=end,
- end=end + timedelta(days=1),
- owner=self.user1,
- resource=self.res1,
- )
- )
-
- self.assertTrue(
- Booking.objects.create(
- start=start - timedelta(days=2),
- end=start - timedelta(days=1),
- owner=self.user1,
- resource=self.res1,
- )
- )
-
- self.assertTrue(
- Booking.objects.create(
- start=end + timedelta(days=1),
- end=end + timedelta(days=2),
- owner=self.user1,
- resource=self.res1,
- )
- )
-
- self.assertTrue(
- Booking.objects.create(
- start=start,
- end=end,
- owner=self.user1,
- resource=self.res2,
- )
- )
-
- def test_extensions(self):
- """
- Test booking extensions.
-
- 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,
- )
- )
-
- 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
deleted file mode 100644
index f405047..0000000
--- a/src/booking/tests/test_quick_booking.py
+++ /dev/null
@@ -1,180 +0,0 @@
-##############################################################################
-# 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
-import json
-
-from django.test import TestCase, Client
-
-from booking.models import Booking
-from dashboard.testing_utils import (
- make_user,
- make_user_profile,
- make_lab,
- make_image,
- make_os,
- make_opnfv_role,
- make_public_net,
- make_resource_template,
- make_server
-)
-
-
-class QuickBookingValidFormTestCase(TestCase):
- @classmethod
- def setUpTestData(cls):
- cls.user = make_user(False, username="newtestuser")
- cls.user.set_password("testpassword")
- cls.user.save()
- make_user_profile(cls.user, True)
-
- cls.lab = make_lab()
-
- cls.res_template = make_resource_template(owner=cls.user, lab=cls.lab)
- cls.res_profile = cls.res_template.getConfigs()[0].profile
- os = make_os()
- cls.image = make_image(cls.res_profile, lab=cls.lab, owner=cls.user, os=os)
- cls.server = make_server(cls.res_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': json.dumps({
- "resource": {
- "resource_" + str(cls.res_profile.id): {
- "selected": True,
- "id": cls.res_template.id
- }
- },
- "lab": {
- "lab_" + str(cls.lab.lab_user.id): {
- "selected": True,
- "id": cls.lab.lab_user.id
- }
- }
- }),
- 'purpose': 'my_purpose',
- 'project': 'my_project',
- 'length': '3',
- 'ignore_this': 1,
- 'users': '',
- 'hostname': 'my_host',
- 'image': str(cls.image.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="newtestuser", 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
-
- host = resource_bundle.get_resources()[0]
- self.assertEqual(host.profile, self.res_profile)
- self.assertEqual(host.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': json.dumps({
- "resource": {
- "resource_" + str(self.res_profile.id + 100): {
- "selected": True,
- "id": self.res_profile.id + 100
- }
- },
- "lab": {
- "lab_" + str(self.lab.lab_user.id): {
- "selected": True,
- "id": self.lab.lab_user.id
- }
- }
- })})
-
- self.assertEqual(response.status_code, 200)
- self.assertIsNone(Booking.objects.first())
-
- def test_with_invalid_lab_id(self):
- response = self.post({'filter_field': json.dumps({
- "resource": {
- "resource_" + str(self.res_profile.id): {
- "selected": True,
- "id": self.res_profile.id
- }
- },
- "lab": {
- "lab_" + str(self.lab.lab_user.id + 100): {
- "selected": True,
- "id": self.lab.lab_user.id + 100
- }
- }
- })})
-
- 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.assertIsNone(booking)
-
- def test_with_valid_form(self):
- response = self.post()
-
- self.assertEqual(response.status_code, 302) # success should redirect
- booking = Booking.objects.first()
- self.assertIsNotNone(booking)
- self.assertValidBooking(booking)
diff --git a/src/booking/tests/test_stats.py b/src/booking/tests/test_stats.py
deleted file mode 100644
index 5501355..0000000
--- a/src/booking/tests/test_stats.py
+++ /dev/null
@@ -1,59 +0,0 @@
-#############################################################################
-# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, Sean Smith, 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 pytz
-from datetime import timedelta, datetime
-
-from django.test import TestCase
-
-from booking.models import Booking
-from booking.stats import StatisticsManager as sm
-from dashboard.testing_utils import make_user
-
-
-class StatsTestCases(TestCase):
-
- def test_no_booking_outside_span(self):
- now = datetime.now(pytz.utc)
-
- bad_date = now + timedelta(days=1200)
- Booking.objects.create(start=now, end=bad_date, owner=make_user(username='jj'))
-
- actual = sm.getContinuousBookingTimeSeries()
- dates = actual['booking'][0]
-
- for date in dates:
- self.assertNotEqual(date, bad_date)
-
- def check_booking_and_user_counts(self):
- now = datetime.now(pytz.utc)
-
- for i in range(20):
- Booking.objects.create(
- start=now,
- end=now + timedelta(weeks=3),
- owner=make_user(username='a'))
-
- for i in range(30):
- Booking.objects.create(
- start=now + timedelta(days=5),
- end=now + timedelta(weeks=3, days=5),
- owner=make_user(username='a'))
-
- for i in range(120):
- Booking.objects.create(
- start=now + timedelta(weeks=1),
- end=now + timedelta(weeks=4),
- owner=make_user(username='a'))
-
- dates = [[now, 20], [now + timedelta(days=5), 30], [now + timedelta(weeks=1), 120]]
- actual = sm.getContinuousBookingTimeSeries()
-
- for date in dates:
- self.assertEqual(date[1], actual['booking'][date[0]])
- self.assertEqual(date[1], actual['booking'][date[1]])
diff --git a/src/booking/urls.py b/src/booking/urls.py
index 0b60351..9784fc5 100644
--- a/src/booking/urls.py
+++ b/src/booking/urls.py
@@ -32,10 +32,6 @@ from booking.views import (
BookingDeleteView,
bookingDelete,
BookingListView,
- booking_stats_view,
- booking_stats_json,
- quick_create,
- booking_modify_image
)
app_name = 'booking'
@@ -45,9 +41,5 @@ urlpatterns = [
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
index 367a18d..25cac43 100644
--- a/src/booking/views.py
+++ b/src/booking/views.py
@@ -18,63 +18,9 @@ from django.shortcuts import redirect, render
from django.db.models import Q
from django.urls import reverse
-from resource_inventory.models import ResourceBundle, ResourceProfile, Image, ResourceQuery
from account.models import Downtime, Lab
+from api.views import get_booking_status
from booking.models import Booking
-from booking.stats import StatisticsManager
-from booking.forms import HostReImageForm
-from workflow.forms import FormUtils
-from api.models import JobFactory, GeneratedCloudConfig
-from workflow.views import login
-from booking.forms import QuickBookingForm
-from booking.quick_deployer import create_from_form, drop_filter
-import traceback
-
-
-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 = {}
- attrs = FormUtils.getLabData(user=request.user)
- context['form'] = QuickBookingForm(lab_data=attrs, default_user=request.user.username, user=request.user)
- context['lab_profile_map'] = {}
- context.update(drop_filter(request.user))
- context['contact_email'] = Lab.objects.filter(name="UNH_IOL").first().contact_email
- return render(request, 'booking/quick_deploy.html', context)
-
- if request.method == 'POST':
- attrs = FormUtils.getLabData(user=request.user)
- form = QuickBookingForm(request.POST, lab_data=attrs, 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:
- print("Error occurred while handling quick deployment:")
- traceback.print_exc()
- print(str(e))
- messages.error(request, "Whoops, an error occurred: " + str(e))
- context.update(drop_filter(request.user))
- 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")
- context['status'] = 'false'
- context.update(drop_filter(request.user))
- return render(request, 'booking/quick_deploy.html', context)
-
class BookingView(TemplateView):
template_name = "booking/booking_detail.html"
@@ -123,31 +69,6 @@ class BookingListView(TemplateView):
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',
- 'config_bundle__name'
- )
- return JsonResponse({'bookings': list(bookings)})
-
-
-def build_image_mapping(lab, user):
- mapping = {}
- for profile in ResourceProfile.objects.filter(labs=lab):
- images = Image.objects.filter(
- from_lab=lab,
- architecture=profile.architecture
- ).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
@@ -157,18 +78,17 @@ def booking_detail_view(request, booking_id):
return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
booking = get_object_or_404(Booking, id=booking_id)
+ statuses = get_booking_status(booking)
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),
- 'posix_username': GeneratedCloudConfig._normalize_username(None, user.username)
+ 'statuses': statuses,
+ 'collab_string': ', '.join(map(str, booking.collaborators.all()))
}
return render(
@@ -176,36 +96,3 @@ def booking_detail_view(request, booking_id):
"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 = ResourceQuery.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": ""}
- )
-
-
-def booking_stats_json(request):
- try:
- span = int(request.GET.get("days", 14))
- except Exception:
- span = 14
- return JsonResponse(StatisticsManager.getContinuousBookingTimeSeries(span), safe=False)