summaryrefslogtreecommitdiffstats
path: root/dashboard/src/booking
diff options
context:
space:
mode:
Diffstat (limited to 'dashboard/src/booking')
-rw-r--r--dashboard/src/booking/__init__.py2
-rw-r--r--dashboard/src/booking/admin.py6
-rw-r--r--dashboard/src/booking/forms.py135
-rw-r--r--dashboard/src/booking/lib.py36
-rw-r--r--dashboard/src/booking/migrations/0001_initial.py48
-rw-r--r--dashboard/src/booking/migrations/0002_booking_changeid.py38
-rw-r--r--dashboard/src/booking/migrations/0002_booking_pdf.py18
-rw-r--r--dashboard/src/booking/migrations/0003_auto_20180108_2024.py25
-rw-r--r--dashboard/src/booking/migrations/0003_auto_20190115_1733.py30
-rw-r--r--dashboard/src/booking/migrations/0004_auto_20190124_1700.py20
-rw-r--r--dashboard/src/booking/migrations/0005_booking_idf.py18
-rw-r--r--dashboard/src/booking/migrations/0006_booking_opnfv_config.py20
-rw-r--r--dashboard/src/booking/migrations/__init__.py10
-rw-r--r--dashboard/src/booking/models.py75
-rw-r--r--dashboard/src/booking/quick_deployer.py355
-rw-r--r--dashboard/src/booking/stats.py58
-rw-r--r--dashboard/src/booking/tests/__init__.py2
-rw-r--r--dashboard/src/booking/tests/test_models.py228
-rw-r--r--dashboard/src/booking/tests/test_quick_booking.py150
-rw-r--r--dashboard/src/booking/tests/test_views.py106
-rw-r--r--dashboard/src/booking/urls.py34
-rw-r--r--dashboard/src/booking/views.py283
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)