From 8f005b94ab8f1687c25e4633589ca6a74b7233cd Mon Sep 17 00:00:00 2001 From: Sawyer Bergeron Date: Mon, 11 Feb 2019 12:54:42 -0500 Subject: Add test utils and tests for quick booking Change-Id: Ie76c6fe26622ca8363055b8ebbe0dc6deaed5824 Signed-off-by: Sawyer Bergeron Signed-off-by: Parker Berberian --- dashboard/src/booking/quick_deployer.py | 208 ++++++++++++++-------- dashboard/src/booking/tests/test_models.py | 7 - dashboard/src/booking/tests/test_quick_booking.py | 155 ++++++++++++++++ 3 files changed, 290 insertions(+), 80 deletions(-) create mode 100644 dashboard/src/booking/tests/test_quick_booking.py (limited to 'dashboard/src/booking') diff --git a/dashboard/src/booking/quick_deployer.py b/dashboard/src/booking/quick_deployer.py index 7946ebf..dd78f15 100644 --- a/dashboard/src/booking/quick_deployer.py +++ b/dashboard/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/dashboard/src/booking/tests/test_models.py b/dashboard/src/booking/tests/test_models.py index c7fb25d..6170295 100644 --- a/dashboard/src/booking/tests/test_models.py +++ b/dashboard/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/dashboard/src/booking/tests/test_quick_booking.py b/dashboard/src/booking/tests/test_quick_booking.py new file mode 100644 index 0000000..936a9a5 --- /dev/null +++ b/dashboard/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)) -- cgit 1.2.3-korg