diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/api/tests/test_models_unittest.py | 255 | ||||
-rw-r--r-- | src/booking/forms.py | 19 | ||||
-rw-r--r-- | src/booking/lib.py | 36 | ||||
-rw-r--r-- | src/booking/quick_deployer.py | 9 | ||||
-rw-r--r-- | src/booking/tests/test_quick_booking.py | 105 | ||||
-rw-r--r-- | src/booking/views.py | 2 | ||||
-rw-r--r-- | src/dashboard/testing_utils.py | 647 | ||||
-rw-r--r-- | src/templates/booking/steps/booking_meta.html | 1 | ||||
-rw-r--r-- | src/templates/dashboard/genericselect.html | 71 | ||||
-rw-r--r-- | src/templates/dashboard/searchable_select_multiple.html | 70 | ||||
-rw-r--r-- | src/workflow/booking_workflow.py | 288 | ||||
-rw-r--r-- | src/workflow/forms.py | 333 | ||||
-rw-r--r-- | src/workflow/models.py | 83 | ||||
-rw-r--r-- | src/workflow/opnfv_workflow.py | 84 | ||||
-rw-r--r-- | src/workflow/sw_bundle_workflow.py | 19 | ||||
-rw-r--r-- | src/workflow/urls.py | 4 | ||||
-rw-r--r-- | src/workflow/workflow_factory.py | 3 |
17 files changed, 945 insertions, 1084 deletions
diff --git a/src/api/tests/test_models_unittest.py b/src/api/tests/test_models_unittest.py index 971f757..cabffc9 100644 --- a/src/api/tests/test_models_unittest.py +++ b/src/api/tests/test_models_unittest.py @@ -7,19 +7,14 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - -from datetime import timedelta -from django.utils import timezone - -from booking.models import Booking from api.models import ( Job, JobStatus, JobFactory, - AccessRelation, HostNetworkRelation, HostHardwareRelation, SoftwareRelation, + AccessConfig, ) from resource_inventory.models import ( @@ -30,105 +25,122 @@ from resource_inventory.models import ( from django.test import TestCase, Client 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, - instantiate_booking, + make_host, + make_user, + make_user_profile, + make_lab, + make_installer, + make_image, + make_scenario, + make_os, + make_complete_host_profile, + make_booking, ) class ValidBookingCreatesValidJob(TestCase): @classmethod def setUpTestData(cls): - cls.loginuser = instantiate_user(False, username="newtestuser", password="testpassword") - cls.userprofile = instantiate_userprofile(cls.loginuser) + cls.user = make_user(False, username="newtestuser", password="testpassword") + cls.userprofile = make_user_profile(cls.user) + cls.lab = make_lab() + + 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) + for i in range(30): + make_host(cls.host_profile, cls.lab, name="host" + str(i), labid="host" + str(i)) + cls.client = Client() - lab_user = instantiate_user(True) - cls.lab = instantiate_lab(lab_user) + def setUp(self): + self.booking, self.compute_hostnames, self.jump_hostname = self.create_multinode_generic_booking() - 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) - for i in range(30): - instantiate_host(cls.host_profile, cls.lab, name="host" + str(i), labid="host" + str(i)) - cls.role = instantiate_opnfvrole("Jumphost") - cls.computerole = instantiate_opnfvrole("Compute") - instantiate_publicnet(10, cls.lab) - instantiate_publicnet(12, cls.lab) - instantiate_publicnet(14, cls.lab) + def create_multinode_generic_booking(self): + topology = {} - cls.lab_selected = 'lab_' + str(cls.lab.lab_user.id) + '_selected' - cls.host_selected = 'host_' + str(cls.host_profile.id) + '_selected' + compute_hostnames = ["cmp01", "cmp02", "cmp03"] - cls.post_data = cls.build_post_data() + host_type = HostProfile.objects.first() - cls.client = Client() + universal_networks = [ + {"name": "public", "tagged": False, "public": True}, + {"name": "admin", "tagged": True, "public": False}] + compute_networks = [{"name": "private", "tagged": True, "public": False}] + jumphost_networks = [{"name": "external", "tagged": True, "public": True}] - def setUp(self): - self.client.login( - username=self.loginuser.username, password="testpassword") - self.booking, self.compute_hostnames, self.jump_hostname = self.create_multinode_generic_booking() + # generate a bunch of extra networks + for i in range(10): + net = {"tagged": False, "public": False} + net["name"] = "net" + str(i) + universal_networks.append(net) - @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 generate_booking(self): - self.post() - return Booking.objects.first() + jumphost_info = { + "type": host_type, + "role": OPNFVRole.objects.get_or_create(name="Jumphost")[0], + "nets": self.make_networks(host_type, jumphost_networks + universal_networks), + "image": self.image + } + topology["jump"] = jumphost_info + + for hostname in compute_hostnames: + host_info = { + "type": host_type, + "role": OPNFVRole.objects.get_or_create(name="Compute")[0], + "nets": self.make_networks(host_type, compute_networks + universal_networks), + "image": self.image + } + topology[hostname] = host_info + + booking = make_booking( + owner=self.user, + lab=self.lab, + topology=topology, + installer=self.installer, + scenario=self.scenario + ) + + if not booking.resource: + raise Exception("Booking does not have a resource when trying to pass to makeCompleteJob") + JobFactory.makeCompleteJob(booking) + + return booking, compute_hostnames, "jump" + + def make_networks(self, hostprofile, nets): + """ + distributes nets accross hostprofile's interfaces + returns a 2D array + """ + network_struct = [] + count = hostprofile.interfaceprofile.all().count() + for i in range(count): + network_struct.append([]) + while(nets): + index = len(nets) % count + network_struct[index].append(nets.pop()) + + return network_struct + + # begin tests def test_valid_access_configs(self): job = Job.objects.get(booking=self.booking) self.assertIsNotNone(job) - access_configs = [r.config for r in AccessRelation.objects.filter(job=job).all()] + access_configs = AccessConfig.objects.filter(accessrelation__job=job) - vpnconfigs = [] - sshconfigs = [] + vpn_configs = access_configs.filter(access_type="vpn") + ssh_configs = access_configs.filter(access_type="ssh") - for config in access_configs: - if config.access_type == "vpn": - vpnconfigs.append(config) - elif config.access_type == "ssh": - sshconfigs.append(config) - else: - self.fail(msg="Undefined accessconfig: " + config.access_type + " found") + self.assertFalse(AccessConfig.objects.exclude(access_type__in=["vpn", "ssh"]).exists()) - user_set = [] - user_set.append(self.booking.owner) - user_set += self.booking.collaborators.all() + all_users = list(self.booking.collaborators.all()) + all_users.append(self.booking.owner) - for configs in [vpnconfigs, sshconfigs]: - for user in user_set: - configusers = [c.user for c in configs] - self.assertTrue(user in configusers) + for user in all_users: + self.assertTrue(vpn_configs.filter(user=user).exists()) + self.assertTrue(ssh_configs.filter(user=user).exists()) def test_valid_network_configs(self): job = Job.objects.get(booking=self.booking) @@ -136,12 +148,12 @@ class ValidBookingCreatesValidJob(TestCase): booking_hosts = self.booking.resource.hosts.all() - netrelation_set = HostNetworkRelation.objects.filter(job=job) - netconfig_set = [r.config for r in netrelation_set] + netrelations = HostNetworkRelation.objects.filter(job=job) + netconfigs = [r.config for r in netrelations] - netrelation_hosts = [r.host for r in netrelation_set] + netrelation_hosts = [r.host for r in netrelations] - for config in netconfig_set: + for config in netconfigs: for interface in config.interfaces.all(): self.assertTrue(interface.host in booking_hosts) @@ -172,15 +184,15 @@ class ValidBookingCreatesValidJob(TestCase): job = Job.objects.get(booking=self.booking) self.assertIsNotNone(job) - hrelations = HostHardwareRelation.objects.filter(job=job).all() + hardware_relations = HostHardwareRelation.objects.filter(job=job) - job_hosts = [r.host for r in hrelations] + job_hosts = [r.host for r in hardware_relations] booking_hosts = self.booking.resource.hosts.all() self.assertEqual(len(booking_hosts), len(job_hosts)) - for relation in hrelations: + for relation in hardware_relations: self.assertTrue(relation.host in booking_hosts) self.assertEqual(relation.status, JobStatus.NEW) config = relation.config @@ -213,66 +225,3 @@ class ValidBookingCreatesValidJob(TestCase): self.assertTrue(host.template.resource.name in self.compute_hostnames) else: self.fail(msg="Host with non-configured role name related to job: " + str(role_name)) - - def create_multinode_generic_booking(self): - topology = {} - - compute_hostnames = ["cmp01", "cmp02", "cmp03"] - - host_type = HostProfile.objects.first() - - universal_networks = [ - {"name": "public", "tagged": False, "public": True}, - {"name": "admin", "tagged": True, "public": False}] - just_compute_networks = [{"name": "private", "tagged": True, "public": False}] - just_jumphost_networks = [{"name": "external", "tagged": True, "public": True}] - - # generate a bunch of extra networks - for i in range(10): - net = {"tagged": False, "public": False} - net["name"] = "u_net" + str(i) - universal_networks.append(net) - - jhost_info = {} - jhost_info["type"] = host_type - jhost_info["role"] = OPNFVRole.objects.get(name="Jumphost") - jhost_info["nets"] = self.make_networks(host_type, list(just_jumphost_networks + universal_networks)) - jhost_info["image"] = self.image - topology["jump"] = jhost_info - - for hostname in compute_hostnames: - host_info = {} - host_info["type"] = host_type - host_info["role"] = OPNFVRole.objects.get(name="Compute") - host_info["nets"] = self.make_networks(host_type, list(just_compute_networks + universal_networks)) - host_info["image"] = self.image - topology[hostname] = host_info - - booking = instantiate_booking(self.loginuser, - timezone.now(), - timezone.now() + timedelta(days=1), - "demobooking", - self.lab, - topology=topology, - installer=self.installer, - scenario=self.scenario) - - if not booking.resource: - raise Exception("Booking does not have a resource when trying to pass to makeCompleteJob") - JobFactory.makeCompleteJob(booking) - - return booking, compute_hostnames, "jump" - - """ - evenly distributes networks given across a given profile's interfaces - """ - def make_networks(self, hostprofile, nets): - network_struct = [] - count = hostprofile.interfaceprofile.all().count() - for i in range(count): - network_struct.append([]) - while(nets): - index = len(nets) % count - network_struct[index].append(nets.pop()) - - return network_struct diff --git a/src/booking/forms.py b/src/booking/forms.py index de427ab..e48b293 100644 --- a/src/booking/forms.py +++ b/src/booking/forms.py @@ -10,12 +10,13 @@ import django.forms as forms from django.forms.widgets import NumberInput from workflow.forms import ( - SearchableSelectMultipleWidget, 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 class QuickBookingForm(forms.Form): @@ -27,16 +28,11 @@ class QuickBookingForm(forms.Form): scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) def __init__(self, data=None, user=None, *args, **kwargs): - chosen_users = [] if "default_user" in kwargs: default_user = kwargs.pop("default_user") else: default_user = "you" self.default_user = default_user - if "chosen_users" in kwargs: - chosen_users = kwargs.pop("chosen_users") - elif data and "users" in data: - chosen_users = data.getlist("users") super(QuickBookingForm, self).__init__(data=data, **kwargs) @@ -44,12 +40,13 @@ class QuickBookingForm(forms.Form): Image.objects.filter(public=True) | Image.objects.filter(owner=user) ) - self.fields['users'] = forms.CharField( - widget=SearchableSelectMultipleWidget( - attrs=self.build_search_widget_attrs(chosen_users, default_user=default_user) - ), - required=False + 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) attrs['selection_data'] = 'false' self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(attrs=attrs)) diff --git a/src/booking/lib.py b/src/booking/lib.py new file mode 100644 index 0000000..8132c75 --- /dev/null +++ b/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/src/booking/quick_deployer.py b/src/booking/quick_deployer.py index 763c8a0..ac69c8c 100644 --- a/src/booking/quick_deployer.py +++ b/src/booking/quick_deployer.py @@ -12,7 +12,6 @@ import json import uuid import re from django.db.models import Q -from django.contrib.auth.models import User from datetime import timedelta from django.utils import timezone from account.models import Lab @@ -321,12 +320,8 @@ def create_from_form(form, request): ) booking.pdf = PDFTemplater.makePDF(booking) - users_field = users_field[2:-2] - if users_field: # may be empty after split, if no collaborators entered - users_field = json.loads(users_field) - for collaborator in users_field: - user = User.objects.get(id=collaborator['id']) - booking.collaborators.add(user) + for collaborator in users_field: # list of UserProfiles + booking.collaborators.add(collaborator.user) booking.save() diff --git a/src/booking/tests/test_quick_booking.py b/src/booking/tests/test_quick_booking.py index 936a9a5..e445860 100644 --- a/src/booking/tests/test_quick_booking.py +++ b/src/booking/tests/test_quick_booking.py @@ -13,60 +13,55 @@ 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, + 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, ) -# 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) + cls.user = make_user(False, username="newtestuser", password="testpassword") + make_user_profile(cls.user, True) - lab_user = instantiate_user(True) - cls.lab = instantiate_lab(lab_user) + lab_user = make_user(True) + cls.lab = make_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.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): - 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 + 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() @@ -75,26 +70,26 @@ class QuickBookingValidFormTestCase(TestCase): return response def setUp(self): - self.client.login( - username=self.loginuser.username, password="testpassword") + self.client.login(username=self.user.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') + 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)) - resourcebundle = booking.resource - configbundle = booking.config_bundle + resource_bundle = booking.resource + config_bundle = 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') + opnfv_config = config_bundle.opnfv_config.first() + self.assertEqual(self.installer, opnfv_config.installer) + self.assertEqual(self.scenario, opnfv_config.scenario) - return True + 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'}) @@ -144,7 +139,7 @@ class QuickBookingValidFormTestCase(TestCase): self.assertEqual(response.status_code, 200) booking = Booking.objects.first() self.assertIsNotNone(booking) - self.assertTrue(self.is_valid_booking(booking)) + self.assertValidBooking(booking) def test_with_valid_form(self): response = self.post() @@ -152,4 +147,4 @@ class QuickBookingValidFormTestCase(TestCase): self.assertEqual(response.status_code, 200) booking = Booking.objects.first() self.assertIsNotNone(booking) - self.assertTrue(self.is_valid_booking(booking)) + self.assertValidBooking(booking) diff --git a/src/booking/views.py b/src/booking/views.py index 8211a0c..13e9d01 100644 --- a/src/booking/views.py +++ b/src/booking/views.py @@ -47,7 +47,7 @@ def quick_create(request): context['lab_profile_map'] = profiles - context['form'] = QuickBookingForm(initial={}, chosen_users=[], default_user=request.user.username, user=request.user) + context['form'] = QuickBookingForm(default_user=request.user.username, user=request.user) context.update(drop_filter(request.user)) diff --git a/src/dashboard/testing_utils.py b/src/dashboard/testing_utils.py index 558031d..1cca3e6 100644 --- a/src/dashboard/testing_utils.py +++ b/src/dashboard/testing_utils.py @@ -9,13 +9,13 @@ from django.contrib.auth.models import User from django.core.files.base import ContentFile +from django.utils import timezone import json import re +from datetime import timedelta -from dashboard.exceptions import ( - InvalidHostnameException -) +from dashboard.exceptions import InvalidHostnameException from booking.models import Booking from account.models import UserProfile, Lab, LabStatus, VlanManager, PublicNetwork from resource_inventory.models import ( @@ -31,7 +31,6 @@ from resource_inventory.models import ( OPNFVRole, RamProfile, Network, - Vlan, GenericResourceBundle, GenericResource, GenericHost, @@ -39,30 +38,13 @@ from resource_inventory.models import ( GenericInterface, HostConfiguration, OPNFVConfig, + NetworkConnection, + HostOPNFVConfig ) from resource_inventory.resource_manager import ResourceManager - -class BookingContextData(object): - def prepopulate(self, *args, **kwargs): - self.loginuser = instantiate_user(False, username=kwargs.get("login_username", "newtestuser"), password="testpassword") - instantiate_userprofile(self.loginuser) - - lab_user = kwargs.get("lab_user", instantiate_user(True)) - self.lab = instantiate_lab(lab_user) - - self.host_profile = make_hostprofile_set(self.lab) - self.scenario = instantiate_scenario() - self.installer = instantiate_installer([self.scenario]) - os = instantiate_os([self.installer]) - self.image = instantiate_image(self.lab, 1, self.loginuser, os, self.host_profile) - self.host = instantiate_host(self.host_profile, self.lab) - self.role = instantiate_opnfvrole() - self.pubnet = instantiate_publicnet(10, self.lab) - - """ -Info for instantiate_booking() function: +Info for make_booking() function: [topology] argument structure: the [topology] argument should describe the structure of the pod the top level should be a dictionary, with each key being a hostname @@ -91,424 +73,323 @@ Info for instantiate_booking() function: """ -def instantiate_booking(owner, - start, - end, - booking_identifier, - lab=Lab.objects.first(), - purpose="purposetext", - project="projecttext", - collaborators=[], - topology={}, - installer=None, - scenario=None): - (grb, host_set) = instantiate_grb(topology, owner, lab, booking_identifier) - cb = instantiate_cb(grb, owner, booking_identifier, topology, host_set, installer, scenario) - - resource = ResourceManager.getInstance().convertResourceBundle(grb, lab, cb) +def make_booking(owner=None, start=timezone.now(), + end=timezone.now() + timedelta(days=1), + lab=None, purpose="my_purpose", + project="my_project", collaborators=[], + topology={}, installer=None, scenario=None): - booking = Booking() - - booking.resource = resource + grb, host_set = make_grb(topology, owner, lab) + config_bundle = make_config_bundle(grb, owner, topology, host_set, installer, scenario) + resource = ResourceManager.getInstance().convertResourceBundle(grb, config=config_bundle) if not resource: raise Exception("Resource not created") - booking.config_bundle = cb - booking.start = start - booking.end = end - booking.owner = owner - booking.purpose = purpose - booking.project = project - booking.lab = lab - booking.save() - - return booking - - -def instantiate_cb(grb, - owner, - booking_identifier, - topology={}, - host_set={}, - installer=None, - scenario=None): - cb = ConfigBundle() - cb.owner = owner - cb.name = str(booking_identifier) + "_cb" - cb.description = "cb generated by instantiate_cb() method" - cb.save() - - opnfvconfig = OPNFVConfig() - opnfvconfig.installer = installer - opnfvconfig.scenario = scenario - opnfvconfig.bundle = cb - opnfvconfig.save() + + return Booking.objects.create( + resource=resource, + config_bundle=config_bundle, + start=start, + end=end, + owner=owner, + purpose=purpose, + project=project, + lab=lab + ) + + +def make_config_bundle(grb, owner, topology={}, host_set={}, + installer=None, scenario=None): + cb = ConfigBundle.objects.create( + owner=owner, + name="config bundle " + str(ConfigBundle.objects.count()), + description="cb generated by make_config_bundle() method" + ) + + opnfv_config = OPNFVConfig.objects.create( + installer=installer, + scenario=scenario, + bundle=cb + ) # generate host configurations based on topology and host set for hostname, host_info in topology.items(): - hconf = HostConfiguration() - hconf.bundle = cb - hconf.host = host_set[hostname] - hconf.image = host_info["image"] - hconf.opnfvRole = host_info["role"] - hconf.save() + host_config = HostConfiguration.objects.create( + host=host_set[hostname], + image=host_info["image"], + bundle=cb, + is_head_node=host_info['role'].name.lower() == "jumphost" + ) + HostOPNFVConfig.objects.create( + role=host_info["role"], + host_config=host_config, + opnfv_config=opnfv_config + ) return cb -def instantiate_grb(topology, - owner, - lab, - booking_identifier): +def make_network(name, lab, grb, public): + network = Network(name=name, bundle=grb, is_public=public) + if public: + public_net = lab.vlan_manager.get_public_vlan() + if not public_net: + raise Exception("No more public networks available") + lab.vlan_manager.reserve_public_vlan(public_net.vlan) + network.vlan_id = public_net.vlan + else: + private_net = lab.vlan_manager.get_vlan() + if not private_net: + raise Exception("No more generic vlans are available") + lab.vlan_manager.reserve_vlans([private_net]) + network.vlan_id = private_net + + network.save() + return network + - grb = GenericResourceBundle(owner=owner, lab=lab) - grb.name = str(booking_identifier) + "_grb" - grb.description = "grb generated by instantiate_grb() method" - grb.save() +def make_grb(topology, owner, lab): + + grb = GenericResourceBundle.objects.create( + owner=owner, + lab=lab, + name="Generic ResourceBundle " + str(GenericResourceBundle.objects.count()), + description="grb generated by make_grb() method" + ) networks = {} host_set = {} - for hostname in topology.keys(): - info = topology[hostname] + for hostname, info in topology.items(): host_profile = info["type"] # need to construct host from hostname and type - ghost = instantiate_ghost(grb, host_profile, hostname) - host_set[hostname] = ghost - - ghost.save() + generic_host = make_generic_host(grb, host_profile, hostname) + host_set[hostname] = generic_host # set up networks nets = info["nets"] for interface_index, interface_profile in enumerate(host_profile.interfaceprofile.all()): - generic_interface = GenericInterface() - generic_interface.host = ghost - generic_interface.profile = interface_profile - generic_interface.save() - + generic_interface = GenericInterface.objects.create(host=generic_host, profile=interface_profile) netconfig = nets[interface_index] - for network_index, network_info in enumerate(netconfig): + for network_info in netconfig: network_name = network_info["name"] - network = None - if network_name in networks: - network = networks[network_name] - else: - network = Network() - network.name = network_name - network.vlan_id = lab.vlan_manager.get_vlan() - network.save() - networks[network_name] = network - if network_info["public"]: - public_net = lab.vlan_manager.get_public_vlan() - if not public_net: - raise Exception("No more public networks available") - lab.vlan_manager.reserve_public_vlan(public_net.vlan) - network.vlan_id = public_net.vlan - else: - private_net = lab.vlan_manager.get_vlan() - if not private_net: - raise Exception("No more generic vlans are available") - lab.vlan_manager.reserve_vlans([private_net]) - network.vlan_id = private_net - - vlan = Vlan() - vlan.vlan_id = network.vlan_id - vlan.public = network_info["public"] - vlan.tagged = network_info["tagged"] - vlan.save() - generic_interface.vlans.add(vlan) - - return (grb, host_set) - - -def instantiate_ghost(grb, host_profile, 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=grb, name=hostname) - gresource.save() + if network_name not in networks: + networks[network_name] = make_network(network_name, lab, grb, network_info['public']) + + generic_interface.connections.add(NetworkConnection.objects.create( + network=networks[network_name], + vlan_is_tagged=network_info["tagged"] + )) - ghost = GenericHost() - ghost.resource = gresource - ghost.profile = host_profile - ghost.save() + return grb, host_set - return ghost +def make_generic_host(grb, host_profile, 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") + gresource = GenericResource.objects.create(bundle=grb, name=hostname) + + return GenericHost.objects.create(resource=gresource, profile=host_profile) -def instantiate_user(is_superuser, - username="testuser", - password="testpassword", - email="default_email@user.com" - ): + +def make_user(is_superuser=False, username="testuser", + password="testpassword", email="default_email@user.com"): user = User.objects.create_user(username=username, email=email, password=password) user.is_superuser = is_superuser - user.save() return user -def instantiate_userprofile(user, email_addr="email@email.com", company="company", full_name="John Doe", booking_privledge=True, ssh_file=None): - up = UserProfile() - up.email_address = email_addr - up.company = company - up.full_name = full_name - up.booking_privledge = booking_privledge - up.user = user - up.save() - up.ssh_public_key.save("user_ssh_key", ssh_file if ssh_file else ContentFile("public key content string")) +def make_user_profile(user=None, email_addr="email@email.com", + company="company", full_name="John Doe", + booking_privledge=True, ssh_file=None): + user = user or User.objects.first() or make_user() + profile = UserProfile.objects.create( + email_addr=email_addr, + company=company, + full_name=full_name, + booking_privledge=booking_privledge, + user=user + ) + profile.ssh_public_key.save("user_ssh_key", ssh_file if ssh_file else ContentFile("public key content string")) - return up + return profile -def instantiate_vlanmanager(vlans=None, - block_size=20, - allow_overlapping=False, - reserved_vlans=None - ): - vlanmanager = VlanManager() +def make_vlan_manager(vlans=None, block_size=20, allow_overlapping=False, reserved_vlans=None): if not vlans: - vlans = [] - for vlan in range(0, 4095): - vlans.append(vlan % 2) - vlanmanager.vlans = json.dumps(vlans) + vlans = [vlan % 2 for vlan in range(4095)] if not reserved_vlans: - reserved_vlans = [] - for vlan in range(0, 4095): - reserved_vlans.append(0) - vlanmanager.reserved_vlans = json.dumps(vlans) - vlanmanager.block_size = block_size - vlanmanager.allow_overlapping = allow_overlapping - - vlanmanager.save() + reserved_vlans = [0 for i in range(4095)] - return vlanmanager + return VlanManager.objects.create( + vlans=json.dumps(vlans), + reserved_vlans=json.dumps(vlans), + block_size=block_size, + allow_overlapping=allow_overlapping + ) -def instantiate_lab(user=None, - name="Test Lab Instance", - status=LabStatus.UP, - vlan_manager=None - ): +def make_lab(user=None, name="Test Lab Instance", + status=LabStatus.UP, vlan_manager=None, + pub_net_count=5): if not vlan_manager: - vlan_manager = instantiate_vlanmanager() + vlan_manager = make_vlan_manager() if not user: - user = instantiate_user(True, 'test_user', 'test_pass', 'test_user@test_site.org') - - lab = Lab() - lab.lab_user = user - lab.name = name - lab.contact_email = 'test_lab@test_site.org' - lab.contact_phone = '603 123 4567' - lab.status = status - lab.vlan_manager = vlan_manager - lab.description = 'test lab instantiation' - lab.api_token = '12345678' - - lab.save() + user = make_user() + + lab = Lab.objects.create( + lab_user=user, + name=name, + contact_email='test_lab@test_site.org', + contact_phone='603 123 4567', + status=status, + vlan_manager=vlan_manager, + description='test lab instantiation', + api_token='12345678' + ) + + for i in range(pub_net_count): + make_public_net(vlan=i * 2 + 1, lab=lab) return lab """ -resource_inventory instantiation section for permenant resources +resource_inventory instantiation section for permanent resources """ -def make_hostprofile_set(lab, name="test_hostprofile"): - hostprof = instantiate_hostprofile(lab, name=name) - instantiate_diskprofile(hostprof, 500, name=name) - instantiate_cpuprofile(hostprof) - instantiate_interfaceprofile(hostprof, name=name) - instantiate_ramprofile(hostprof) - - return hostprof - - -def instantiate_hostprofile(lab, - host_type=0, - name="test hostprofile instance" - ): - hostprof = HostProfile() - hostprof.host_type = host_type - hostprof.name = name - hostprof.description = 'test hostprofile instance' - hostprof.save() - hostprof.labs.add(lab) - - hostprof.save() - - return hostprof - - -def instantiate_ramprofile(host, - channels=4, - amount=256): - ramprof = RamProfile() - ramprof.host = host - ramprof.amount = amount - ramprof.channels = channels - ramprof.save() - - return ramprof - - -def instantiate_diskprofile(hostprofile, - size=0, - media_type="SSD", - name="test diskprofile", - rotation=0, - interface="sata"): - - diskprof = DiskProfile() - diskprof.name = name - diskprof.size = size - diskprof.media_type = media_type - diskprof.host = hostprofile - diskprof.rotation = rotation - diskprof.interface = interface - - diskprof.save() - - return diskprof - - -def instantiate_cpuprofile(hostprofile, - cores=4, - architecture="x86_64", - cpus=4, - ): - cpuprof = CpuProfile() - cpuprof.cores = cores - cpuprof.architecture = architecture - cpuprof.cpus = cpus - cpuprof.host = hostprofile - cpuprof.cflags = '' - - cpuprof.save() - - return cpuprof - - -def instantiate_interfaceprofile(hostprofile, - speed=1000, - name="test interface profile", - nic_type="pcie" - ): - intprof = InterfaceProfile() - intprof.host = hostprofile - intprof.name = name - intprof.speed = speed - intprof.nic_type = nic_type - - intprof.save() - - return intprof - - -def instantiate_image(lab, - lab_id, - owner, - os, - host_profile, - public=True, - name="default image", - description="default image" - ): - image = Image() - image.from_lab = lab - image.lab_id = lab_id - image.os = os - image.host_type = host_profile - image.public = public - image.name = name - image.description = description - - image.save() - - return image - - -def instantiate_scenario(name="test scenario"): - scenario = Scenario() - scenario.name = name - scenario.save() - return scenario - - -def instantiate_installer(supported_scenarios, - name="test installer" - ): - installer = Installer() - installer.name = name - installer.save() - for scenario in supported_scenarios: +def make_complete_host_profile(lab, name="test_hostprofile"): + host_profile = make_host_profile(lab, name=name) + make_disk_profile(host_profile, 500, name=name) + make_cpu_profile(host_profile) + make_interface_profile(host_profile, name=name) + make_ram_profile(host_profile) + + return host_profile + + +def make_host_profile(lab, host_type=0, name="test hostprofile"): + host_profile = HostProfile.objects.create( + host_type=host_type, + name=name, + description='test hostprofile instance' + ) + host_profile.labs.add(lab) + + return host_profile + + +def make_ram_profile(host, channels=4, amount=256): + return RamProfile.objects.create( + host=host, + amount=amount, + channels=channels + ) + + +def make_disk_profile(hostprofile, size=0, media_type="SSD", + name="test diskprofile", rotation=0, + interface="sata"): + return DiskProfile.objects.create( + name=name, + size=size, + media_type=media_type, + host=hostprofile, + rotation=rotation, + interface=interface + ) + + +def make_cpu_profile(hostprofile, + cores=4, + architecture="x86_64", + cpus=4,): + return CpuProfile.objects.create( + cores=cores, + architecture=architecture, + cpus=cpus, + host=hostprofile, + cflags='' + ) + + +def make_interface_profile(hostprofile, + speed=1000, + name="test interface profile", + nic_type="pcie"): + return InterfaceProfile.objects.create( + host=hostprofile, + name=name, + speed=speed, + nic_type=nic_type + ) + + +def make_image(lab, lab_id, owner, os, host_profile, + public=True, name="default image", description="default image"): + return Image.objects.create( + from_lab=lab, + lab_id=lab_id, + os=os, + host_type=host_profile, + public=public, + name=name, + description=description + ) + + +def make_scenario(name="test scenario"): + return Scenario.objects.create(name=name) + + +def make_installer(scenarios, name="test installer"): + installer = Installer.objects.create(name=name) + for scenario in scenarios: installer.sup_scenarios.add(scenario) - installer.save() return installer -def instantiate_os(supported_installers, - name="test operating system", - ): - os = Opsys() - os.name = name - os.save() - for installer in supported_installers: +def make_os(installers, name="test OS"): + os = Opsys.objects.create(name=name) + for installer in installers: os.sup_installers.add(installer) - os.save() + return os -def instantiate_host(host_profile, - lab, - labid="test_host", - name="test_host", - booked=False, - working=True, - config=None, - template=None, - bundle=None, - model="Model 1", - vendor="ACME"): - host = Host() - host.lab = lab - host.profile = host_profile - host.name = name - host.booked = booked - host.working = working - host.config = config - host.template = template - host.bundle = bundle - host.model = model - host.vendor = vendor - - host.save() - - return host - - -def instantiate_opnfvrole(name="Jumphost", - description="test opnfvrole"): - role = OPNFVRole() - role.name = name - role.description = description - role.save() - - return role - - -def instantiate_publicnet(vlan, - lab, - in_use=False, - cidr="0.0.0.0/0", - gateway="0.0.0.0"): - pubnet = PublicNetwork() - pubnet.lab = lab - pubnet.vlan = vlan - pubnet.cidr = cidr - pubnet.gateway = gateway - pubnet.save() - - return pubnet +def make_host(host_profile, lab, labid="test_host", name="test_host", + booked=False, working=True, config=None, template=None, + bundle=None, model="Model 1", vendor="ACME"): + return Host.objects.create( + lab=lab, + profile=host_profile, + name=name, + booked=booked, + working=working, + config=config, + template=template, + bundle=bundle, + model=model, + vendor=vendor + ) + + +def make_opnfv_role(name="Jumphost", description="test opnfvrole"): + return OPNFVRole.objects.create( + name=name, + description=description + ) + + +def make_public_net(vlan, lab, in_use=False, + cidr="0.0.0.0/0", gateway="0.0.0.0"): + return PublicNetwork.objects.create( + lab=lab, + vlan=vlan, + cidr=cidr, + gateway=gateway + ) diff --git a/src/templates/booking/steps/booking_meta.html b/src/templates/booking/steps/booking_meta.html index fe43f53..cdd7834 100644 --- a/src/templates/booking/steps/booking_meta.html +++ b/src/templates/booking/steps/booking_meta.html @@ -48,6 +48,7 @@ </script> {% bootstrap_field form.info_file %} <p>You must provide a url to your project's INFO.yaml file if you are a PTL and you are trying to book a POD with multiple servers in it.</p> + {% bootstrap_field form.deploy_opnfv %} </div> <div class="panel panel_center"> </div> diff --git a/src/templates/dashboard/genericselect.html b/src/templates/dashboard/genericselect.html new file mode 100644 index 0000000..fc29ee6 --- /dev/null +++ b/src/templates/dashboard/genericselect.html @@ -0,0 +1,71 @@ +{% extends "workflow/viewport-element.html" %} +{% load staticfiles %} + +{% load bootstrap3 %} + +{% block content %} + +<style> +#{{select_type}}_form_div { + width: 100%; + padding: 5%; + } + + .panel { + border: none; + } + .select_panels { + width: 100%; + display: grid; + grid-template-columns: 45% 10% 45%; + + } + + .panel_center { + text-align: center; + border: none; + + } + .panel_center p{ + font-size: 20pt; + } +</style> + +<div id="{{select_type}}_form_div"> + <div class="select_panels"> + <div class="panel_chooser panel"> + <form id="{{select_type}}_select_form" method="post" action="" class="form" id="{{select_type}}selectorform"> + {% csrf_token %} + {{ form|default:"<p>no form loaded</p>" }} + {% buttons %} + + {% endbuttons %} + </form> + </div> + <div class="panel_center panel"><p>OR</p></div> + <div class="panel_add panel"> + <button class="btn {% if disabled %} disabled {% endif %}" + style="width: 100%; height: 100%;" + {% if not disabled %}onclick="parent.add_wf({{addable_type_num}})" + {% endif %}>Create {{select_type_title}} + </button> + </div> + </div> +</div> +<script> + {% if disabled %} + disable(); + {% endif %} +</script> + +{% endblock content %} +{% block onleave %} +var form = $("#{{select_type}}_select_form"); +var formData = form.serialize(); +var req = new XMLHttpRequest(); +req.open("POST", "/wf/workflow/", false); +req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); +req.onerror = function() { alert("problem with form submission"); } +req.send(formData); +{% endblock %} + diff --git a/src/templates/dashboard/searchable_select_multiple.html b/src/templates/dashboard/searchable_select_multiple.html index c08fbe5..69394ab 100644 --- a/src/templates/dashboard/searchable_select_multiple.html +++ b/src/templates/dashboard/searchable_select_multiple.html @@ -7,9 +7,10 @@ <p>Please make a different selection, as the current config conflicts with the selected pod</p> {% endif %} </div> - <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="{{initial.name}}" oninput="search(this.value)" + <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="" oninput="search(this.value)" {% if disabled %} disabled {% endif %} > + </input> <input type="hidden" id="selector" name="{{ name }}" class="form-control" style="display: none;" {% if disabled %} disabled {% endif %} @@ -18,14 +19,6 @@ <ul id="drop_results"></ul> - - <div id="default_entry_wrap" style="display: none;"> - <div class="list_entry unremovable_list_entry"> - <p id="default_text" class="full_name"></p> - <button class="btn-remove btn disabled">remove</button> - </div> - </div> - <div id="added_list"> </div> @@ -96,16 +89,32 @@ <script type="text/javascript"> //flags - var show_from_noentry = {{show_from_noentry|default:"false"}}; - var show_x_results = {{show_x_results|default:-1}}; - var results_scrollable = {{results_scrollable|default:"false"}}; - var selectable_limit = {{selectable_limit|default:-1}}; - var field_name = "{{name|default:"users"}}"; - var placeholder = "{{placeholder|default:"begin typing"}}"; - var default_entry = "{{default_entry}}"; + var show_from_noentry = {{show_from_noentry|yesno:"true,false"}}; // whether to show any results before user starts typing + var show_x_results = {{show_x_results|default:-1}}; // how many results to show at a time, -1 shows all results + var results_scrollable = {{results_scrollable|yesno:"true,false"}}; // whether list should be scrollable + var selectable_limit = {{selectable_limit|default:-1}}; // how many selections can be made, -1 allows infinitely many + var placeholder = "{{placeholder|default:"begin typing"}}"; // placeholder that goes in text box //needed info - var items = {{items|safe}} + var items = {{items|safe}} // items to add to trie. Type is a dictionary of dictionaries with structure: + /* + { + id# : { + "id": any, identifiable on backend + "small_name": string, displayed first (before separator), searchable (use for e.g. username) + "expanded_name": string, displayed second (after separator), searchable (use for e.g. email address) + "string": string, not displayed, still searchable + } + } + */ + + /* used later: + {{ selectable_limit }}: changes what number displays for field + {{ name }}: form identifiable name, relevant for backend + // when submitted, form will contain field data in post with name as the key + {{ placeholder }}: "greyed out" contents put into search field initially to guide user as to what they're searching for + {{ initial }}: in search_field_init(), marked safe, an array of id's each referring to an id from items + */ //tries var expanded_name_trie = {} @@ -116,18 +125,6 @@ string_trie.isComplete = false; var added_items = []; - var initial_log = {{ initial|safe }}; - - var added_template = {{ added_list|default:"{}" }}; - - if( default_entry ) - { - var default_entry_div = document.getElementById("default_entry_wrap"); - default_entry_div.style.display = "inherit"; - - var entry_p = document.getElementById("default_text"); - entry_p.innerText = default_entry; - } search_field_init(); @@ -335,7 +332,7 @@ function select_item(item_id) { //TODO make faster - var item = items[item_id]; + var item = items[item_id]['id']; if( (selectable_limit > -1 && added_items.length < selectable_limit) || selectable_limit < 0 ) { if( added_items.indexOf(item) == -1 ) @@ -357,11 +354,6 @@ document.getElementById("user_field").focus(); } - function edit_item(item_id){ - var wf_type = "{{wf_type}}"; - parent.add_edit_wf(wf_type, item_id); - } - function update_selected_list() { document.getElementById("added_number").innerText = added_items.length; @@ -382,7 +374,8 @@ for( var key in added_items ) { - item = added_items[key]; + item_id = added_items[key]; + item = items[item_id]; list_html += '<div class="list_entry"><p class="full_name">' + item["expanded_name"] @@ -391,11 +384,6 @@ + '</p><button onclick="remove_item(' + Object.values(items).indexOf(item) + ')" class="btn-remove btn">remove</button>'; - {% if edit %} - list_html += '<button onclick="edit_item(' - + item['id'] - + ')" class="btn-remove btn">edit</button>'; - {% endif %} list_html += '</div>'; } diff --git a/src/workflow/booking_workflow.py b/src/workflow/booking_workflow.py index eb87728..42372ce 100644 --- a/src/workflow/booking_workflow.py +++ b/src/workflow/booking_workflow.py @@ -8,188 +8,136 @@ ############################################################################## from django.contrib import messages -from django.shortcuts import render -from django.contrib.auth.models import User from django.utils import timezone -import json from datetime import timedelta from booking.models import Booking -from workflow.models import WorkflowStep -from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm -from resource_inventory.models import GenericResourceBundle, ResourceBundle, ConfigBundle +from workflow.models import WorkflowStep, AbstractSelectOrCreate +from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm, OPNFVSelectForm +from resource_inventory.models import GenericResourceBundle, ConfigBundle, OPNFVConfig -class Resource_Select(WorkflowStep): - template = 'booking/steps/resource_select.html' +""" +subclassing notes: + subclasses have to define the following class attributes: + self.repo_key: main output of step, where the selected/created single selector + result is placed at the end + self.confirm_key: +""" + + +class Abstract_Resource_Select(AbstractSelectOrCreate): + form = ResourceSelectorForm + template = 'dashboard/genericselect.html' title = "Select Resource" description = "Select a resource template to use for your deployment" short_title = "pod select" def __init__(self, *args, **kwargs): - super(Resource_Select, self).__init__(*args, **kwargs) - self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE - self.repo_check_key = False - self.confirm_key = "booking" + super().__init__(*args, **kwargs) + self.select_repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE + self.confirm_key = self.workflow_type - def get_context(self): - context = super(Resource_Select, self).get_context() - default = [] - - chosen_bundle = self.repo_get(self.repo_key, False) - if chosen_bundle: - default.append(chosen_bundle.id) + def alert_bundle_missing(self): + self.set_invalid("Please select a valid resource bundle") - bundle = chosen_bundle - edit = self.repo_get(self.repo.EDIT, False) + def get_form_queryset(self): user = self.repo_get(self.repo.SESSION_USER) - context['form'] = ResourceSelectorForm( - data={"user": user}, - chosen_resource=default, - bundle=bundle, - edit=edit - ) - return context + qs = GenericResourceBundle.objects.filter(owner=user) + return qs - def post_render(self, request): - form = ResourceSelectorForm(request.POST) - context = self.get_context() - if form.is_valid(): - data = form.cleaned_data['generic_resource_bundle'] - data = data[2:-2] - if not data: - self.set_invalid("Please select a valid bundle") - return render(request, self.template, context) - selected_bundle = json.loads(data) - if len(selected_bundle) < 1: - self.set_invalid("Please select a valid bundle") - return render(request, self.template, context) - selected_id = selected_bundle[0]['id'] - gresource_bundle = None - try: - selected_id = int(selected_id) - gresource_bundle = GenericResourceBundle.objects.get(id=selected_id) - except ValueError: - # we want the bundle in the repo - gresource_bundle = self.repo_get( - self.repo.GRESOURCE_BUNDLE_MODELS, - {} - ).get("bundle", GenericResourceBundle()) - self.repo_put( - self.repo_key, - gresource_bundle - ) - confirm = self.repo_get(self.repo.CONFIRMATION) - if self.confirm_key not in confirm: - confirm[self.confirm_key] = {} - confirm[self.confirm_key]["resource name"] = gresource_bundle.name - self.repo_put(self.repo.CONFIRMATION, confirm) - messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True) - self.set_valid("Step Completed") - return render(request, self.template, context) - else: - messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True) - self.set_invalid("Please complete the fields highlighted in red to continue") - return render(request, self.template, context) + def get_page_context(self): + return { + 'select_type': 'resource', + 'select_type_title': 'Resource Bundle', + 'addable_type_num': 1 + } + def put_confirm_info(self, bundle): + confirm_dict = self.repo_get(self.repo.CONFIRMATION) + if self.confirm_key not in confirm_dict: + confirm_dict[self.confirm_key] = {} + confirm_dict[self.confirm_key]["Resource Template"] = bundle.name + self.repo_put(self.repo.CONFIRMATION, confirm_dict) -class Booking_Resource_Select(Resource_Select): - def __init__(self, *args, **kwargs): - super(Booking_Resource_Select, self).__init__(*args, **kwargs) - self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE - self.confirm_key = "booking" +class Booking_Resource_Select(Abstract_Resource_Select): + workflow_type = "booking" - def get_context(self): - context = super(Booking_Resource_Select, self).get_context() - return context - def post_render(self, request): - response = super(Booking_Resource_Select, self).post_render(request) - models = self.repo_get(self.repo.BOOKING_MODELS, {}) - if "booking" not in models: - models['booking'] = Booking() - booking = models['booking'] - resource = self.repo_get(self.repo_key, False) - if resource: - try: - booking.resource.template = resource - except Exception: - booking.resource = ResourceBundle(template=resource) - models['booking'] = booking - self.repo_put(self.repo.BOOKING_MODELS, models) - return response - - -class SWConfig_Select(WorkflowStep): - template = 'booking/steps/swconfig_select.html' +class SWConfig_Select(AbstractSelectOrCreate): title = "Select Software Configuration" description = "Choose the software and related configurations you want to have used for your deployment" short_title = "pod config" + form = SWConfigSelectorForm - def post_render(self, request): - form = SWConfigSelectorForm(request.POST) - if form.is_valid(): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE + self.confirm_key = "booking" - bundle_json = form.cleaned_data['software_bundle'] - bundle_json = bundle_json[2:-2] # Stupid django string bug - if not bundle_json: - self.set_invalid("Please select a valid config") - return self.render(request) - bundle_json = json.loads(bundle_json) - if len(bundle_json) < 1: - self.set_invalid("Please select a valid config") - return self.render(request) - bundle = None - id = int(bundle_json[0]['id']) - bundle = ConfigBundle.objects.get(id=id) - - grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE) - - if grb and bundle.bundle != grb: - self.set_invalid("Incompatible config selected for resource bundle") - return self.render(request) - if not grb: - self.repo_set(self.repo.SELECTED_GRESOURCE_BUNDLE, bundle.bundle) + def alert_bundle_missing(self): + self.set_invalid("Please select a valid pod config") - models = self.repo_get(self.repo.BOOKING_MODELS, {}) - if "booking" not in models: - models['booking'] = Booking() - models['booking'].config_bundle = bundle - self.repo_put(self.repo.BOOKING_MODELS, models) - confirm = self.repo_get(self.repo.CONFIRMATION) - if "booking" not in confirm: - confirm['booking'] = {} - confirm['booking']["configuration name"] = bundle.name - self.repo_put(self.repo.CONFIRMATION, confirm) - self.set_valid("Step Completed") - messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True) - else: - self.set_invalid("Please select or create a valid config") - messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True) + def get_form_queryset(self): + user = self.repo_get(self.repo.SESSION_USER) + grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE) + qs = ConfigBundle.objects.filter(owner=user).filter(bundle=grb) + return qs - return self.render(request) + def put_confirm_info(self, bundle): + confirm_dict = self.repo_get(self.repo.CONFIRMATION) + if self.confirm_key not in confirm_dict: + confirm_dict[self.confirm_key] = {} + confirm_dict[self.confirm_key]["Software Configuration"] = bundle.name + self.repo_put(self.repo.CONFIRMATION, confirm_dict) - def get_context(self): - context = super(SWConfig_Select, self).get_context() - default = [] - bundle = None - chosen_bundle = None - created_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) - booking = self.repo_get(self.repo.BOOKING_MODELS, {}).get("booking", False) - try: - chosen_bundle = booking.config_bundle - default.append(chosen_bundle.id) - bundle = chosen_bundle - except Exception: - if created_bundle: - default.append(created_bundle.id) - bundle = created_bundle - edit = self.repo_get(self.repo.EDIT, False) - grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE) - context['form'] = SWConfigSelectorForm(chosen_software=default, bundle=bundle, edit=edit, resource=grb) - return context + def get_page_context(self): + return { + 'select_type': 'swconfig', + 'select_type_title': 'Software Config', + 'addable_type_num': 2 + } + + +class OPNFV_EnablePicker(object): + pass + + +class OPNFV_Select(AbstractSelectOrCreate, OPNFV_EnablePicker): + title = "Choose an OPNFV Config" + description = "Choose or create a description of how you want to deploy OPNFV" + short_title = "opnfv config" + form = OPNFVSelectForm + enabled = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.select_repo_key = self.repo.SELECTED_OPNFV_CONFIG + self.confirm_key = "booking" + + def alert_bundle_missing(self): + self.set_invalid("Please select a valid OPNFV config") + + def get_form_queryset(self): + cb = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) + qs = OPNFVConfig.objects.filter(bundle=cb) + return qs + + def put_confirm_info(self, config): + confirm_dict = self.repo_get(self.repo.CONFIRMATION) + if self.confirm_key not in confirm_dict: + confirm_dict[self.confirm_key] = {} + confirm_dict[self.confirm_key]["OPNFV Configuration"] = config.name + self.repo_put(self.repo.CONFIRMATION, confirm_dict) + + def get_page_context(self): + return { + 'select_type': 'opnfv', + 'select_type_title': 'OPNFV Config', + 'addable_type_num': 4 + } class Booking_Meta(WorkflowStep): @@ -214,23 +162,17 @@ class Booking_Meta(WorkflowStep): initial['info_file'] = info users = models.get("collaborators", []) for user in users: - default.append(user.id) + default.append(user.userprofile) except Exception: pass - default_user = self.repo_get(self.repo.SESSION_USER) - if default_user is None: - # TODO: error - default_user = "you" - else: - default_user = default_user.username + owner = self.repo_get(self.repo.SESSION_USER) - context['form'] = BookingMetaForm(initial=initial, chosen_users=default, default_user=default_user) + context['form'] = BookingMetaForm(initial=initial, user_initial=default, owner=owner) return context def post_render(self, request): - form = BookingMetaForm(data=request.POST) - context = self.get_context() + form = BookingMetaForm(data=request.POST, owner=request.user) forms = self.repo_get(self.repo.BOOKING_FORMS, {}) @@ -253,15 +195,16 @@ class Booking_Meta(WorkflowStep): for key in ['length', 'project', 'purpose']: confirm['booking'][key] = form.cleaned_data[key] - user_data = form.cleaned_data['users'] + if form.cleaned_data["deploy_opnfv"]: + self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=True) + else: + self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=False) + + userprofile_list = form.cleaned_data['users'] confirm['booking']['collaborators'] = [] - user_data = user_data[2:-2] # fixes malformed string from querydict - if user_data: - form_users = json.loads(user_data) - for user_json in form_users: - user = User.objects.get(pk=user_json['id']) - models['collaborators'].append(user) - confirm['booking']['collaborators'].append(user.username) + for userprofile in userprofile_list: + models['collaborators'].append(userprofile.user) + confirm['booking']['collaborators'].append(userprofile.user.username) info_file = form.cleaned_data.get("info_file", False) if info_file: @@ -274,5 +217,4 @@ class Booking_Meta(WorkflowStep): else: messages.add_message(request, messages.ERROR, "Form didn't validate", fail_silently=True) self.set_invalid("Please complete the fields highlighted in red to continue") - context['form'] = form # TODO: store this form - return render(request, self.template, context) + return self.render(request) diff --git a/src/workflow/forms.py b/src/workflow/forms.py index 6d26b5c..bd2d14a 100644 --- a/src/workflow/forms.py +++ b/src/workflow/forms.py @@ -9,40 +9,37 @@ import django.forms as forms -from django.forms import widgets +from django.forms import widgets, ValidationError from django.utils.safestring import mark_safe from django.template.loader import render_to_string from django.forms.widgets import NumberInput +import json + from account.models import Lab from account.models import UserProfile from resource_inventory.models import ( - GenericResourceBundle, - ConfigBundle, OPNFVRole, Installer, Scenario, ) +from booking.lib import get_user_items, get_user_field_opts class SearchableSelectMultipleWidget(widgets.SelectMultiple): template_name = 'dashboard/searchable_select_multiple.html' def __init__(self, attrs=None): - self.items = attrs['set'] + self.items = attrs['items'] self.show_from_noentry = attrs['show_from_noentry'] self.show_x_results = attrs['show_x_results'] - self.results_scrollable = attrs['scrollable'] + self.results_scrollable = attrs['results_scrollable'] self.selectable_limit = attrs['selectable_limit'] self.placeholder = attrs['placeholder'] self.name = attrs['name'] - self.initial = attrs.get("initial", "") - self.default_entry = attrs.get("default_entry", "") - self.edit = attrs.get("edit", False) - self.wf_type = attrs.get("wf_type") - self.incompatible = attrs.get("incompatible", "false") + self.initial = attrs.get("initial", []) - super(SearchableSelectMultipleWidget, self).__init__(attrs) + super(SearchableSelectMultipleWidget, self).__init__() def render(self, name, value, attrs=None, renderer=None): @@ -59,132 +56,160 @@ class SearchableSelectMultipleWidget(widgets.SelectMultiple): 'selectable_limit': self.selectable_limit, 'placeholder': self.placeholder, 'initial': self.initial, - 'default_entry': self.default_entry, - 'edit': self.edit, - 'wf_type': self.wf_type, - 'incompatible': self.incompatible } -class ResourceSelectorForm(forms.Form): +class SearchableSelectMultipleField(forms.Field): + def __init__(self, *args, required=True, widget=None, label=None, disabled=False, + items=None, queryset=None, show_from_noentry=True, show_x_results=-1, + results_scrollable=False, selectable_limit=-1, placeholder="search here", + name="searchable_select", initial=[], **kwargs): + """from the documentation: + # required -- Boolean that specifies whether the field is required. + # True by default. + # widget -- A Widget class, or instance of a Widget class, that should + # be used for this Field when displaying it. Each Field has a + # default Widget that it'll use if you don't specify this. In + # most cases, the default widget is TextInput. + # label -- A verbose name for this field, for use in displaying this + # field in a form. By default, Django will use a "pretty" + # version of the form field name, if the Field is part of a + # Form. + # initial -- A value to use in this Field's initial display. This value + # is *not* used as a fallback if data isn't given. + # help_text -- An optional string to use as "help text" for this Field. + # error_messages -- An optional dictionary to override the default + # messages that the field will raise. + # show_hidden_initial -- Boolean that specifies if it is needed to render a + # hidden widget with initial value after widget. + # validators -- List of additional validators to use + # localize -- Boolean that specifies if the field should be localized. + # disabled -- Boolean that specifies whether the field is disabled, that + # is its widget is shown in the form but not editable. + # label_suffix -- Suffix to be added to the label. Overrides + # form's label_suffix. + """ + + self.widget = widget + if self.widget is None: + self.widget = SearchableSelectMultipleWidget( + attrs={ + 'items': items, + 'initial': [obj.id for obj in initial], + 'show_from_noentry': show_from_noentry, + 'show_x_results': show_x_results, + 'results_scrollable': results_scrollable, + 'selectable_limit': selectable_limit, + 'placeholder': placeholder, + 'name': name, + 'disabled': disabled + } + ) + self.disabled = disabled + self.queryset = queryset + self.selectable_limit = selectable_limit - def __init__(self, data=None, **kwargs): - chosen_resource = "" - bundle = None - edit = False - if "chosen_resource" in kwargs: - chosen_resource = kwargs.pop("chosen_resource") - if "bundle" in kwargs: - bundle = kwargs.pop("bundle") - if "edit" in kwargs: - edit = kwargs.pop("edit") - super(ResourceSelectorForm, self).__init__(data=data, **kwargs) - queryset = GenericResourceBundle.objects.select_related("owner").all() - if data and 'user' in data: - queryset = queryset.filter(owner=data['user']) + super().__init__(disabled=disabled, **kwargs) - attrs = self.build_search_widget_attrs(chosen_resource, bundle, edit, queryset) + self.required = required - self.fields['generic_resource_bundle'] = forms.CharField( - widget=SearchableSelectMultipleWidget(attrs=attrs) + def clean(self, data): + data = data[0] + if not data: + if self.required: + raise ValidationError("Nothing was selected") + else: + return [] + data_as_list = json.loads(data) + if self.selectable_limit != -1: + if len(data_as_list) > self.selectable_limit: + raise ValidationError("Too many items were selected") + + items = [] + for elem in data_as_list: + items.append(self.queryset.get(id=elem)) + + return items + + +class SearchableSelectAbstractForm(forms.Form): + def __init__(self, *args, queryset=None, initial=[], **kwargs): + self.queryset = queryset + items = self.generate_items(self.queryset) + options = self.generate_options() + + super(SearchableSelectAbstractForm, self).__init__(*args, **kwargs) + self.fields['searchable_select'] = SearchableSelectMultipleField( + initial=initial, + items=items, + queryset=self.queryset, + **options ) - def build_search_widget_attrs(self, chosen_resource, bundle, edit, queryset): - resources = {} - for res in queryset: - displayable = {} - displayable['small_name'] = res.name - if res.owner: - displayable['expanded_name'] = res.owner.username - else: - displayable['expanded_name'] = "" - displayable['string'] = res.description - displayable['id'] = res.id - resources[res.id] = displayable - - attrs = { - 'set': resources, - 'show_from_noentry': "true", + def get_validated_bundle(self): + bundles = self.cleaned_data['searchable_select'] + if len(bundles) < 1: # don't need to check for >1, as field does that for us + raise ValidationError("No bundle was selected") + return bundles[0] + + def generate_items(self, queryset): + raise Exception("SearchableSelectAbstractForm does not implement concrete generate_items()") + + def generate_options(self, disabled=False): + return { + 'show_from_noentry': True, 'show_x_results': -1, - 'scrollable': "true", + 'results_scrollable': True, 'selectable_limit': 1, - 'name': "generic_resource_bundle", - 'placeholder': "resource", - 'initial': chosen_resource, - 'edit': edit, - 'wf_type': 1 + 'placeholder': 'Search for a Bundle', + 'name': 'searchable_select', + 'disabled': False } - return attrs -class SWConfigSelectorForm(forms.Form): +class SWConfigSelectorForm(SearchableSelectAbstractForm): + def generate_items(self, queryset): + items = {} - def __init__(self, *args, **kwargs): - chosen_software = "" - bundle = None - edit = False - resource = None - user = None - if "chosen_software" in kwargs: - chosen_software = kwargs.pop("chosen_software") - - if "bundle" in kwargs: - bundle = kwargs.pop("bundle") - if "edit" in kwargs: - edit = kwargs.pop("edit") - if "resource" in kwargs: - resource = kwargs.pop("resource") - if "user" in kwargs: - user = kwargs.pop("user") - super(SWConfigSelectorForm, self).__init__(*args, **kwargs) - attrs = self.build_search_widget_attrs(chosen_software, bundle, edit, resource, user) - self.fields['software_bundle'] = forms.CharField( - widget=SearchableSelectMultipleWidget(attrs=attrs) - ) + for bundle in queryset: + items[bundle.id] = { + 'small_name': bundle.name, + 'expanded_name': bundle.owner.username, + 'string': bundle.description, + 'id': bundle.id + } - def build_search_widget_attrs(self, chosen, bundle, edit, resource, user): - configs = {} - queryset = ConfigBundle.objects.select_related('owner').all() - if resource: - if user is None: - user = resource.owner - queryset = queryset.filter(bundle=resource) + return items - if user: - queryset = queryset.filter(owner=user) + +class OPNFVSelectForm(SearchableSelectAbstractForm): + def generate_items(self, queryset): + items = {} for config in queryset: - displayable = {} - displayable['small_name'] = config.name - displayable['expanded_name'] = config.owner.username - displayable['string'] = config.description - displayable['id'] = config.id - configs[config.id] = displayable - - incompatible_choice = "false" - if bundle and bundle.id not in configs: - displayable = {} - displayable['small_name'] = bundle.name - displayable['expanded_name'] = bundle.owner.username - displayable['string'] = bundle.description - displayable['id'] = bundle.id - configs[bundle.id] = displayable - incompatible_choice = "true" - - attrs = { - 'set': configs, - 'show_from_noentry': "true", - 'show_x_results': -1, - 'scrollable': "true", - 'selectable_limit': 1, - 'name': "software_bundle", - 'placeholder': "config", - 'initial': chosen, - 'edit': edit, - 'wf_type': 2, - 'incompatible': incompatible_choice - } - return attrs + items[config.id] = { + 'small_name': config.name, + 'expanded_name': config.bundle.owner.username, + 'string': config.description, + 'id': config.id + } + + return items + + +class ResourceSelectorForm(SearchableSelectAbstractForm): + def generate_items(self, queryset): + items = {} + + for bundle in queryset: + items[bundle.id] = { + 'small_name': bundle.name, + 'expanded_name': bundle.owner.username, + 'string': bundle.description, + 'id': bundle.id + } + + return items class BookingMetaForm(forms.Form): @@ -202,66 +227,18 @@ class BookingMetaForm(forms.Form): purpose = forms.CharField(max_length=1000) project = forms.CharField(max_length=400) info_file = forms.CharField(max_length=1000, required=False) + deploy_opnfv = forms.BooleanField(required=False) - def __init__(self, data=None, *args, **kwargs): - chosen_users = [] - if "default_user" in kwargs: - default_user = kwargs.pop("default_user") - else: - default_user = "you" - self.default_user = default_user - if "chosen_users" in kwargs: - chosen_users = kwargs.pop("chosen_users") - elif data and "users" in data: - chosen_users = data.getlist("users") - else: - pass - - super(BookingMetaForm, self).__init__(data=data, **kwargs) - - self.fields['users'] = forms.CharField( - widget=SearchableSelectMultipleWidget( - attrs=self.build_search_widget_attrs(chosen_users, default_user=default_user) - ), - required=False - ) - - def build_user_list(self): - """ - returns a mapping of UserProfile ids to displayable objects expected by - searchable multiple select widget - """ - try: - users = {} - d_qset = UserProfile.objects.select_related('user').all().exclude(user__username=self.default_user) - for userprofile in d_qset: - user = { - 'id': userprofile.user.id, - 'expanded_name': userprofile.full_name, - 'small_name': userprofile.user.username, - 'string': userprofile.email_addr - } - - users[userprofile.user.id] = user - - return users - except Exception: - pass - - def build_search_widget_attrs(self, chosen_users, default_user="you"): + def __init__(self, *args, user_initial=[], owner=None, **kwargs): + super(BookingMetaForm, self).__init__(**kwargs) - 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 + self.fields['users'] = SearchableSelectMultipleField( + queryset=UserProfile.objects.select_related('user').exclude(user=owner), + initial=user_initial, + items=get_user_items(exclude=owner), + required=False, + **get_user_field_opts() + ) class MultipleSelectFilterWidget(forms.Widget): @@ -298,7 +275,7 @@ class MultipleSelectFilterField(forms.Field): # Form. # initial -- A value to use in this Field's initial display. This value # is *not* used as a fallback if data isn't given. - # help_text -- An optional string to use as "help text" for this Field. + # help_text -- An optional string to use as "help; text" for this Field. # error_messages -- An optional dictionary to override the default # messages that the field will raise. # show_hidden_initial -- Boolean that specifies if it is needed to render a diff --git a/src/workflow/models.py b/src/workflow/models.py index 25d7e84..43a9bf2 100644 --- a/src/workflow/models.py +++ b/src/workflow/models.py @@ -240,6 +240,67 @@ class WorkflowStep(object): return self.repo.put(key, value, self.id) +""" +subclassing notes: + subclasses have to define the following class attributes: + self.select_repo_key: where the selected "object" or "bundle" is to be placed in the repo + self.form: the form to be used + alert_bundle_missing(): what message to display if a user does not select/selects an invalid object + get_form_queryset(): generate a queryset to be used to filter available items for the field + get_page_context(): return simple context such as page header and other info +""" + + +class AbstractSelectOrCreate(WorkflowStep): + template = 'dashboard/genericselect.html' + title = "Select a Bundle" + short_title = "select" + description = "Generic bundle selector step" + + select_repo_key = None + form = None # subclasses are expected to use a form that is a subclass of SearchableSelectGenericForm + + def alert_bundle_missing(self): # override in subclasses to change message if field isn't filled out + self.set_invalid("Please select a valid bundle") + + def post_render(self, request): + context = self.get_context() + form = self.form(request.POST, queryset=self.get_form_queryset()) + if form.is_valid(): + bundle = form.get_validated_bundle() + if not bundle: + self.alert_bundle_missing() + return render(request, self.template, context) + self.repo_put(self.select_repo_key, bundle) + self.put_confirm_info(bundle) + self.set_valid("Step Completed") + else: + self.alert_bundle_missing() + messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True) + + return self.render(request) + + def get_context(self): + default = [] + + bundle = self.repo_get(self.select_repo_key, False) + if bundle: + default.append(bundle) + + form = self.form(queryset=self.get_form_queryset(), initial=default) + + context = {'form': form, **self.get_page_context()} + context.update(super().get_context()) + + return context + + def get_page_context(): + return { + 'select_type': 'generic', + 'select_type_title': 'Generic Bundle' + } + + class Confirmation_Step(WorkflowStep): template = 'workflow/confirm.html' title = "Confirm Changes" @@ -335,6 +396,7 @@ class Repository(): self.el[key] = value def get(self, key, default, id): + self.add_get_history(key, id) return self.el.get(key, default) @@ -359,6 +421,7 @@ class Repository(): errors = self.make_snapshot() if errors: return errors + # if GRB WF, create it if self.GRESOURCE_BUNDLE_MODELS in self.el: errors = self.make_generic_resource_bundle() @@ -499,7 +562,7 @@ class Repository(): models = self.el[self.CONFIG_MODELS] if 'bundle' in models: bundle = models['bundle'] - bundle.bundle = bundle.bundle + bundle.bundle = self.el[self.SELECTED_GRESOURCE_BUNDLE] try: bundle.save() except Exception as e: @@ -537,15 +600,22 @@ class Repository(): models = self.el[self.BOOKING_MODELS] owner = self.el[self.SESSION_USER] + if 'booking' in models: + booking = models['booking'] + else: + return "BOOK, no booking model exists. CODE:0x000f" + + selected_grb = None + if self.SELECTED_GRESOURCE_BUNDLE in self.el: selected_grb = self.el[self.SELECTED_GRESOURCE_BUNDLE] else: return "BOOK, no selected resource. CODE:0x000e" - if 'booking' in models: - booking = models['booking'] - else: - return "BOOK, no booking model exists. CODE:0x000f" + if self.SELECTED_CONFIG_BUNDLE not in self.el: + return "BOOK, no selected config bundle. CODE:0x001f" + + booking.config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE] if not booking.start: return "BOOK, booking has no start. CODE:0x0010" @@ -567,7 +637,6 @@ class Repository(): booking.resource = resource_bundle booking.owner = owner - booking.config_bundle = booking.config_bundle booking.lab = selected_grb.lab is_allowed = BookingAuthManager().booking_allowed(booking, self) @@ -600,7 +669,7 @@ class Repository(): def make_opnfv_config(self): opnfv_models = self.el[self.OPNFV_MODELS] - config_bundle = opnfv_models['configbundle'] + config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE] if not config_bundle: return "No Configuration bundle selected" info = opnfv_models.get("meta", {}) diff --git a/src/workflow/opnfv_workflow.py b/src/workflow/opnfv_workflow.py index 490d2f0..7d499ec 100644 --- a/src/workflow/opnfv_workflow.py +++ b/src/workflow/opnfv_workflow.py @@ -9,66 +9,39 @@ from django.forms import formset_factory -from django.contrib import messages -import json - -from workflow.models import WorkflowStep +from workflow.models import WorkflowStep, AbstractSelectOrCreate from resource_inventory.models import ConfigBundle, OPNFV_SETTINGS from workflow.forms import OPNFVSelectionForm, OPNFVNetworkRoleForm, OPNFVHostRoleForm, SWConfigSelectorForm, BasicMetaForm -class OPNFV_Resource_Select(WorkflowStep): - template = 'booking/steps/swconfig_select.html' +class OPNFV_Resource_Select(AbstractSelectOrCreate): title = "Select Software Configuration" - description = "Choose the software and related configurations you want to use to configure OPNFV" - short_title = "software configuration" - modified_key = "configbundle_step" + description = "Choose the software bundle you wish to use as a base for your OPNFV configuration" + short_title = "software config" + form = SWConfigSelectorForm - def update_confirmation(self): - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - config_bundle = self.repo_get(self.repo.OPNFV_MODELS, {}).get("configbundle") - if not config_bundle: - return - confirm['software bundle'] = config_bundle.name - confirm['hardware POD'] = config_bundle.bundle.name - self.repo_put(self.repo.CONFIRMATION, confirm) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE - def post_render(self, request): - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - form = SWConfigSelectorForm(request.POST) - if form.is_valid(): - bundle_json = form.cleaned_data['software_bundle'] - bundle_json = bundle_json[2:-2] # Stupid django string bug - if not bundle_json: - self.set_invalid("Please select a valid config") - return self.render(request) - bundle_json = json.loads(bundle_json) - if len(bundle_json) < 1: - self.set_invalid("Please select a valid config") - return self.render(request) - bundle = None - id = int(bundle_json[0]['id']) - bundle = ConfigBundle.objects.get(id=id) - - models['configbundle'] = bundle - self.repo_put(self.repo.OPNFV_MODELS, models) - self.set_valid("Step Completed") - messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True) - self.update_confirmation() - else: - self.set_invalid("Please select or create a valid config") - messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True) - - return self.render(request) - - def get_context(self): - context = super(OPNFV_Resource_Select, self).get_context() - default = [] + def get_form_queryset(self): user = self.repo_get(self.repo.SESSION_USER) - - context['form'] = SWConfigSelectorForm(chosen_software=default, bundle=None, edit=True, resource=None, user=user) - return context + qs = ConfigBundle.objects.filter(owner=user) + return qs + + def put_confirm_info(self, bundle): + confirm_dict = self.repo_get(self.repo.CONFIRMATION) + confirm_dict['software bundle'] = bundle.name + confirm_dict['hardware POD'] = bundle.bundle.name + self.repo_put(self.repo.CONFIRMATION, confirm_dict) + + def get_page_context(self): + return { + 'select_type': 'swconfig', + 'select_type_title': 'Software Config', + 'addable_type_num': 2 + } class Pick_Installer(WorkflowStep): @@ -92,7 +65,7 @@ class Pick_Installer(WorkflowStep): def get_context(self): context = super(Pick_Installer, self).get_context() - models = self.repo_get(self.repo.OPNFV_MODELS, None) + models = self.repo_get(self.repo.OPNFV_MODELS, {}) initial = { "installer": models.get("installer_chosen"), "scenario": models.get("scenario_chosen") @@ -155,7 +128,7 @@ class Assign_Network_Roles(WorkflowStep): def get_context(self): context = super(Assign_Network_Roles, self).get_context() - config_bundle = self.repo_get(self.repo.OPNFV_MODELS, {}).get("configbundle") + config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) if config_bundle is None: context["unavailable"] = True return context @@ -179,7 +152,7 @@ class Assign_Network_Roles(WorkflowStep): def post_render(self, request): models = self.repo_get(self.repo.OPNFV_MODELS, {}) - config_bundle = models.get("configbundle") + config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) roles = OPNFV_SETTINGS.NETWORK_ROLES net_role_formset = self.create_netformset(roles, config_bundle, data=request.POST) if net_role_formset.is_valid(): @@ -228,8 +201,7 @@ class Assign_Host_Roles(WorkflowStep): # taken verbatim from Define_Software in def get_context(self): context = super(Assign_Host_Roles, self).get_context() - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - config = models.get("configbundle") + config = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) if config is None: context['error'] = "Please select a Configuration on the first step" diff --git a/src/workflow/sw_bundle_workflow.py b/src/workflow/sw_bundle_workflow.py index 329b716..0c558fc 100644 --- a/src/workflow/sw_bundle_workflow.py +++ b/src/workflow/sw_bundle_workflow.py @@ -12,25 +12,12 @@ from django.forms import formset_factory from workflow.models import WorkflowStep from workflow.forms import BasicMetaForm, HostSoftwareDefinitionForm -from workflow.booking_workflow import Resource_Select +from workflow.booking_workflow import Abstract_Resource_Select from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration -# resource selection step is reused from Booking workflow -class SWConf_Resource_Select(Resource_Select): - def __init__(self, *args, **kwargs): - super(SWConf_Resource_Select, self).__init__(*args, **kwargs) - self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE - self.confirm_key = "configuration" - - def post_render(self, request): - response = super(SWConf_Resource_Select, self).post_render(request) - models = self.repo_get(self.repo.CONFIG_MODELS, {}) - bundle = models.get("bundle", ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER))) - bundle.bundle = self.repo_get(self.repo_key) # super put grb here - models['bundle'] = bundle - self.repo_put(self.repo.CONFIG_MODELS, models) - return response +class SWConf_Resource_Select(Abstract_Resource_Select): + workflow_type = "configuration" class Define_Software(WorkflowStep): diff --git a/src/workflow/urls.py b/src/workflow/urls.py index b131d84..5a97904 100644 --- a/src/workflow/urls.py +++ b/src/workflow/urls.py @@ -14,7 +14,7 @@ from django.conf import settings from workflow.views import step_view, delete_session, manager_view, viewport_view from workflow.models import Repository from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info -from workflow.booking_workflow import SWConfig_Select, Resource_Select, Booking_Meta +from workflow.booking_workflow import SWConfig_Select, Booking_Resource_Select, Booking_Meta app_name = 'workflow' urlpatterns = [ @@ -31,4 +31,4 @@ if settings.TESTING: urlpatterns.append(url(r'^workflow/step/resource_meta$', Resource_Meta_Info("", Repository()).test_render)) urlpatterns.append(url(r'^workflow/step/booking_meta$', Booking_Meta("", Repository()).test_render)) urlpatterns.append(url(r'^workflow/step/software_select$', SWConfig_Select("", Repository()).test_render)) - urlpatterns.append(url(r'^workflow/step/resource_select$', Resource_Select("", Repository()).test_render)) + urlpatterns.append(url(r'^workflow/step/resource_select$', Booking_Resource_Select("", Repository()).test_render)) diff --git a/src/workflow/workflow_factory.py b/src/workflow/workflow_factory.py index 08cf296..03c8126 100644 --- a/src/workflow/workflow_factory.py +++ b/src/workflow/workflow_factory.py @@ -8,7 +8,7 @@ ############################################################################## -from workflow.booking_workflow import Booking_Resource_Select, SWConfig_Select, Booking_Meta +from workflow.booking_workflow import Booking_Resource_Select, SWConfig_Select, Booking_Meta, OPNFV_Select from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info from workflow.sw_bundle_workflow import Config_Software, Define_Software, SWConf_Resource_Select from workflow.snapshot_workflow import Select_Host_Step, Image_Meta_Step @@ -76,6 +76,7 @@ class WorkflowFactory(): Booking_Resource_Select, SWConfig_Select, Booking_Meta, + OPNFV_Select, ] resource_steps = [ |