aboutsummaryrefslogtreecommitdiffstats
path: root/src/booking
diff options
context:
space:
mode:
authorParker Berberian <pberberian@iol.unh.edu>2019-03-01 18:18:43 +0000
committerGerrit Code Review <gerrit@opnfv.org>2019-03-01 18:18:43 +0000
commit382853e398c144235beee96fe33bf7404a524f69 (patch)
tree2a3d951aba041d85940046553e695a46ecf24c83 /src/booking
parentc04dcca919cc89d04daa73d570a42134829a91d8 (diff)
parentc23bdfcb145c4934d55c8ab0d1d32ccfe2ac62fa (diff)
Merge "Add test utils and tests for quick booking"
Diffstat (limited to 'src/booking')
-rw-r--r--src/booking/quick_deployer.py208
-rw-r--r--src/booking/tests/test_models.py7
-rw-r--r--src/booking/tests/test_quick_booking.py155
3 files changed, 290 insertions, 80 deletions
diff --git a/src/booking/quick_deployer.py b/src/booking/quick_deployer.py
index 7946ebf..dd78f15 100644
--- a/src/booking/quick_deployer.py
+++ b/src/booking/quick_deployer.py
@@ -38,7 +38,8 @@ from booking.models import Booking
from dashboard.exceptions import (
InvalidHostnameException,
ResourceAvailabilityException,
- ModelValidationException
+ ModelValidationException,
+ BookingLengthException
)
from api.models import JobFactory
@@ -88,22 +89,8 @@ class NoRemainingPublicNetwork(Exception):
pass
-def create_from_form(form, request):
- quick_booking_id = str(uuid.uuid4())
-
- host_field = form.cleaned_data['filter_field']
- host_json = json.loads(host_field)
- purpose_field = form.cleaned_data['purpose']
- project_field = form.cleaned_data['project']
- users_field = form.cleaned_data['users']
- host_name = form.cleaned_data['hostname']
- length = form.cleaned_data['length']
-
- image = form.cleaned_data['image']
- scenario = form.cleaned_data['scenario']
- installer = form.cleaned_data['installer']
-
- # get all initial info we need to validate
+def parse_host_field(host_field_contents):
+ host_json = json.loads(host_field_contents)
lab_dict = host_json['labs'][0]
lab_id = list(lab_dict.keys())[0]
lab_user_id = int(lab_id.split("_")[-1])
@@ -115,110 +102,185 @@ def create_from_form(form, request):
profile = HostProfile.objects.get(id=profile_id)
# check validity of field data before trying to apply to models
+ if len(host_json['labs']) != 1:
+ raise NoLabSelectedError("No lab was selected")
if not lab:
raise LabDNE("Lab with provided ID does not exist")
if not profile:
raise HostProfileDNE("Host type with provided ID does not exist")
- # check that hostname is valid
- if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", host_name):
- raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
- # 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 != 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")
+ return lab, profile
+
- # check if host type is available
- # ResourceManager.getInstance().acquireHost(ghost, lab.name)
+def check_available_matching_host(lab, hostprofile):
available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
- if profile not in available_host_types:
+ if hostprofile not in available_host_types:
# TODO: handle deleting generic resource in this instance along with grb
raise HostNotAvailable("Could not book selected host due to changed availability. Try again later")
- # check if any hosts with profile at lab are still available
- hostset = Host.objects.filter(lab=lab, profile=profile).filter(booked=False).filter(working=True)
- if not hostset.first():
+ 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")
- # generate GenericResourceBundle
- if len(host_json['labs']) != 1:
- raise NoLabSelectedError("No lab was selected")
+ return True
+
- grbundle = GenericResourceBundle(owner=request.user)
+def generate_grb(owner, lab, common_id):
+ grbundle = GenericResourceBundle(owner=owner)
grbundle.lab = lab
- grbundle.name = "grbundle for quick booking with uid " + quick_booking_id
+ grbundle.name = "grbundle for quick booking with uid " + common_id
grbundle.description = "grbundle created for quick-deploy booking"
grbundle.save()
- # generate GenericResource, GenericHost
- gresource = GenericResource(bundle=grbundle, name=host_name)
+ 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 = gresource
- ghost.profile = profile
+ ghost.resource = generic_resource
+ ghost.profile = host_profile
ghost.save()
- # generate config bundle
+ return ghost
+
+
+def generate_config_bundle(owner, common_id, grbundle):
cbundle = ConfigBundle()
- cbundle.owner = request.user
- cbundle.name = "configbundle for quick booking with uid " + quick_booking_id
+ 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()
- # generate OPNFVConfig pointing to cbundle
- if installer:
- opnfvconfig = OPNFVConfig()
- opnfvconfig.scenario = scenario
- opnfvconfig.installer = installer
- opnfvconfig.bundle = cbundle
- opnfvconfig.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
+
- # generate HostConfiguration pointing to cbundle
+def generate_hostconfig(generic_host, image, config_bundle):
hconf = HostConfiguration()
- hconf.host = ghost
+ hconf.host = generic_host
hconf.image = image
- hconf.opnfvRole = OPNFVRole.objects.get(name="Jumphost")
- if not hconf.opnfvRole:
- raise OPNFVRoleDNE("No jumphost role was found")
- hconf.bundle = cbundle
+
+ opnfvrole = OPNFVRole.objects.get(name="Jumphost")
+ if not opnfvrole:
+ raise OPNFVRoleDNE("No jumphost role was found.")
+
+ hconf.opnfvRole = opnfvrole
+ hconf.bundle = config_bundle
hconf.save()
+ return hconf
+
+
+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 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_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)
+
+ # if no installer provided, just create blank host
+ if installer:
+ generate_opnfvconfig(scenario, installer, cbundle)
+
+ generate_hostconfig(ghost, image, cbundle)
+
# construct generic interfaces
- for interface_profile in profile.interfaceprofile.all():
+ for interface_profile in host_profile.interfaceprofile.all():
generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
generic_interface.save()
- ghost.save()
# get vlan, assign to first interface
publicnetwork = lab.vlan_manager.get_public_vlan()
- publicvlan = publicnetwork.vlan
if not publicnetwork:
raise NoRemainingPublicNetwork("No public networks were available for your pod")
+ publicvlan = publicnetwork.vlan
lab.vlan_manager.reserve_public_vlan(publicvlan)
vlan = Vlan.objects.create(vlan_id=publicvlan, tagged=False, public=True)
vlan.save()
+
ghost.generic_interfaces.first().vlans.add(vlan)
ghost.generic_interfaces.first().save()
# generate resource bundle
- try:
- resource_bundle = ResourceManager.getInstance().convertResourceBundle(grbundle, config=cbundle)
- except ResourceAvailabilityException:
- raise ResourceAvailabilityException("Requested resources not available")
- except ModelValidationException:
- raise ModelValidationException("Encountered error while saving grbundle")
+ resource_bundle = generate_resource_bundle(grbundle, cbundle)
# generate booking
booking = Booking()
diff --git a/src/booking/tests/test_models.py b/src/booking/tests/test_models.py
index c7fb25d..6170295 100644
--- a/src/booking/tests/test_models.py
+++ b/src/booking/tests/test_models.py
@@ -230,10 +230,3 @@ class BookingModelTestCase(TestCase):
booking.save()
except Exception:
self.fail("save() threw an exception")
- booking.end = booking.end + timedelta(weeks=2)
- self.assertRaises(ValueError, booking.save)
- booking.end = booking.end - timedelta(days=8)
- try:
- self.assertTrue(booking.save())
- except Exception:
- self.fail("save() threw an exception")
diff --git a/src/booking/tests/test_quick_booking.py b/src/booking/tests/test_quick_booking.py
new file mode 100644
index 0000000..936a9a5
--- /dev/null
+++ b/src/booking/tests/test_quick_booking.py
@@ -0,0 +1,155 @@
+##############################################################################
+# 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 (
+ instantiate_host,
+ instantiate_user,
+ instantiate_userprofile,
+ instantiate_lab,
+ instantiate_installer,
+ instantiate_image,
+ instantiate_scenario,
+ instantiate_os,
+ make_hostprofile_set,
+ instantiate_opnfvrole,
+ instantiate_publicnet,
+)
+# from dashboard import test_utils
+
+
+class QuickBookingValidFormTestCase(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.loginuser = instantiate_user(False, username="newtestuser", password="testpassword")
+ instantiate_userprofile(cls.loginuser, True)
+
+ lab_user = instantiate_user(True)
+ cls.lab = instantiate_lab(lab_user)
+
+ cls.host_profile = make_hostprofile_set(cls.lab)
+ cls.scenario = instantiate_scenario()
+ cls.installer = instantiate_installer([cls.scenario])
+ os = instantiate_os([cls.installer])
+ cls.image = instantiate_image(cls.lab, 1, cls.loginuser, os, cls.host_profile)
+ cls.host = instantiate_host(cls.host_profile, cls.lab)
+ cls.role = instantiate_opnfvrole()
+ cls.pubnet = instantiate_publicnet(10, cls.lab)
+
+ cls.lab_selected = 'lab_' + str(cls.lab.lab_user.id) + '_selected'
+ cls.host_selected = 'host_' + str(cls.host_profile.id) + '_selected'
+
+ cls.post_data = cls.build_post_data()
+
+ cls.client = Client()
+
+ @classmethod
+ def build_post_data(cls):
+ post_data = {}
+ post_data['filter_field'] = '{"hosts":[{"host_' + str(cls.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(cls.lab.lab_user.id) + '":"true"}]}'
+ post_data['purpose'] = 'purposefieldcontentstring'
+ post_data['project'] = 'projectfieldcontentstring'
+ post_data['length'] = '3'
+ post_data['ignore_this'] = 1
+ post_data['users'] = ''
+ post_data['hostname'] = 'hostnamefieldcontentstring'
+ post_data['image'] = str(cls.image.id)
+ post_data['installer'] = str(cls.installer.id)
+ post_data['scenario'] = str(cls.scenario.id)
+ return post_data
+
+ 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.loginuser.username, password="testpassword")
+
+ def is_valid_booking(self, booking):
+ self.assertEqual(booking.owner, self.loginuser)
+ self.assertEqual(booking.purpose, 'purposefieldcontentstring')
+ self.assertEqual(booking.project, 'projectfieldcontentstring')
+ delta = booking.end - booking.start
+ delta -= datetime.timedelta(days=3)
+ self.assertLess(delta, datetime.timedelta(minutes=1))
+
+ resourcebundle = booking.resource
+ configbundle = booking.config_bundle
+
+ self.assertEqual(self.installer, configbundle.opnfv_config.first().installer)
+ self.assertEqual(self.scenario, configbundle.opnfv_config.first().scenario)
+ self.assertEqual(resourcebundle.template.getHosts()[0].profile, self.host_profile)
+ self.assertEqual(resourcebundle.template.getHosts()[0].resource.name, 'hostnamefieldcontentstring')
+
+ return True
+
+ 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.assertTrue(self.is_valid_booking(booking))
+
+ def test_with_valid_form(self):
+ response = self.post()
+
+ self.assertEqual(response.status_code, 200)
+ booking = Booking.objects.first()
+ self.assertIsNotNone(booking)
+ self.assertTrue(self.is_valid_booking(booking))