diff options
Diffstat (limited to 'dashboard/src/booking')
22 files changed, 1184 insertions, 513 deletions
diff --git a/dashboard/src/booking/__init__.py b/dashboard/src/booking/__init__.py index b5914ce..b6fef6c 100644 --- a/dashboard/src/booking/__init__.py +++ b/dashboard/src/booking/__init__.py @@ -6,5 +6,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/dashboard/src/booking/admin.py b/dashboard/src/booking/admin.py index 51e1031..162777e 100644 --- a/dashboard/src/booking/admin.py +++ b/dashboard/src/booking/admin.py @@ -1,5 +1,6 @@ ############################################################################## # 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 @@ -10,9 +11,6 @@ from django.contrib import admin -from booking.models import * +from booking.models import Booking admin.site.register(Booking) -admin.site.register(Opsys) -admin.site.register(Installer) -admin.site.register(Scenario) diff --git a/dashboard/src/booking/forms.py b/dashboard/src/booking/forms.py index 9d71b42..df88cc6 100644 --- a/dashboard/src/booking/forms.py +++ b/dashboard/src/booking/forms.py @@ -1,65 +1,104 @@ ############################################################################## -# 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 ############################################################################## - - 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 -from booking.models import Installer, Scenario, Opsys -from datetime import datetime -class BookingForm(forms.Form): - fields = ['start', 'end', 'purpose', 'opsys', 'reset', 'installer', 'scenario'] +class QuickBookingForm(forms.Form): + purpose = forms.CharField(max_length=1000) + project = forms.CharField(max_length=400) + hostname = forms.CharField(max_length=400) - start = forms.DateTimeField() - end = forms.DateTimeField() - reset = forms.ChoiceField(choices = ((True, 'Yes'),(False, 'No')), label="Reset System", initial='False', required=False) - purpose = forms.CharField(max_length=300) - opsys = forms.ModelChoiceField(queryset=Opsys.objects.all(), required=False) - opsys.label = "Operating System" installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) -class BookingEditForm(forms.Form): - fields = ['start', 'end', 'purpose', 'opsys', 'reset', 'installer', 'scenario'] + 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 - start = forms.DateTimeField() - end = forms.DateTimeField() - purpose = forms.CharField(max_length=300) - opsys = forms.ModelChoiceField(queryset=Opsys.objects.all(), required=False) - installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) - scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) - reset = forms.ChoiceField(choices = ((True, 'Yes'),(False, 'No')), label="Reset System", initial='False', required=True) - - - def __init__(self, *args, **kwargs ): - cloned_kwargs = {} - cloned_kwargs['purpose'] = kwargs.pop('purpose') - cloned_kwargs['start'] = kwargs.pop('start') - cloned_kwargs['end'] = kwargs.pop('end') - if 'installer' in kwargs: - cloned_kwargs['installer'] = kwargs.pop('installer') - if 'scenario' in kwargs: - cloned_kwargs['scenario'] = kwargs.pop('scenario') - super(BookingEditForm, self).__init__( *args, **kwargs) - - self.fields['purpose'].initial = cloned_kwargs['purpose'] - self.fields['start'].initial = cloned_kwargs['start'].strftime('%m/%d/%Y %H:%M') - self.fields['end'].initial = cloned_kwargs['end'].strftime('%m/%d/%Y %H:%M') - try: - self.fields['installer'].initial = cloned_kwargs['installer'].id - except KeyError: - pass - except AttributeError: - pass + 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: - self.fields['scenario'].initial = cloned_kwargs['scenario'].id - except KeyError: - pass - except AttributeError: + 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/dashboard/src/booking/lib.py b/dashboard/src/booking/lib.py new file mode 100644 index 0000000..8132c75 --- /dev/null +++ b/dashboard/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/dashboard/src/booking/migrations/0001_initial.py b/dashboard/src/booking/migrations/0001_initial.py index 6932dae..20415fe 100644 --- a/dashboard/src/booking/migrations/0001_initial.py +++ b/dashboard/src/booking/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2016-11-03 13:33 -from __future__ import unicode_literals +# Generated by Django 2.1 on 2018-09-14 14:48 from django.conf import settings from django.db import migrations, models @@ -12,8 +10,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('dashboard', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('account', '0001_initial'), + ('resource_inventory', '__first__'), ] operations = [ @@ -23,9 +22,17 @@ class Migration(migrations.Migration): ('id', models.AutoField(primary_key=True, serialize=False)), ('start', models.DateTimeField()), ('end', models.DateTimeField()), - ('jira_issue_id', models.IntegerField(null=True)), - ('jira_issue_status', models.CharField(max_length=50)), + ('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', @@ -39,6 +46,14 @@ class Migration(migrations.Migration): ], ), 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)), @@ -46,23 +61,8 @@ class Migration(migrations.Migration): ], ), migrations.AddField( - model_name='booking', - name='installer', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Installer'), - ), - migrations.AddField( - model_name='booking', - name='resource', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dashboard.Resource'), - ), - migrations.AddField( - model_name='booking', - name='scenario', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Scenario'), - ), - migrations.AddField( - model_name='booking', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name='installer', + name='sup_scenarios', + field=models.ManyToManyField(blank=True, to='booking.Scenario'), ), ] diff --git a/dashboard/src/booking/migrations/0002_booking_changeid.py b/dashboard/src/booking/migrations/0002_booking_changeid.py deleted file mode 100644 index 33af8fd..0000000 --- a/dashboard/src/booking/migrations/0002_booking_changeid.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-12-13 15:06 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Opsys', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100)), - ], - ), - migrations.AddField( - model_name='booking', - name='opsys', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Opsys'), - ), - migrations.AddField( - model_name='booking', - name='changeid', - field=models.TextField(default='no change ID'), - ), - migrations.AlterField( - model_name='booking', - name='changeid', - field=models.TextField(blank=True, default='no change ID', null=True), - ), - ] diff --git a/dashboard/src/booking/migrations/0002_booking_pdf.py b/dashboard/src/booking/migrations/0002_booking_pdf.py new file mode 100644 index 0000000..53232c9 --- /dev/null +++ b/dashboard/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/dashboard/src/booking/migrations/0003_auto_20180108_2024.py b/dashboard/src/booking/migrations/0003_auto_20180108_2024.py deleted file mode 100644 index 93cecc2..0000000 --- a/dashboard/src/booking/migrations/0003_auto_20180108_2024.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-01-08 20:24 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0002_booking_changeid'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='reset', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='booking', - name='changeid', - field=models.TextField(blank=True, default='initial', null=True), - ), - ]
\ No newline at end of file diff --git a/dashboard/src/booking/migrations/0003_auto_20190115_1733.py b/dashboard/src/booking/migrations/0003_auto_20190115_1733.py new file mode 100644 index 0000000..70eecfe --- /dev/null +++ b/dashboard/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/dashboard/src/booking/migrations/0004_auto_20190124_1700.py b/dashboard/src/booking/migrations/0004_auto_20190124_1700.py new file mode 100644 index 0000000..baa32d2 --- /dev/null +++ b/dashboard/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/dashboard/src/booking/migrations/0005_booking_idf.py b/dashboard/src/booking/migrations/0005_booking_idf.py new file mode 100644 index 0000000..31e9170 --- /dev/null +++ b/dashboard/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/dashboard/src/booking/migrations/0006_booking_opnfv_config.py b/dashboard/src/booking/migrations/0006_booking_opnfv_config.py new file mode 100644 index 0000000..e5ffc71 --- /dev/null +++ b/dashboard/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/dashboard/src/booking/migrations/__init__.py b/dashboard/src/booking/migrations/__init__.py index b5914ce..e69de29 100644 --- a/dashboard/src/booking/migrations/__init__.py +++ b/dashboard/src/booking/migrations/__init__.py @@ -1,10 +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/dashboard/src/booking/models.py b/dashboard/src/booking/models.py index 9156484..9836730 100644 --- a/dashboard/src/booking/models.py +++ b/dashboard/src/booking/models.py @@ -1,5 +1,6 @@ ############################################################################## # 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 @@ -8,66 +9,35 @@ ############################################################################## -from django.conf import settings +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 -from jira import JIRA -from jira import JIRAError -from django.utils.crypto import get_random_string -import hashlib +import resource_inventory.resource_manager -from dashboard.models import Resource - - -class Installer(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=30) - - def __str__(self): - return self.name - -class Scenario(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=300) - - def __str__(self): - return self.name - -class Opsys(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100) - - def __str__(self): - return self.name class Booking(models.Model): id = models.AutoField(primary_key=True) - changeid = models.TextField(default='initial', blank=True, null=True) - user = models.ForeignKey(User, models.CASCADE) # delete if user is deleted - resource = models.ForeignKey(Resource, models.PROTECT) + 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) - jira_issue_status = models.CharField(max_length=50) - - opsys = models.ForeignKey(Opsys, models.DO_NOTHING, null=True) - installer = models.ForeignKey(Installer, models.DO_NOTHING, null=True) - scenario = models.ForeignKey(Scenario, models.DO_NOTHING, null=True) + 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 get_jira_issue(self): - try: - jira = JIRA(server=settings.JIRA_URL, - basic_auth=(settings.JIRA_USER_NAME, settings.JIRA_USER_PASSWORD)) - issue = jira.issue(self.jira_issue_id) - return issue - except JIRAError: - return None - def save(self, *args, **kwargs): """ Save the booking if self.user is authorized and there is no overlapping booking. @@ -82,11 +52,14 @@ class Booking(models.Model): conflicting_dates = conflicting_dates.filter(start__lt=self.end) if conflicting_dates.count() > 0: raise ValueError('This booking overlaps with another booking') - if not self.changeid: - self.changeid = self.id - else: - self.changeid = hashlib.md5(self.changeid.encode() + get_random_string(length=32).encode()).hexdigest() 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.resource) + ' from ' + str(self.start) + ' until ' + str(self.end) + return str(self.purpose) + ' from ' + str(self.start) + ' until ' + str(self.end) diff --git a/dashboard/src/booking/quick_deployer.py b/dashboard/src/booking/quick_deployer.py new file mode 100644 index 0000000..0e0cc5a --- /dev/null +++ b/dashboard/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/dashboard/src/booking/stats.py b/dashboard/src/booking/stats.py new file mode 100644 index 0000000..383723a --- /dev/null +++ b/dashboard/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/dashboard/src/booking/tests/__init__.py b/dashboard/src/booking/tests/__init__.py index b5914ce..b6fef6c 100644 --- a/dashboard/src/booking/tests/__init__.py +++ b/dashboard/src/booking/tests/__init__.py @@ -6,5 +6,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/dashboard/src/booking/tests/test_models.py b/dashboard/src/booking/tests/test_models.py index b4cd113..6170295 100644 --- a/dashboard/src/booking/tests/test_models.py +++ b/dashboard/src/booking/tests/test_models.py @@ -1,5 +1,6 @@ ############################################################################## # 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 @@ -10,85 +11,222 @@ from datetime import timedelta -from django.contrib.auth.models import Permission +from django.contrib.auth.models import Permission, User from django.test import TestCase from django.utils import timezone -from booking.models import * -from dashboard.models import Resource -from jenkins.models import JenkinsSlave +# 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.slave = JenkinsSlave.objects.create(name='test', url='test') self.owner = User.objects.create(username='owner') - self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x', - url='x',owner=self.owner) - self.res2 = Resource.objects.create(name='res2', slave=self.slave, description='x', - url='x',owner=self.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.installer = Installer.objects.create(name='TestInstaller') - self.scenario = Scenario.objects.create(name='TestScenario') + 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 + 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, user=self.user1) + 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, user=self.user1) + 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 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, user=self.user1, resource=self.res1)) + 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.assertRaises(ValueError, Booking.objects.create, start=start, - end=end, resource=self.res1, user=self.user1) - self.assertRaises(ValueError, Booking.objects.create, start=start + timedelta(days=1), - end=end - timedelta(days=1), resource=self.res1, user=self.user1) - - self.assertRaises(ValueError, Booking.objects.create, start=start - timedelta(days=1), - end=end, resource=self.res1, user=self.user1) - self.assertRaises(ValueError, Booking.objects.create, start=start - timedelta(days=1), - end=end - timedelta(days=1), resource=self.res1, user=self.user1) + self.assertTrue( + Booking.objects.create( + start=start - timedelta(days=1), + end=start, + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) - self.assertRaises(ValueError, Booking.objects.create, start=start, - end=end + timedelta(days=1), resource=self.res1, user=self.user1) - self.assertRaises(ValueError, Booking.objects.create, start=start + timedelta(days=1), - end=end + timedelta(days=1), resource=self.res1, user=self.user1) + 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=1), end=start, - user=self.user1, resource=self.res1)) - self.assertTrue(Booking.objects.create(start=end, end=end + timedelta(days=1), - user=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, + config_bundle=self.config_bundle + ) + ) self.assertTrue( - Booking.objects.create(start=start - timedelta(days=2), end=start - timedelta(days=1), - user=self.user1, resource=self.res1)) + 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=end + timedelta(days=1), end=end + timedelta(days=2), - user=self.user1, resource=self.res1)) + 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, - user=self.user1, resource=self.res2, scenario=self.scenario, - installer=self.installer))
\ No newline at end of file + 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/dashboard/src/booking/tests/test_quick_booking.py b/dashboard/src/booking/tests/test_quick_booking.py new file mode 100644 index 0000000..e445860 --- /dev/null +++ b/dashboard/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/dashboard/src/booking/tests/test_views.py b/dashboard/src/booking/tests/test_views.py deleted file mode 100644 index c1da013..0000000 --- a/dashboard/src/booking/tests/test_views.py +++ /dev/null @@ -1,106 +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 -############################################################################## - - -from datetime import timedelta - -from django.test import Client -from django.test import TestCase -from django.urls import reverse -from django.utils import timezone -from django.utils.encoding import force_text -from registration.forms import User - -from account.models import UserProfile -from booking.models import Booking -from dashboard.models import Resource -from jenkins.models import JenkinsSlave - - -class BookingViewTestCase(TestCase): - def setUp(self): - self.client = Client() - self.slave = JenkinsSlave.objects.create(name='test', url='test') - self.owner = User.objects.create(username='owner') - self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x', - url='x',owner=self.owner) - self.user1 = User.objects.create(username='user1') - self.user1.set_password('user1') - self.user1profile = UserProfile.objects.create(user=self.user1) - self.user1.save() - - self.user1 = User.objects.get(pk=self.user1.id) - - - def test_resource_bookings_json(self): - url = reverse('booking:bookings_json', kwargs={'resource_id': 0}) - self.assertEqual(self.client.get(url).status_code, 404) - - url = reverse('booking:bookings_json', kwargs={'resource_id': self.res1.id}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertJSONEqual(force_text(response.content), {"bookings": []}) - booking1 = Booking.objects.create(start=timezone.now(), - end=timezone.now() + timedelta(weeks=1), user=self.user1, - resource=self.res1) - response = self.client.get(url) - json = response.json() - self.assertEqual(response.status_code, 200) - self.assertIn('bookings', json) - self.assertEqual(len(json['bookings']), 1) - self.assertIn('start', json['bookings'][0]) - self.assertIn('end', json['bookings'][0]) - self.assertIn('id', json['bookings'][0]) - self.assertIn('purpose', json['bookings'][0]) - - def test_booking_form_view(self): - url = reverse('booking:create', kwargs={'resource_id': 0}) - self.assertEqual(self.client.get(url).status_code, 404) - - # authenticated user - url = reverse('booking:create', kwargs={'resource_id': self.res1.id}) - self.client.login(username='user1',password='user1') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('booking/booking_calendar.html') - self.assertTemplateUsed('booking/booking_form.html') - self.assertIn('resource', response.context) - - - def test_booking_view(self): - start = timezone.now() - end = start + timedelta(weeks=1) - booking = Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) - - url = reverse('booking:detail', kwargs={'booking_id':0}) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - - url = reverse('booking:detail', kwargs={'booking_id':booking.id}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('booking/booking_detail.html') - self.assertIn('booking', response.context) - - def test_booking_list_view(self): - start = timezone.now() - timedelta(weeks=2) - end = start + timedelta(weeks=1) - Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) - - url = reverse('booking:list') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('booking/booking_list.html') - self.assertTrue(len(response.context['bookings']) == 0) - - start = timezone.now() - end = start + timedelta(weeks=1) - Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) - response = self.client.get(url) - self.assertTrue(len(response.context['bookings']) == 1)
\ No newline at end of file diff --git a/dashboard/src/booking/urls.py b/dashboard/src/booking/urls.py index ed3b1d4..310aaa7 100644 --- a/dashboard/src/booking/urls.py +++ b/dashboard/src/booking/urls.py @@ -1,5 +1,6 @@ ############################################################################## # 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 @@ -25,22 +26,29 @@ Including another URLconf """ from django.conf.urls import url -from booking.views import * - +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'^(?P<resource_id>[0-9]+)/$', BookingFormView.as_view(), name='create'), - url(r'^(?P<resource_id>[0-9]+)/edit/(?P<booking_id>[0-9]+)/$', BookingEditFormView.as_view(), name='edit'), - - url(r'^(?P<resource_id>[0-9]+)/bookings_json/$', ResourceBookingsJSON.as_view(), - name='bookings_json'), - url(r'^detail/$', BookingView.as_view(), name='detail_prefix'), - url(r'^detail/(?P<booking_id>[0-9]+)/$', BookingView.as_view(), name='detail'), + 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'^list/$', BookingListView.as_view(), name='list') -]
\ No newline at end of file + 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/dashboard/src/booking/views.py b/dashboard/src/booking/views.py index 7e35af2..bad7dc9 100644 --- a/dashboard/src/booking/views.py +++ b/dashboard/src/booking/views.py @@ -1,5 +1,6 @@ ############################################################################## # 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 @@ -7,164 +8,74 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## -from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.http import JsonResponse, HttpResponse from django.utils import timezone from django.views import View -from django.views.generic import FormView from django.views.generic import TemplateView -from jira import JIRAError -from django.shortcuts import redirect +from django.shortcuts import redirect, render +from django.db.models import Q +from django.urls import reverse -from account.jira_util import get_jira -from booking.forms import BookingForm, BookingEditForm +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 dashboard.models import Resource - -def create_jira_ticket(user, booking): - jira = get_jira(user) - issue_dict = { - 'project': 'PHAROS', - 'summary': str(booking.resource) + ': Access Request', - 'description': booking.purpose, - 'issuetype': {'name': 'Task'}, - 'components': [{'name': 'POD Access Request'}], - 'assignee': {'name': booking.resource.owner.username} - } - issue = jira.create_issue(fields=issue_dict) - jira.add_attachment(issue, user.userprofile.pgp_public_key) - jira.add_attachment(issue, user.userprofile.ssh_public_key) - booking.jira_issue_id = issue.id - booking.save() +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 -class BookingFormView(FormView): - template_name = "booking/booking_calendar.html" - form_class = BookingForm +def quick_create_clear_fields(request): + request.session['quick_create_forminfo'] = None - def dispatch(self, request, *args, **kwargs): - self.resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) - return super(BookingFormView, self).dispatch(request, *args, **kwargs) - def get_context_data(self, **kwargs): - title = 'Booking: ' + self.resource.name - context = super(BookingFormView, self).get_context_data(**kwargs) - context.update({'title': title, 'resource': self.resource}) - #raise PermissionDenied('check') - return context +def quick_create(request): + if not request.user.is_authenticated: + return login(request) - def get_success_url(self): - return reverse('booking:create', kwargs=self.kwargs) - - def form_valid(self, form): - if not self.request.user.is_authenticated: - messages.add_message(self.request, messages.ERROR, - 'You need to be logged in to book a Pod.') - return super(BookingFormView, self).form_invalid(form) - - user = self.request.user - booking = Booking(start=form.cleaned_data['start'], - end=form.cleaned_data['end'], - purpose=form.cleaned_data['purpose'], - opsys=form.cleaned_data['opsys'], - installer=form.cleaned_data['installer'], - scenario=form.cleaned_data['scenario'], - resource=self.resource, user=user) - try: - booking.save() - except ValueError as err: - messages.add_message(self.request, messages.ERROR, err) - return super(BookingFormView, self).form_invalid(form) - try: - if settings.CREATE_JIRA_TICKET: - create_jira_ticket(user, booking) - except JIRAError: - messages.add_message(self.request, messages.ERROR, 'Failed to create Jira Ticket. ' - 'Please check your Jira ' - 'permissions.') - booking.delete() - return super(BookingFormView, self).form_invalid(form) - messages.add_message(self.request, messages.SUCCESS, 'Booking saved') - return super(BookingFormView, self).form_valid(form) - - -class BookingEditFormView(FormView): - template_name = "booking/booking_calendar.html" - form_class = BookingEditForm - - def is_valid(self): - return True - - def dispatch(self, request, *args, **kwargs): - self.resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) - self.original_booking = get_object_or_404(Booking, id=self.kwargs['booking_id']) - return super(BookingEditFormView, self).dispatch(request, *args, **kwargs) + if request.method == 'GET': + context = {} - def get_context_data(self, **kwargs): - title = 'Editing Booking on: ' + self.resource.name - context = super(BookingEditFormView, self).get_context_data(**kwargs) - context.update({'title': title, 'resource': self.resource}) - return 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) - def get_form_kwargs(self): - kwargs = super(BookingEditFormView, self).get_form_kwargs() - kwargs['purpose'] = self.original_booking.purpose - kwargs['start'] = self.original_booking.start - kwargs['end'] = self.original_booking.end - try: - kwargs['installer'] = self.original_booking.installer - except AttributeError: - pass - try: - kwargs['scenario'] = self.original_booking.scenario - except AttributeError: - pass - return kwargs - - def get_success_url(self): - return reverse('booking:create', args=(self.resource.id,)) - - def form_valid(self, form): - - if not self.request.user.is_authenticated: - messages.add_message(self.request, messages.ERROR, - 'You need to be logged in to book a Pod.') - return super(BookingEditFormView, self).form_invalid(form) - - if not self.request.user == self.original_booking.user: - messages.add_message(self.request, messages.ERROR, - 'You are not the owner of this booking.') - return super(BookingEditFormView, self).form_invalid(form) - - #Do Conflict Checks - if self.original_booking.start != form.cleaned_data['start']: - if timezone.now() > form.cleaned_data['start']: - messages.add_message(self.request, messages.ERROR, - 'Cannot change start date after it has occurred.') - return super(BookingEditFormView, self).form_invalid(form) - self.original_booking.start = form.cleaned_data['start'] - self.original_booking.end = form.cleaned_data['end'] - self.original_booking.purpose = form.cleaned_data['purpose'] - self.original_booking.installer = form.cleaned_data['installer'] - self.original_booking.scenario = form.cleaned_data['scenario'] - self.original_booking.reset = form.cleaned_data['reset'] - try: - self.original_booking.save() - except ValueError as err: - messages.add_message(self.request, messages.ERROR, err) - return super(BookingEditFormView, self).form_invalid(form) - - user = self.request.user - return super(BookingEditFormView, self).form_valid(form) 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' @@ -172,6 +83,7 @@ class BookingView(TemplateView): context.update({'title': title, 'booking': booking}) return context + class BookingDeleteView(TemplateView): template_name = "booking/booking_delete.html" @@ -182,12 +94,14 @@ class BookingDeleteView(TemplateView): 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" @@ -201,8 +115,87 @@ class BookingListView(TemplateView): class ResourceBookingsJSON(View): def get(self, request, *args, **kwargs): - resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) - bookings = resource.booking_set.get_queryset().values('id', 'start', 'end', 'purpose', - 'jira_issue_status', 'opsys__name', - 'installer__name', 'scenario__name') - return JsonResponse({'bookings': list(bookings)})
\ No newline at end of file + 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) |