diff options
Diffstat (limited to 'src/booking')
-rw-r--r-- | src/booking/forms.py | 114 | ||||
-rw-r--r-- | src/booking/migrations/0010_auto_20230608_1913.py | 29 | ||||
-rw-r--r-- | src/booking/migrations/0011_booking_aggregateid.py | 19 | ||||
-rw-r--r-- | src/booking/models.py | 33 | ||||
-rw-r--r-- | src/booking/quick_deployer.py | 343 | ||||
-rw-r--r-- | src/booking/stats.py | 109 | ||||
-rw-r--r-- | src/booking/tests/__init__.py | 8 | ||||
-rw-r--r-- | src/booking/tests/test_models.py | 210 | ||||
-rw-r--r-- | src/booking/tests/test_quick_booking.py | 180 | ||||
-rw-r--r-- | src/booking/tests/test_stats.py | 59 | ||||
-rw-r--r-- | src/booking/urls.py | 8 | ||||
-rw-r--r-- | src/booking/views.py | 123 |
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) |