diff options
author | Sawyer Bergeron <sbergeron@iol.unh.edu> | 2020-05-15 14:58:37 -0400 |
---|---|---|
committer | Sawyer Bergeron <sbergeron@iol.unh.edu> | 2020-05-15 17:42:23 -0400 |
commit | 530271c247a4ce538e3aa69fd3893481fada44ab (patch) | |
tree | 48640138c825bf0906a1c261c28939d5311ae6d6 | |
parent | 6bf37e9864787e0398a1d2e1cdd10b40a8ebc6e6 (diff) |
Merge resource branch
This pulls master up to date to include
changes to models and surrounding infra that allow
for multi-node templates and merging of pods
Squashed commit of the following:
commit abc8f27d9c6b05fb3afcb9b00dc35c0f2232d1a6
Author: Sawyer Bergeron <sawyerbergeron@gmail.com>
Date: Thu Apr 2 14:05:26 2020 -0400
Start fixing workflow for model changes
Change-Id: I79df975ef45abf2e6e69594d358bbd205938828f
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.com>
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
commit 7a7e2182acd0ea94e19aba4926c3a12771b30a6d
Author: sms1097 <ssmith@iol.unh.edu>
Date: Tue Mar 31 15:13:06 2020 -0400
Working on workflow refactoring
Change-Id: I4141b6aca98aff7bff9cb78a7d5594e25eb45e98
Signed-off-by: Sean Smith <ssmith@iol.unh.edu>
commit c09050ae2814f07af58557b40f9ed3559063d2c7
Merge: 71438d9 b5ccdc4
Author: Parker Berberian <pberberian@iol.unh.edu>
Date: Tue Mar 24 20:34:16 2020 +0000
Merge "Able to delete configurations and view lab details" into resource
commit b5ccdc4ffbb883c20f2f6f69aeef5002aef5db53
Author: sms1097 <ssmith@iol.unh.edu>
Date: Thu Mar 19 17:08:12 2020 -0400
Able to delete configurations and view lab details
Change-Id: Ib15c86d84f4cc7e7745551889ce91c89b5de46e2
Signed-off-by: Sean Smith <ssmith@iol.unh.edu>
Change-Id: Id6748c6bea67773a861921394d88579730246598
commit 71438d9a35cdb316cece865c9d410aeffb0053d8
Merge: 5460d0d a758223
Author: Parker Berberian <pberberian@iol.unh.edu>
Date: Thu Mar 19 18:51:09 2020 +0000
Merge "Add / Fix tests for refactor" into resource
commit 5460d0d447b075433a763f9bfa33448b88ec8393
Merge: a9063a3 f55d839
Author: Parker Berberian <pberberian@iol.unh.edu>
Date: Wed Mar 18 15:59:37 2020 +0000
Merge "Fixed the quick booking form resource template filtering. Added some more models to the admin page." into resource
commit f55d839a029ab1f5ab1273872e71a97fa1d5108b
Author: Adam Hassick <ahassick@iol.unh.edu>
Date: Tue Mar 17 11:35:40 2020 -0400
Fixed the quick booking form resource template filtering. Added some more models to the admin page.
Signed-off-by: Adam Hassick <ahassick@iol.unh.edu>
Change-Id: I2d2e7aeb96b10c231804a62f37a476039c954b7b
commit a9063a347c4ebef0e53a17f198468bb135772810
Author: Parker Berberian <pberberian@iol.unh.edu>
Date: Wed Mar 18 10:29:51 2020 -0400
Fixes Some Issues with Quick Booking Seen in the Akraino lab
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
Change-Id: I2a1e843fbaa7984225f2f80742dad59dc348fbf2
commit a758223f44c6fec595b055d7c9b232b00e9174a0
Author: Parker Berberian <pberberian@iol.unh.edu>
Date: Tue Mar 17 11:07:32 2020 -0400
Add / Fix tests for refactor
Change-Id: I0526d1942f87707082a4eb1c8c98910f84481c23
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
Author: Parker Berberian <pberberian@iol.unh.edu>
Add "Pod" Column to booking list
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
Change-Id: I270913283bf1e5815cadf622ba2fd5f98bb61675
Author: Parker Berberian <pberberian@iol.unh.edu>
Fixes that make the Akraino dashboard work
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
Change-Id: I81746473a4511ef7d46445a7b16809a6e9da100f
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
Change-Id: I4b428e7c8a8d401d7bae95cba01077feb0332a7f
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
33 files changed, 750 insertions, 599 deletions
diff --git a/src/account/views.py b/src/account/views.py index a8bb02b..d1cc813 100644 --- a/src/account/views.py +++ b/src/account/views.py @@ -29,6 +29,7 @@ from django.shortcuts import render from jira import JIRA from rest_framework.authtoken.models import Token + from account.forms import AccountSettingsForm from account.jira_util import SignatureMethod_RSA_SHA1 from account.models import UserProfile @@ -177,20 +178,15 @@ def account_resource_view(request): if not request.user.is_authenticated: return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) template = "account/resource_list.html" - resources = ResourceTemplate.objects.filter( - owner=request.user).prefetch_related("configbundle_set") - mapping = {} - resource_list = [] - booking_mapping = {} - for grb in resources: - resource_list.append(grb) - mapping[grb.id] = [{"id": x.id, "name": x.name} for x in grb.configbundle_set.all()] - if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists(): - booking_mapping[grb.id] = "true" + + active_bundles = [book.resource for book in Booking.objects.filter( + owner=request.user, end__gte=timezone.now())] + active_resources = [bundle.template.id for bundle in active_bundles] + resource_list = list(ResourceTemplate.objects.filter(owner=request.user)) + context = { "resources": resource_list, - "grb_mapping": mapping, - "booking_mapping": booking_mapping, + "active_resources": active_resources, "title": "My Resources" } return render(request, template, context=context) @@ -260,7 +256,7 @@ def configuration_delete_view(request, config_id=None): config = get_object_or_404(ResourceTemplate, pk=config_id) if not request.user.id == config.owner.id: return HttpResponse('no') # 403? - if Booking.objects.filter(config_bundle=config, end__gt=timezone.now()).exists(): + if Booking.objects.filter(resource__template=config, end__gt=timezone.now()).exists(): return HttpResponse('no') config.delete() return HttpResponse('') diff --git a/src/api/models.py b/src/api/models.py index e41a44d..addc02d 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -979,7 +979,7 @@ class JobFactory(object): relation.config = relation.config relation.save() - hardware_config.set("image", "hostname", "power", "ipmi_create") + hardware_config.set("id", "image", "hostname", "power", "ipmi_create") hardware_config.save() @classmethod diff --git a/src/booking/forms.py b/src/booking/forms.py index b9c9231..886f0f6 100644 --- a/src/booking/forms.py +++ b/src/booking/forms.py @@ -11,8 +11,7 @@ from django.forms.widgets import NumberInput from workflow.forms import ( MultipleSelectFilterField, - MultipleSelectFilterWidget, - FormUtils) + MultipleSelectFilterWidget) from account.models import UserProfile from resource_inventory.models import Image, Installer, Scenario from workflow.forms import SearchableSelectMultipleField @@ -27,7 +26,7 @@ class QuickBookingForm(forms.Form): installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) - def __init__(self, data=None, user=None, *args, **kwargs): + def __init__(self, data=None, user=None, lab_data=None, *args, **kwargs): if "default_user" in kwargs: default_user = kwargs.pop("default_user") else: @@ -47,8 +46,6 @@ class QuickBookingForm(forms.Form): **get_user_field_opts() ) - attrs = FormUtils.getLabData() - self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**attrs)) self.fields['length'] = forms.IntegerField( widget=NumberInput( attrs={ @@ -60,6 +57,8 @@ class QuickBookingForm(forms.Form): ) ) + self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**lab_data)) + def build_user_list(self): """ Build list of UserProfiles. diff --git a/src/booking/quick_deployer.py b/src/booking/quick_deployer.py index 951ff47..9cfc465 100644 --- a/src/booking/quick_deployer.py +++ b/src/booking/quick_deployer.py @@ -79,13 +79,14 @@ def update_template(old_template, image, hostname, user): lab=old_template.lab, description=old_template.description, public=False, - temporary=True + temporary=True, + copy_of=old_template ) for old_network in old_template.networks.all(): Network.objects.create( name=old_network.name, - bundle=old_template, + bundle=template, is_public=False ) # We are assuming there is only one opnfv config per public resource template @@ -105,7 +106,8 @@ def update_template(old_template, image, hostname, user): config = ResourceConfiguration.objects.create( profile=old_config.profile, image=image, - template=template + template=template, + is_head_node=old_config.is_head_node ) for old_iface_config in old_config.interface_configs.all(): @@ -127,6 +129,7 @@ def update_template(old_template, image, hostname, user): resource_config=config, opnfv_config=opnfv_config ) + return template def generate_opnfvconfig(scenario, installer, template): @@ -165,7 +168,6 @@ def check_invariants(request, **kwargs): image = kwargs['image'] scenario = kwargs['scenario'] lab = kwargs['lab'] - resource_template = kwargs['resource_template'] length = kwargs['length'] # check that image os is compatible with installer if installer in image.os.sup_installers.all(): @@ -176,8 +178,8 @@ def check_invariants(request, **kwargs): raise ValidationError("The chosen installer does not support the chosen scenario") if image.from_lab != lab: raise ValidationError("The chosen image is not available at the chosen hosting lab") - #TODO - #if image.host_type != host_profile: + # TODO + # if image.host_type != host_profile: # raise ValidationError("The chosen image is not available for the chosen host type") if not image.public and image.owner != request.user: raise ValidationError("You are not the owner of the chosen private image") @@ -217,11 +219,12 @@ def create_from_form(form, request): ResourceManager.getInstance().templateIsReservable(resource_template) - hconf = update_template(resource_template, image, hostname, request.user) + resource_template = update_template(resource_template, image, hostname, request.user) # if no installer provided, just create blank host opnfv_config = None if installer: + hconf = resource_template.getConfigs()[0] opnfv_config = generate_opnfvconfig(scenario, installer, resource_template) generate_hostopnfv(hconf, opnfv_config) diff --git a/src/booking/tests/test_models.py b/src/booking/tests/test_models.py index c8c8ea8..37eb655 100644 --- a/src/booking/tests/test_models.py +++ b/src/booking/tests/test_models.py @@ -11,13 +11,12 @@ from datetime import timedelta -from django.contrib.auth.models import Permission, User +from django.contrib.auth.models import User from django.test import TestCase from django.utils import timezone -# from booking.models import * from booking.models import Booking -from resource_inventory.models import ResourceBundle, GenericResourceBundle, ConfigBundle +from dashboard.testing_utils import make_resource_template, make_user class BookingModelTestCase(TestCase): @@ -27,8 +26,6 @@ class BookingModelTestCase(TestCase): Creates all the scafolding needed and tests the Booking model """ - count = 0 - def setUp(self): """ Prepare for Booking model tests. @@ -36,29 +33,9 @@ class BookingModelTestCase(TestCase): Creates all the needed models, such as users, resources, and configurations """ self.owner = User.objects.create(username='owner') - - self.res1 = ResourceBundle.objects.create( - template=GenericResourceBundle.objects.create( - name="gbundle" + str(self.count) - ) - ) - self.count += 1 - self.res2 = ResourceBundle.objects.create( - template=GenericResourceBundle.objects.create( - name="gbundle2" + str(self.count) - ) - ) - self.count += 1 - self.user1 = User.objects.create(username='user1') - - self.add_booking_perm = Permission.objects.get(codename='add_booking') - self.user1.user_permissions.add(self.add_booking_perm) - - self.user1 = User.objects.get(pk=self.user1.id) - self.config_bundle = ConfigBundle.objects.create( - owner=self.user1, - name="test config" - ) + self.res1 = make_resource_template(name="Test template 1") + self.res2 = make_resource_template(name="Test template 2") + self.user1 = make_user(username='user1') def test_start_end(self): """ @@ -76,7 +53,6 @@ class BookingModelTestCase(TestCase): end=end, resource=self.res1, owner=self.user1, - config_bundle=self.config_bundle ) end = start self.assertRaises( @@ -86,7 +62,6 @@ class BookingModelTestCase(TestCase): end=end, resource=self.res1, owner=self.user1, - config_bundle=self.config_bundle ) def test_conflicts(self): @@ -105,7 +80,6 @@ class BookingModelTestCase(TestCase): end=end, owner=self.user1, resource=self.res1, - config_bundle=self.config_bundle ) ) @@ -116,7 +90,6 @@ class BookingModelTestCase(TestCase): end=end, resource=self.res1, owner=self.user1, - config_bundle=self.config_bundle ) self.assertRaises( @@ -126,7 +99,6 @@ class BookingModelTestCase(TestCase): end=end - timedelta(days=1), resource=self.res1, owner=self.user1, - config_bundle=self.config_bundle ) self.assertRaises( @@ -136,7 +108,6 @@ class BookingModelTestCase(TestCase): end=end, resource=self.res1, owner=self.user1, - config_bundle=self.config_bundle ) self.assertRaises( @@ -146,7 +117,6 @@ class BookingModelTestCase(TestCase): end=end - timedelta(days=1), resource=self.res1, owner=self.user1, - config_bundle=self.config_bundle ) self.assertRaises( @@ -156,7 +126,6 @@ class BookingModelTestCase(TestCase): end=end + timedelta(days=1), resource=self.res1, owner=self.user1, - config_bundle=self.config_bundle ) self.assertRaises( @@ -166,7 +135,6 @@ class BookingModelTestCase(TestCase): end=end + timedelta(days=1), resource=self.res1, owner=self.user1, - config_bundle=self.config_bundle ) self.assertTrue( @@ -175,7 +143,6 @@ class BookingModelTestCase(TestCase): end=start, owner=self.user1, resource=self.res1, - config_bundle=self.config_bundle ) ) @@ -185,7 +152,6 @@ class BookingModelTestCase(TestCase): end=end + timedelta(days=1), owner=self.user1, resource=self.res1, - config_bundle=self.config_bundle ) ) @@ -195,7 +161,6 @@ class BookingModelTestCase(TestCase): end=start - timedelta(days=1), owner=self.user1, resource=self.res1, - config_bundle=self.config_bundle ) ) @@ -205,7 +170,6 @@ class BookingModelTestCase(TestCase): end=end + timedelta(days=2), owner=self.user1, resource=self.res1, - config_bundle=self.config_bundle ) ) @@ -215,7 +179,6 @@ class BookingModelTestCase(TestCase): end=end, owner=self.user1, resource=self.res2, - config_bundle=self.config_bundle ) ) @@ -234,7 +197,6 @@ class BookingModelTestCase(TestCase): end=end, owner=self.user1, resource=self.res1, - config_bundle=self.config_bundle ) ) diff --git a/src/booking/tests/test_quick_booking.py b/src/booking/tests/test_quick_booking.py index 5ba1744..f405047 100644 --- a/src/booking/tests/test_quick_booking.py +++ b/src/booking/tests/test_quick_booking.py @@ -14,17 +14,15 @@ from django.test import TestCase, Client from booking.models import Booking from dashboard.testing_utils import ( - make_host, make_user, make_user_profile, make_lab, - make_installer, make_image, - make_scenario, make_os, - make_complete_host_profile, make_opnfv_role, make_public_net, + make_resource_template, + make_server ) @@ -36,15 +34,13 @@ class QuickBookingValidFormTestCase(TestCase): cls.user.save() make_user_profile(cls.user, True) - lab_user = make_user(True) - cls.lab = make_lab(lab_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) - cls.host = make_host(cls.host_profile, cls.lab) + cls.res_template = make_resource_template(owner=cls.user, lab=cls.lab) + cls.res_profile = cls.res_template.getConfigs()[0].profile + os = make_os() + cls.image = make_image(cls.res_profile, lab=cls.lab, owner=cls.user, os=os) + cls.server = make_server(cls.res_profile, cls.lab) cls.role = make_opnfv_role() cls.pubnet = make_public_net(10, cls.lab) @@ -55,10 +51,10 @@ class QuickBookingValidFormTestCase(TestCase): def build_post_data(cls): return { 'filter_field': json.dumps({ - "host": { - "host_" + str(cls.host_profile.id): { + "resource": { + "resource_" + str(cls.res_profile.id): { "selected": True, - "id": cls.host_profile.id + "id": cls.res_template.id } }, "lab": { @@ -75,8 +71,6 @@ class QuickBookingValidFormTestCase(TestCase): 'users': '', 'hostname': 'my_host', 'image': str(cls.image.id), - 'installer': str(cls.installer.id), - 'scenario': str(cls.scenario.id) } def post(self, changed_fields={}): @@ -97,15 +91,10 @@ class QuickBookingValidFormTestCase(TestCase): self.assertLess(delta, datetime.timedelta(minutes=1)) resource_bundle = booking.resource - config_bundle = booking.config_bundle - opnfv_config = config_bundle.opnfv_config.first() - self.assertEqual(self.installer, opnfv_config.installer) - self.assertEqual(self.scenario, opnfv_config.scenario) - - host = resource_bundle.hosts.first() - self.assertEqual(host.profile, self.host_profile) - self.assertEqual(host.template.resource.name, 'my_host') + host = resource_bundle.get_resources()[0] + self.assertEqual(host.profile, self.res_profile) + self.assertEqual(host.name, 'my_host') def test_with_too_long_length(self): response = self.post({'length': '22'}) @@ -133,10 +122,10 @@ class QuickBookingValidFormTestCase(TestCase): def test_with_invalid_host_id(self): response = self.post({'filter_field': json.dumps({ - "host": { - "host_" + str(self.host_profile.id + 100): { + "resource": { + "resource_" + str(self.res_profile.id + 100): { "selected": True, - "id": self.host_profile.id + 100 + "id": self.res_profile.id + 100 } }, "lab": { @@ -151,12 +140,11 @@ class QuickBookingValidFormTestCase(TestCase): 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"}]}'}) response = self.post({'filter_field': json.dumps({ - "host": { - "host_" + str(self.host_profile.id): { + "resource": { + "resource_" + str(self.res_profile.id): { "selected": True, - "id": self.host_profile.id + "id": self.res_profile.id } }, "lab": { diff --git a/src/booking/views.py b/src/booking/views.py index daaf026..3c95e07 100644 --- a/src/booking/views.py +++ b/src/booking/views.py @@ -19,11 +19,11 @@ from django.db.models import Q from django.urls import reverse from resource_inventory.models import ResourceBundle, ResourceProfile, Image, ResourceQuery -from resource_inventory.resource_manager import ResourceManager -from account.models import Lab, Downtime +from account.models import Downtime from booking.models import Booking from booking.stats import StatisticsManager from booking.forms import HostReImageForm +from workflow.forms import FormUtils from api.models import JobFactory from workflow.views import login from booking.forms import QuickBookingForm @@ -40,21 +40,16 @@ def quick_create(request): if request.method == 'GET': context = {} - - r_manager = ResourceManager.getInstance() - templates = {} - for lab in Lab.objects.all(): - templates[str(lab)] = r_manager.getAvailableResourceTemplates(lab, request.user) - - context['lab_profile_map'] = templates - - context['form'] = QuickBookingForm(default_user=request.user.username, user=request.user) - + attrs = FormUtils.getLabData(user=request.user) + context['form'] = QuickBookingForm(lab_data=attrs, default_user=request.user.username, user=request.user) + context['lab_profile_map'] = {} context.update(drop_filter(request.user)) - return render(request, 'booking/quick_deploy.html', context) + if request.method == 'POST': - form = QuickBookingForm(request.POST, user=request.user) + attrs = FormUtils.getLabData(user=request.user) + form = QuickBookingForm(request.POST, lab_data=attrs, user=request.user) + context = {} context['lab_profile_map'] = {} context['form'] = form diff --git a/src/dashboard/testing_utils.py b/src/dashboard/testing_utils.py index b7272ea..d7a346e 100644 --- a/src/dashboard/testing_utils.py +++ b/src/dashboard/testing_utils.py @@ -178,6 +178,9 @@ def make_vlan_manager(vlans=None, block_size=20, allow_overlapping=False, reserv def make_lab(user=None, name="Test_Lab_Instance", status=LabStatus.UP, vlan_manager=None, pub_net_count=5): + if Lab.objects.filter(name=name).exists(): + return Lab.objects.get(name=name) + if not vlan_manager: vlan_manager = make_vlan_manager() @@ -207,6 +210,9 @@ resource_inventory instantiation section for permanent resources def make_resource_profile(lab, name="test_hostprofile"): + if ResourceProfile.objects.filter(name=name).exists(): + return ResourceProfile.objects.get(name=name) + resource_profile = ResourceProfile.objects.create( name=name, description='test resourceprofile instance' diff --git a/src/dashboard/views.py b/src/dashboard/views.py index 498bd9d..2ace2d4 100644 --- a/src/dashboard/views.py +++ b/src/dashboard/views.py @@ -15,7 +15,7 @@ from django.shortcuts import render from account.models import Lab -from resource_inventory.models import Image, ResourceProfile +from resource_inventory.models import Image, ResourceProfile, ResourceQuery from workflow.workflow_manager import ManagerTracker @@ -37,14 +37,17 @@ def lab_detail_view(request, lab_name): if user: images = images | Image.objects.filter(from_lab=lab).filter(owner=user) + hosts = ResourceQuery.filter(lab=lab) + return render( request, "dashboard/lab_detail.html", { 'title': "Lab Overview", 'lab': lab, - 'hostprofiles': lab.hostprofiles.all(), + 'hostprofiles': ResourceProfile.objects.filter(labs=lab), 'images': images, + 'hosts': hosts } ) diff --git a/src/resource_inventory/admin.py b/src/resource_inventory/admin.py index 13afd99..439dad3 100644 --- a/src/resource_inventory/admin.py +++ b/src/resource_inventory/admin.py @@ -30,7 +30,9 @@ from resource_inventory.models import ( OPNFVConfig, OPNFVRole, Image, - RemoteInfo + RemoteInfo, + PhysicalNetwork, + NetworkConnection ) admin.site.register([ @@ -53,4 +55,6 @@ admin.site.register([ OPNFVConfig, OPNFVRole, Image, + PhysicalNetwork, + NetworkConnection, RemoteInfo]) diff --git a/src/resource_inventory/migrations/0013_auto_20200218_1536.py b/src/resource_inventory/migrations/0013_auto_20200218_1536.py index d9dcbd6..053453b 100644 --- a/src/resource_inventory/migrations/0013_auto_20200218_1536.py +++ b/src/resource_inventory/migrations/0013_auto_20200218_1536.py @@ -15,7 +15,7 @@ def clear_resource_bundles(apps, schema_editor): def create_default_template(apps, schema_editor): ResourceTemplate = apps.get_model('resource_inventory', 'ResourceTemplate') - ResourceTemplate.objects.create(id=1, name="Default Template") + ResourceTemplate.objects.create(name="Default Template", hidden=True) def populate_servers(apps, schema_editor): diff --git a/src/resource_inventory/migrations/0015_resourcetemplate_copy_of.py b/src/resource_inventory/migrations/0015_resourcetemplate_copy_of.py new file mode 100644 index 0000000..322dc00 --- /dev/null +++ b/src/resource_inventory/migrations/0015_resourcetemplate_copy_of.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2020-04-13 13:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('resource_inventory', '0014_auto_20200305_1415'), + ] + + operations = [ + migrations.AddField( + model_name='resourcetemplate', + name='copy_of', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceTemplate'), + ), + ] diff --git a/src/resource_inventory/models.py b/src/resource_inventory/models.py index 7115ece..d1b7a75 100644 --- a/src/resource_inventory/models.py +++ b/src/resource_inventory/models.py @@ -155,16 +155,18 @@ class ResourceTemplate(models.Model): # TODO: template might not be a good name because this is a collection of lots of configured resources id = models.AutoField(primary_key=True) - name = models.CharField(max_length=300, unique=True) + name = models.CharField(max_length=300) xml = models.TextField() owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL, related_name="resourcetemplates") description = models.CharField(max_length=1000, default="") public = models.BooleanField(default=False) temporary = models.BooleanField(default=False) + copy_of = models.ForeignKey("ResourceTemplate", null=True, on_delete=models.SET_NULL) def getConfigs(self): - return list(self.resourceConfigurations.all()) + configs = self.resourceConfigurations.all() + return list(configs) def __str__(self): return self.name @@ -191,6 +193,13 @@ class ResourceBundle(models.Model): # TODO pass + def get_template_name(self): + if not self.template: + return "" + if not self.template.temporary: + return self.template.name + return self.template.copy_of.name + class ResourceConfiguration(models.Model): """Model to represent a complete configuration for a single physical Resource.""" @@ -200,7 +209,7 @@ class ResourceConfiguration(models.Model): image = models.ForeignKey("Image", on_delete=models.PROTECT) template = models.ForeignKey(ResourceTemplate, related_name="resourceConfigurations", null=True, on_delete=models.CASCADE) is_head_node = models.BooleanField(default=False) - # name? + name = models.CharField(max_length=3000, default="<Hostname>") def __str__(self): return "config with " + str(self.template) + " and image " + str(self.image) @@ -428,7 +437,7 @@ class InterfaceConfiguration(models.Model): connections = models.ManyToManyField(NetworkConnection) def __str__(self): - return "type " + str(self.profile) + " on host " + str(self.host) + return "type " + str(self.profile) + " on host " + str(self.resource_config) """ diff --git a/src/resource_inventory/pdf_templater.py b/src/resource_inventory/pdf_templater.py index 367ba43..27a264e 100644 --- a/src/resource_inventory/pdf_templater.py +++ b/src/resource_inventory/pdf_templater.py @@ -10,7 +10,7 @@ from django.template.loader import render_to_string import booking -from resource_inventory.models import Server, InterfaceProfile +from resource_inventory.models import Server class PDFTemplater: diff --git a/src/resource_inventory/resource_manager.py b/src/resource_inventory/resource_manager.py index 4310f8c..4d539bd 100644 --- a/src/resource_inventory/resource_manager.py +++ b/src/resource_inventory/resource_manager.py @@ -17,6 +17,7 @@ from resource_inventory.models import ( Network, Vlan, PhysicalNetwork, + InterfaceConfiguration, ) @@ -33,10 +34,12 @@ class ResourceManager: ResourceManager.instance = ResourceManager() return ResourceManager.instance - def getAvailableResourceTemplates(self, lab, user): - templates = ResourceTemplate.objects.filter(lab=lab) - templates = templates.filter(Q(owner=user) | Q(public=True)).filter(temporary=False) - return templates + def getAvailableResourceTemplates(self, lab, user=None): + filter = Q(public=True) + if user: + filter = filter | Q(owner=user) + filter = filter & Q(temporary=False) & Q(lab=lab) + return ResourceTemplate.objects.filter(filter) def templateIsReservable(self, resource_template): """ @@ -110,9 +113,15 @@ class ResourceManager: def configureNetworking(self, resource, vlan_map): for physical_interface in resource.interfaces.all(): - iface_config = physical_interface.acts_as - if not iface_config: + # assign interface configs + iface_configs = InterfaceConfiguration.objects.filter(profile=physical_interface.profile, resource_config=resource.config) + if iface_configs.count() != 1: continue + iface_config = iface_configs.first() + physical_interface.acts_as = iface_config + physical_interface.acts_as.save() + #if not iface_config: + # continue physical_interface.config.clear() for connection in iface_config.connections.all(): physicalNetwork = PhysicalNetwork.objects.create( diff --git a/src/static/js/dashboard.js b/src/static/js/dashboard.js index 6bff8d9..10c7d84 100644 --- a/src/static/js/dashboard.js +++ b/src/static/js/dashboard.js @@ -408,11 +408,8 @@ class MultipleSelectFilterWidget { this.dropdown_count++; const label = document.createElement("H5") label.appendChild(document.createTextNode(node['name'])) - label.classList.add("p-1", "m-1"); + label.classList.add("p-1", "m-1", "flex-grow-1"); div.appendChild(label); - let input = this.make_input(div, node, prepopulate); - input.classList.add("flex-grow-1", "p-1", "m-1"); - div.appendChild(input); let remove_btn = this.make_remove_button(div, node); remove_btn.classList.add("p-1", "m-1"); div.appendChild(remove_btn); @@ -425,10 +422,10 @@ class MultipleSelectFilterWidget { const node = this.filter_items[node_id] const parent = div.parentNode; div.parentNode.removeChild(div); - delete this.result[node.class][node.id]['values'][div.id]; + this.result[node.class][node.id]['count']--; //checks if we have removed last item in class - if(jQuery.isEmptyObject(this.result[node.class][node.id]['values'])){ + if(this.result[node.class][node.id]['count'] == 0){ delete this.result[node.class][node.id]; this.clear(node); } @@ -444,9 +441,9 @@ class MultipleSelectFilterWidget { updateObjectResult(node, childKey, childValue){ if(!this.result[node.class][node.id]) - this.result[node.class][node.id] = {selected: true, id: node.model_id, values: {}} + this.result[node.class][node.id] = {selected: true, id: node.model_id, count: 0} - this.result[node.class][node.id]['values'][childKey] = childValue; + this.result[node.class][node.id]['count']++; } finish(){ @@ -455,9 +452,41 @@ class MultipleSelectFilterWidget { } class NetworkStep { - constructor(debug, xml, hosts, added_hosts, removed_host_ids, graphContainer, overviewContainer, toolbarContainer){ - if(!this.check_support()) + // expects: + // + // debug: bool + // resources: { + // id: { + // id: int, + // value: { + // description: string, + // }, + // interfaces: [ + // id: int, + // name: str, + // description: str, + // connections: [ + // { + // network: int, [networks.id] + // tagged: bool + // } + // ], + // ], + // } + // } + // networks: { + // id: { + // id: int, + // name: str, + // public: bool, + // } + // } + // + constructor(debug, resources, networks, graphContainer, overviewContainer, toolbarContainer){ + if(!this.check_support()) { + console.log("Aborting, browser is not supported"); return; + } this.currentWindow = null; this.netCount = 0; @@ -470,9 +499,24 @@ class NetworkStep { this.editor = new mxEditor(); this.graph = this.editor.graph; + window.global_graph = this.graph; + window.network_rr_index = 5; + this.editor.setGraphContainer(graphContainer); this.doGlobalConfig(); - this.prefill(xml, hosts, added_hosts, removed_host_ids); + + let mx_networks = {} + + for(const network_id in networks) { + let network = networks[network_id]; + + mx_networks[network_id] = this.populateNetwork(network); + } + + this.prefillHosts(resources, mx_networks); + + //this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true); + //this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true); this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', 'fa-search-plus'); this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', 'fa-search-minus'); @@ -489,10 +533,6 @@ class NetworkStep { this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this)); //hooks up double click functionality this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this); - - if(!this.has_public_net){ - this.addPublicNetwork(); - } } check_support(){ @@ -503,22 +543,84 @@ class NetworkStep { return true; } - prefill(xml, hosts, added_hosts, removed_host_ids){ - //populate existing data - if(xml){ - this.restoreFromXml(xml, this.editor); - } else if(hosts){ - for(const host of hosts) - this.makeHost(host); - } + /** + * Expects + * mx_interface: mxCell for the interface itself + * network: mxCell for the outer network + * tagged: bool + */ + connectNetwork(mx_interface, network, tagged) { + var cell = new mxCell( + "connection from " + network + " to " + mx_interface, + new mxGeometry(0, 0, 50, 50)); + cell.edge = true; + cell.geometry.relative = true; + cell.setValue(JSON.stringify({tagged: tagged})); + + let terminal = this.getClosestNetworkCell(mx_interface.geometry.y, network); + let edge = this.graph.addEdge(cell, null, mx_interface, terminal); + this.colorEdge(edge, terminal, true); + this.graph.refresh(edge); + } - //apply any changes - if(added_hosts){ - for(const host of added_hosts) - this.makeHost(host); - this.updateHosts([]); //TODO: why? + /** + * Expects: + * + * to: desired y axis position of the matching cell + * within: graph cell for a full network, with all child cells + * + * Returns: + * an mx cell, the one vertically closest to the desired value + * + * Side effect: + * modifies the <rr_index> on the <within> parameter + */ + getClosestNetworkCell(to, within) { + if(window.network_rr_index === undefined) { + window.network_rr_index = 5; + } + + let child_keys = within.children.keys(); + let children = Array.from(within.children); + let index = (window.network_rr_index++) % children.length; + + let child = within.children[child_keys[index]]; + + return children[index]; + } + + /** Expects + * + * hosts: { + * id: { + * id: int, + * value: { + * description: string, + * }, + * interfaces: [ + * id: int, + * name: str, + * description: str, + * connections: [ + * { + * network: int, [networks.id] + * tagged: bool + * } + * ], + * ], + * } + * } + * + * network_mappings: { + * <django network id>: <mxnetwork id> + * } + * + * draws given hosts into the mxgraph + */ + prefillHosts(hosts, network_mappings){ + for(const host_id in hosts) { + this.makeHost(hosts[host_id], network_mappings); } - this.updateHosts(removed_host_ids); } cellConnectionHandler(sender, event){ @@ -625,7 +727,10 @@ class NetworkStep { color = kvp[1]; } } + edge.setStyle('strokeColor=' + color); + } else { + console.log("Failed to color " + edge + ", " + terminal + ", " + source); } } @@ -848,6 +953,7 @@ class NetworkStep { return true; } } + return false; }; @@ -926,6 +1032,27 @@ class NetworkStep { return ret_val; } + // expects: + // + // { + // id: int, + // name: str, + // public: bool, + // } + // + // returns: + // mxgraph id of network + populateNetwork(network) { + let mxNet = this.makeMxNetwork(network.name, network.public); + this.makeSidebarNetwork(network.name, mxNet.color, mxNet.element_id); + + if( network.public ) { + this.has_public_net = true; + } + + return mxNet.element_id; + } + addPublicNetwork() { const net = this.makeMxNetwork("public", true); this.makeSidebarNetwork("public", net['color'], net['element_id']); @@ -986,7 +1113,33 @@ class NetworkStep { document.getElementById("network_list").appendChild(newNet); } - makeHost(hostInfo) { + /** + * Expects format: + * { + * 'id': int, + * 'value': { + * 'description': string, + * }, + * 'interfaces': [ + * { + * id: int, + * name: str, + * description: str, + * connections: [ + * { + * network: int, <django network id>, + * tagged: bool + * } + * ] + * } + * ] + * } + * + * network_mappings: { + * <django network id>: <mxnetwork id> + * } + */ + makeHost(hostInfo, network_mappings) { const value = JSON.stringify(hostInfo['value']); const interfaces = hostInfo['interfaces']; const width = 100; @@ -1022,6 +1175,15 @@ class NetworkStep { false ); port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0); + const iface = interfaces[i]; + for( const connection of iface.connections ) { + const network = this + .graph + .getModel() + .getCell(network_mappings[connection.network]); + + this.connectNetwork(port, network, connection.tagged); + } this.graph.refresh(port); } this.graph.refresh(host); diff --git a/src/templates/akraino/booking/booking_table.html b/src/templates/akraino/booking/booking_table.html new file mode 100644 index 0000000..4afb4d2 --- /dev/null +++ b/src/templates/akraino/booking/booking_table.html @@ -0,0 +1,41 @@ +{% load jira_filters %} + + +<thead> +<tr> + <th>Owner</th> + <th>Purpose</th> + <th>Project</th> + <th>Start</th> + <th>End</th> + <th>Operating System</th> + <th>Pod</th> +</tr> +</thead> +<tbody> +{% for booking in bookings %} + <tr> + <td> + {{ booking.owner.username }} + </td> + <td> + {{ booking.purpose }} + </td> + <td> + {{ booking.project }} + </td> + <td> + {{ booking.start }} + </td> + <td> + {{ booking.end }} + </td> + <td> + {{ booking.resource.get_head_node.config.image.os.name }} + </td> + <td> + {{ booking.resource.get_template_name }} + </td> + </tr> +{% endfor %} +</tbody> diff --git a/src/templates/akraino/booking/quick_deploy.html b/src/templates/akraino/booking/quick_deploy.html index 56a4791..80354d9 100644 --- a/src/templates/akraino/booking/quick_deploy.html +++ b/src/templates/akraino/booking/quick_deploy.html @@ -1,6 +1,14 @@ {% extends "base/booking/quick_deploy.html" %} {% block opnfv %} {% endblock opnfv %} +{% block form-text %} +<p class="my-0"> + Please select a host type you wish to book. + Only available types are shown. + More information can be found here: + <a href="https://wiki.akraino.org/display/AK/Shared+Community+Lab">Akraino Wiki</a> +</p> +{% endblock form-text %} {% block collab %} <div class="col-12 col-lg-4 my-2"> <div class="col border rounded py-2 h-100"> diff --git a/src/templates/base/account/configuration_list.html b/src/templates/base/account/configuration_list.html index 206c203..fee6e83 100644 --- a/src/templates/base/account/configuration_list.html +++ b/src/templates/base/account/configuration_list.html @@ -41,6 +41,11 @@ var formData = ajaxForm.serialize(); req = new XMLHttpRequest(); var url = "delete/" + current_config_id; + req.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + location.reload(); + } + }; req.open("POST", url, true); req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); req.onerror = function() { alert("problem submitting form"); } diff --git a/src/templates/base/account/resource_list.html b/src/templates/base/account/resource_list.html index 65b46f1..33ccaff 100644 --- a/src/templates/base/account/resource_list.html +++ b/src/templates/base/account/resource_list.html @@ -29,23 +29,20 @@ {% endfor %} </div> <script> - var grb_mapping = {{grb_mapping|safe|default:"{}"}}; - var booking_mapping = {{booking_mapping|safe|default:"{}"}}; + var active_resources = {{active_resources|safe|default:"{}"}} var current_resource_id = -1; function delete_resource(resource_id) { document.getElementById("confirm_delete_button").removeAttribute("disabled"); - var configs = grb_mapping[resource_id]; var warning = document.createTextNode("Are You Sure?"); var warning_subtext = document.createTextNode("This cannot be undone"); - if(booking_mapping[resource_id]){ - var warning = document.createTextNode("This resource is being used. It cannot be deleted."); + if(active_resources[resource_id]){ + var warning = document.createTextNode("This resource is being used or is scheduled to be used. It cannot be deleted."); var warning_subtext = document.createTextNode("If your booking just ended, you may need to give us a few minutes to clean it up before this can be removed."); document.getElementById("confirm_delete_button").disabled = true; } - else if(configs.length > 0) { - list_configs(configs); - warning_text = "Are You Sure? The following Configurations will also be deleted."; + else { + warning_text = "Are You Sure?"; warning = document.createTextNode(warning_text); } @@ -56,7 +53,7 @@ function set_modal_text(title, text) { var clear = function(node) { while(node.lastChild) { - node.removeChild(node.lastChild); + node.removeChild(node.lastChild); } } var warning_title = document.getElementById("config_warning"); @@ -84,6 +81,11 @@ var formData = ajaxForm.serialize(); req = new XMLHttpRequest(); var url = "delete/" + current_resource_id; + req.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + location.reload(); + } + }; req.open("POST", url, true); req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); req.onerror = function() { alert("problem submitting form"); } diff --git a/src/templates/base/base.html b/src/templates/base/base.html index 663741a..cc6d38d 100644 --- a/src/templates/base/base.html +++ b/src/templates/base/base.html @@ -93,12 +93,9 @@ Design a Pod </a> <a href="#" onclick="create_workflow(2)" class="list-group-item list-group-item-action list-group-item-secondary"> - Configure a Pod - </a> - <a href="#" onclick="create_workflow(3)" class="list-group-item list-group-item-action list-group-item-secondary"> Create a Snapshot </a> - <a href="#" onclick="create_workflow(4)" class="list-group-item list-group-item-action list-group-item-secondary"> + <a href="#" onclick="create_workflow(3)" class="list-group-item list-group-item-action list-group-item-secondary"> Configure OPNFV </a> </div> diff --git a/src/templates/base/booking/quick_deploy.html b/src/templates/base/booking/quick_deploy.html index ad9adf2..70b9869 100644 --- a/src/templates/base/booking/quick_deploy.html +++ b/src/templates/base/booking/quick_deploy.html @@ -8,7 +8,9 @@ {% csrf_token %} <div class="row mx-0 px-0"> <div class="col-12 mx-0 px-0 mt-2"> + {% block form-text %} <p class="my-0">Please select a host type you wish to book. Only available types are shown.</p> + {% endblock form-text %} {% bootstrap_field form.filter_field show_label=False %} </div> </div> diff --git a/src/templates/base/dashboard/lab_detail.html b/src/templates/base/dashboard/lab_detail.html index a12c5da..3d90a51 100644 --- a/src/templates/base/dashboard/lab_detail.html +++ b/src/templates/base/dashboard/lab_detail.html @@ -140,9 +140,9 @@ <th>Working</th> <th>Vendor</th> </tr> - {% for host in lab.host_set.all %} + {% for host in hosts %} <tr> - <td>{{host.labid}}</td> + <td>{{host.name}}</td> <td>{{host.profile}}</td> <td>{{host.booked|yesno:"Yes,No"}}</td> {% if host.working %} diff --git a/src/templates/base/resource/steps/pod_definition.html b/src/templates/base/resource/steps/pod_definition.html index 4b8b296..83c4fcb 100644 --- a/src/templates/base/resource/steps/pod_definition.html +++ b/src/templates/base/resource/steps/pod_definition.html @@ -44,29 +44,16 @@ debug = true; {% endif %} - let xml = ''; - {% if xml %} - xml = '{{xml|safe}}'; - {% endif %} - - let hosts = []; - {% for host in hosts %} - hosts.push({{host|safe}}); - {% endfor %} - - let added_hosts = []; - {% for host in added_hosts %} - added_hosts.push({{host|safe}}); - {% endfor %} + const False = false; + const True = true; - let removed_host_ids = {{removed_hosts|safe}}; + let resources = {{resources|safe}}; + let networks = {{networks|safe}}; network_step = new NetworkStep( debug, - xml, - hosts, - added_hosts, - removed_host_ids, + resources, + networks, document.getElementById('graphContainer'), document.getElementById('outlineContainer'), document.getElementById('toolbarContainer'), diff --git a/src/workflow/booking_workflow.py b/src/workflow/booking_workflow.py index 00fa0f9..128f179 100644 --- a/src/workflow/booking_workflow.py +++ b/src/workflow/booking_workflow.py @@ -36,20 +36,20 @@ class Abstract_Resource_Select(AbstractSelectOrCreate): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.select_repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE + self.select_repo_key = self.repo.SELECTED_RESOURCE_TEMPLATE self.confirm_key = self.workflow_type def alert_bundle_missing(self): - self.set_invalid("Please select a valid resource bundle") + self.set_invalid("Please select a valid resource template") def get_form_queryset(self): user = self.repo_get(self.repo.SESSION_USER) - return ResourceTemplate.objects.filter(Q(hidden=False) & (Q(owner=user) | Q(public=True))) + return ResourceTemplate.objects.filter((Q(owner=user) | Q(public=True))) def get_page_context(self): return { 'select_type': 'resource', - 'select_type_title': 'Resource Bundle', + 'select_type_title': 'Resource template', 'addable_type_num': 1 } @@ -81,7 +81,7 @@ class SWConfig_Select(AbstractSelectOrCreate): def get_form_queryset(self): user = self.repo_get(self.repo.SESSION_USER) - grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE) + grb = self.repo_get(self.repo.SELECTED_RESOURCE_TEMPLATE) qs = ResourceTemplate.objects.filter(Q(hidden=False) & (Q(owner=user) | Q(public=True))).filter(bundle=grb) return qs diff --git a/src/workflow/forms.py b/src/workflow/forms.py index a8d3413..4220dea 100644 --- a/src/workflow/forms.py +++ b/src/workflow/forms.py @@ -24,6 +24,7 @@ from resource_inventory.models import ( Installer, Scenario, ) +from resource_inventory.resource_manager import ResourceManager from booking.lib import get_user_items, get_user_field_opts @@ -286,7 +287,7 @@ class MultipleSelectFilterField(forms.Field): class FormUtils: @staticmethod - def getLabData(multiple_hosts=False): + def getLabData(multiple_hosts=False, user=None): """ Get all labs and thier host profiles, returns a serialized version the form can understand. @@ -319,7 +320,7 @@ class FormUtils: neighbors[lab_node['id']] = [] labs[lab_node['id']] = lab_node - for template in lab.resourcetemplates.all(): + for template in ResourceManager.getInstance().getAvailableResourceTemplates(lab, user): resource_node = { 'form': {"name": "host_name", "type": "text", "placeholder": "hostname"}, 'id': "resource_" + str(template.id), @@ -353,9 +354,9 @@ class FormUtils: class HardwareDefinitionForm(forms.Form): - def __init__(self, *args, **kwargs): + def __init__(self, user, *args, **kwargs): super(HardwareDefinitionForm, self).__init__(*args, **kwargs) - attrs = FormUtils.getLabData(multiple_hosts=True) + attrs = FormUtils.getLabData(multiple_hosts=True, user=user) self.fields['filter_field'] = MultipleSelectFilterField( widget=MultipleSelectFilterWidget(**attrs) ) @@ -391,7 +392,7 @@ class NetworkConfigurationForm(forms.Form): class HostSoftwareDefinitionForm(forms.Form): - host_name = forms.CharField(max_length=200, disabled=True, required=False) + host_name = forms.CharField(max_length=200, disabled=False, required=True) headnode = forms.BooleanField(required=False, widget=forms.HiddenInput) def __init__(self, *args, **kwargs): diff --git a/src/workflow/models.py b/src/workflow/models.py index df00d21..173fdba 100644 --- a/src/workflow/models.py +++ b/src/workflow/models.py @@ -18,7 +18,7 @@ import requests from workflow.forms import ConfirmationForm from api.models import JobFactory from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException -from resource_inventory.models import Image, InterfaceConfiguration, OPNFVConfig, ResourceOPNFVConfig, NetworkRole +from resource_inventory.models import Image, OPNFVConfig, ResourceOPNFVConfig, NetworkRole from resource_inventory.resource_manager import ResourceManager from resource_inventory.pdf_templater import PDFTemplater from notifier.manager import NotificationHandler @@ -352,6 +352,7 @@ class Confirmation_Step(WorkflowStep): self.set_valid("Confirmed") elif data == "False": + self.repo.cancel() self.set_valid("Canceled") else: self.set_invalid("Bad Form Contents") @@ -366,14 +367,14 @@ class Repository(): MODELS = "models" RESOURCE_SELECT = "resource_select" CONFIRMATION = "confirmation" - SELECTED_GRESOURCE_BUNDLE = "selected generic bundle pk" + SELECTED_RESOURCE_TEMPLATE = "selected resource template pk" SELECTED_CONFIG_BUNDLE = "selected config bundle pk" SELECTED_OPNFV_CONFIG = "selected opnfv deployment config" - GRESOURCE_BUNDLE_MODELS = "generic_resource_bundle_models" - GRESOURCE_BUNDLE_INFO = "generic_resource_bundle_info" + RESOURCE_TEMPLATE_MODELS = "generic_resource_template_models" + RESOURCE_TEMPLATE_INFO = "generic_resource_template_info" BOOKING = "booking" LAB = "lab" - GRB_LAST_HOSTLIST = "grb_network_previous_hostlist" + RCONFIG_LAST_HOSTLIST = "resource_configuration_network_previous_hostlist" BOOKING_FORMS = "booking_forms" SWCONF_HOSTS = "swconf_hosts" BOOKING_MODELS = "booking models" @@ -391,6 +392,9 @@ class Repository(): SNAPSHOT_DESC = "description of the snapshot" BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking" + # new keys for migration to using ResourceTemplates: + RESOURCE_TEMPLATE_MODELS = "current working model of resource template" + # migratory elements of segmented workflow # each of these is the end result of a different workflow. HAS_RESULT = "whether or not workflow has a result" @@ -399,7 +403,7 @@ class Repository(): def get_child_defaults(self): return_tuples = [] - for key in [self.SELECTED_GRESOURCE_BUNDLE, self.SESSION_USER]: + for key in [self.SELECTED_RESOURCE_TEMPLATE, self.SESSION_USER]: return_tuples.append((key, self.el.get(key))) return return_tuples @@ -428,6 +432,14 @@ class Repository(): else: history[key].append(id) + def cancel(self): + if self.RESOURCE_TEMPLATE_MODELS in self.el: + models = self.el[self.RESOURCE_TEMPLATE_MODELS] + if models['template'].temporary: + models['template'].delete() + # deleting current template should cascade delete all + # necessary related models + def make_models(self): if self.SNAPSHOT_MODELS in self.el: errors = self.make_snapshot() @@ -435,13 +447,13 @@ class Repository(): return errors # if GRB WF, create it - if self.GRESOURCE_BUNDLE_MODELS in self.el: + if self.RESOURCE_TEMPLATE_MODELS in self.el: errors = self.make_generic_resource_bundle() if errors: return errors else: self.el[self.HAS_RESULT] = True - self.el[self.RESULT_KEY] = self.SELECTED_GRESOURCE_BUNDLE + self.el[self.RESULT_KEY] = self.SELECTED_RESOURCE_TEMPLATE return if self.CONFIG_MODELS in self.el: @@ -507,78 +519,23 @@ class Repository(): def make_generic_resource_bundle(self): owner = self.el[self.SESSION_USER] - if self.GRESOURCE_BUNDLE_MODELS in self.el: - models = self.el[self.GRESOURCE_BUNDLE_MODELS] - if 'hosts' in models: - hosts = models['hosts'] - else: - return "GRB has no hosts. CODE:0x0002" - if 'bundle' in models: - bundle = models['bundle'] - else: - return "GRB, no bundle in models. CODE:0x0003" - - try: - bundle.owner = owner - bundle.save() - except Exception as e: - return "GRB, saving bundle generated exception: " + str(e) + " CODE:0x0004" - try: - for host in hosts: - genericresource = host.resource - genericresource.bundle = bundle - genericresource.save() - host.resource = genericresource - host.save() - except Exception as e: - return "GRB, saving hosts generated exception: " + str(e) + " CODE:0x0005" - - if 'networks' in models: - for net in models['networks'].values(): - net.bundle = bundle - net.save() - - if 'interfaces' in models: - for interface_set in models['interfaces'].values(): - for interface in interface_set: - try: - interface.host = interface.host - interface.save() - except Exception: - return "GRB, saving interface " + str(interface) + " failed. CODE:0x0019" - else: - return "GRB, no interface set provided. CODE:0x001a" - - if 'connections' in models: - for resource_name, mapping in models['connections'].items(): - for profile_name, connection_set in mapping.items(): - interface = InterfaceConfiguration.objects.get( - profile__name=profile_name, - host__resource__name=resource_name, - host__resource__bundle=models['bundle'] - ) - for connection in connection_set: - try: - connection.network = connection.network - connection.save() - interface.connections.add(connection) - except Exception as e: - return "GRB, saving vlan " + str(connection) + " failed. Exception: " + str(e) + ". CODE:0x0017" - else: - return "GRB, no vlan set provided. CODE:0x0018" + if self.RESOURCE_TEMPLATE_MODELS in self.el: + models = self.el[self.RESOURCE_TEMPLATE_MODELS] + models['template'].owner = owner + models['template'].temporary = False + models['template'].save() + self.el[self.RESULT] = models['template'] + self.el[self.HAS_RESULT] = True + return False else: return "GRB no models given. CODE:0x0001" - self.el[self.RESULT] = bundle - self.el[self.HAS_RESULT] = True - return False - def make_software_config_bundle(self): models = self.el[self.CONFIG_MODELS] if 'bundle' in models: bundle = models['bundle'] - bundle.bundle = self.el[self.SELECTED_GRESOURCE_BUNDLE] + bundle.bundle = self.el[self.SELECTED_RESOURCE_TEMPLATE] try: bundle.save() except Exception as e: @@ -589,8 +546,8 @@ class Repository(): if 'host_configs' in models: host_configs = models['host_configs'] for host_config in host_configs: - host_config.bundle = host_config.bundle - host_config.host = host_config.host + host_config.template = host_config.template + host_config.profile = host_config.profile try: host_config.save() except Exception as e: @@ -623,8 +580,8 @@ class Repository(): selected_grb = None - if self.SELECTED_GRESOURCE_BUNDLE in self.el: - selected_grb = self.el[self.SELECTED_GRESOURCE_BUNDLE] + if self.SELECTED_RESOURCE_TEMPLATE in self.el: + selected_grb = self.el[self.SELECTED_RESOURCE_TEMPLATE] else: return "BOOK, no selected resource. CODE:0x000e" diff --git a/src/workflow/resource_bundle_workflow.py b/src/workflow/resource_bundle_workflow.py index 89baae7..391d33e 100644 --- a/src/workflow/resource_bundle_workflow.py +++ b/src/workflow/resource_bundle_workflow.py @@ -9,10 +9,13 @@ from django.conf import settings +from django.forms import formset_factory + +from typing import List import json -import re from xml.dom import minidom +import traceback from workflow.models import WorkflowStep from account.models import Lab @@ -20,20 +23,19 @@ from workflow.forms import ( HardwareDefinitionForm, NetworkDefinitionForm, ResourceMetaForm, + HostSoftwareDefinitionForm, ) from resource_inventory.models import ( - ResourceProfile, ResourceTemplate, ResourceConfiguration, InterfaceConfiguration, Network, - NetworkConnection + NetworkConnection, + Image, ) from dashboard.exceptions import ( InvalidVlanConfigurationException, NetworkExistsException, - InvalidHostnameException, - NonUniqueHostnameException, ResourceAvailabilityException ) @@ -54,61 +56,112 @@ class Define_Hardware(WorkflowStep): def get_context(self): context = super(Define_Hardware, self).get_context() - context['form'] = self.form or HardwareDefinitionForm() + user = self.repo_get(self.repo.SESSION_USER) + context['form'] = self.form or HardwareDefinitionForm(user) return context def update_models(self, data): data = data['filter_field'] - models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) - models['hosts'] = [] # This will always clear existing data when this step changes + models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) + models['resources'] = [] # This will always clear existing data when this step changes + models['connections'] = [] models['interfaces'] = {} - if "bundle" not in models: - models['bundle'] = ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER)) - host_data = data['host'] - names = {} - for host_profile_dict in host_data.values(): - id = host_profile_dict['id'] - profile = ResourceProfile.objects.get(id=id) + if "template" not in models: + template = ResourceTemplate.objects.create(temporary=True) + models['template'] = template + + resource_data = data['resource'] + + new_template = models['template'] + + public_network = Network.objects.create(name="public", bundle=new_template, is_public=True) + + all_networks = {public_network.id: public_network} + + for resource_template_dict in resource_data.values(): + id = resource_template_dict['id'] + old_template = ResourceTemplate.objects.get(id=id) + # instantiate genericHost and store in repo - for name in host_profile_dict['values'].values(): - if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name): - raise InvalidHostnameException("Invalid hostname: '" + name + "'") - if name in names: - raise NonUniqueHostnameException("All hosts must have unique names") - names[name] = True - resourceConfig = ResourceConfiguration(profile=profile, template=models['bundle']) - models['hosts'].append(resourceConfig) - for interface_profile in profile.interfaceprofile.all(): - genericInterface = InterfaceConfiguration(profile=interface_profile, resource_config=resourceConfig) - if resourceConfig.name not in models['interfaces']: - models['interfaces'][resourceConfig.name] = [] - models['interfaces'][resourceConfig.name].append(genericInterface) + for _ in range(0, resource_template_dict['count']): + resource_configs = old_template.resourceConfigurations.all() + for config in resource_configs: + # need to save now for connections to refer to it later + new_config = ResourceConfiguration.objects.create( + profile=config.profile, + image=config.image, + name=config.name, + template=new_template) + + for interface_config in config.interface_configs.all(): + new_interface_config = InterfaceConfiguration.objects.create( + profile=interface_config.profile, + resource_config=new_config) + + for connection in interface_config.connections.all(): + network = None + if connection.network.is_public: + network = public_network + else: + # check if network is known + if connection.network.id not in all_networks: + # create matching one + new_network = Network( + name=connection.network.name + "_" + str(new_config.id), + bundle=new_template, + is_public=False) + new_network.save() + + all_networks[connection.network.id] = new_network + + network = all_networks[connection.network.id] + + new_connection = NetworkConnection( + network=network, + vlan_is_tagged=connection.vlan_is_tagged) + + new_interface_config.save() # can't do later because M2M on next line + new_connection.save() + + new_interface_config.connections.add(new_connection) + + unique_resource_ref = new_config.name + "_" + str(new_config.id) + if unique_resource_ref not in models['interfaces']: + models['interfaces'][unique_resource_ref] = [] + models['interfaces'][unique_resource_ref].append(interface_config) + + models['resources'].append(new_config) + + models['networks'] = all_networks # add selected lab to models for lab_dict in data['lab'].values(): if lab_dict['selected']: - models['bundle'].lab = Lab.objects.get(lab_user__id=lab_dict['id']) + models['template'].lab = Lab.objects.get(lab_user__id=lab_dict['id']) + models['template'].save() break # if somehow we get two 'true' labs, we only use one # return to repo - self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models) + self.repo_put(self.repo.RESOURCE_TEMPLATE_MODELS, models) def update_confirmation(self): confirm = self.repo_get(self.repo.CONFIRMATION, {}) - if "resource" not in confirm: - confirm['resource'] = {} - confirm['resource']['hosts'] = [] - models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {"hosts": []}) - for host in models['hosts']: - host_dict = {"name": host.resource.name, "profile": host.profile.name} - confirm['resource']['hosts'].append(host_dict) - if "lab" in models: - confirm['resource']['lab'] = models['lab'].lab_user.username + if "template" not in confirm: + confirm['template'] = {} + confirm['template']['resources'] = [] + models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) + if 'template' in models: + for resource in models['template'].getConfigs(): + host_dict = {"name": resource.name, "profile": resource.profile.name} + confirm['template']['resources'].append(host_dict) + if "template" in models: + confirm['template']['lab'] = models['template'].lab.lab_user.username self.repo_put(self.repo.CONFIRMATION, confirm) def post(self, post_data, user): try: - self.form = HardwareDefinitionForm(post_data) + user = self.repo_get(self.repo.SESSION_USER) + self.form = HardwareDefinitionForm(user, post_data) if self.form.is_valid(): self.update_models(self.form.cleaned_data) self.update_confirmation() @@ -116,9 +169,107 @@ class Define_Hardware(WorkflowStep): else: self.set_invalid("Please complete the fields highlighted in red to continue") except Exception as e: + print("Caught exception: " + str(e)) + traceback.print_exc() self.set_invalid(str(e)) +class Define_Software(WorkflowStep): + template = 'config_bundle/steps/define_software.html' + title = "Pick Software" + description = "Choose the opnfv and image of your machines" + short_title = "host config" + + def build_filter_data(self, hosts_data): + """ + Build list of Images to filter out. + + returns a 2D array of images to exclude + based on the ordering of the passed + hosts_data + """ + + filter_data = [] + user = self.repo_get(self.repo.SESSION_USER) + lab = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS)['template'].lab + for i, host_data in enumerate(hosts_data): + host = ResourceConfiguration.objects.get(pk=host_data['host_id']) + wrong_owner = Image.objects.exclude(owner=user).exclude(public=True) + wrong_host = Image.objects.exclude(host_type=host.profile) + wrong_lab = Image.objects.exclude(from_lab=lab) + excluded_images = wrong_owner | wrong_host | wrong_lab + filter_data.append([]) + for image in excluded_images: + filter_data[i].append(image.pk) + return filter_data + + def create_hostformset(self, hostlist, data=None): + hosts_initial = [] + configs = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}).get("resources") + if configs: + for config in configs: + hosts_initial.append({ + 'host_id': config.id, + 'host_name': config.name, + 'headnode': config.is_head_node, + 'image': config.image + }) + else: + for host in hostlist: + hosts_initial.append({ + 'host_id': host.id, + 'host_name': host.name + }) + + HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0) + filter_data = self.build_filter_data(hosts_initial) + + class SpecialHostFormset(HostFormset): + def get_form_kwargs(self, index): + kwargs = super(SpecialHostFormset, self).get_form_kwargs(index) + if index is not None: + kwargs['imageQS'] = Image.objects.exclude(pk__in=filter_data[index]) + return kwargs + + if data: + return SpecialHostFormset(data, initial=hosts_initial) + return SpecialHostFormset(initial=hosts_initial) + + def get_host_list(self, grb=None): + return self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS).get("resources") + + def get_context(self): + context = super(Define_Software, self).get_context() + + context["formset"] = self.create_hostformset(self.get_host_list()) + + return context + + def post(self, post_data, user): + hosts = self.get_host_list() + + # TODO: fix headnode in form, currently doesn't return a selected one + # models['headnode_index'] = post_data.get("headnode", 1) + formset = self.create_hostformset(hosts, data=post_data) + has_headnode = False + if formset.is_valid(): + for i, form in enumerate(formset): + host = hosts[i] + image = form.cleaned_data['image'] + hostname = form.cleaned_data['host_name'] + headnode = form.cleaned_data['headnode'] + if headnode: + has_headnode = True + host.is_head_node = headnode + host.name = hostname + host.image = image + host.save() + + self.set_valid("Completed") + else: + self.set_invalid("Please complete all fields") + + class Define_Nets(WorkflowStep): template = 'resource/steps/pod_definition.html' title = "Define Networks" @@ -131,7 +282,7 @@ class Define_Nets(WorkflowStep): if vlans: return vlans # try to grab some vlans from lab - models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) if "bundle" not in models: return None lab = models['bundle'].lab @@ -144,12 +295,46 @@ class Define_Nets(WorkflowStep): except Exception: return None + def make_mx_network_dict(self, network): + return { + 'id': network.id, + 'name': network.name, + 'public': network.is_public + } + + def make_mx_resource_dict(self, resource_config): + resource_dict = { + 'id': resource_config.id, + 'interfaces': [], + 'value': { + 'name': resource_config.name, + 'id': resource_config.id, + 'description': resource_config.profile.description + } + } + + for interface_config in resource_config.interface_configs.all(): + connections = [] + for connection in interface_config.connections.all(): + connections.append({'tagged': connection.vlan_is_tagged, 'network': connection.network.id}) + + interface_dict = { + "id": interface_config.id, + "name": interface_config.profile.name, + "description": "speed: " + str(interface_config.profile.speed) + "M\ntype: " + interface_config.profile.nic_type, + "connections": connections + } + + resource_dict['interfaces'].append(interface_dict) + + return resource_dict + def make_mx_host_dict(self, generic_host): host = { - 'id': generic_host.resource.name, + 'id': generic_host.profile.name, 'interfaces': [], 'value': { - "name": generic_host.resource.name, + "name": generic_host.profile.name, "description": generic_host.profile.description } } @@ -160,50 +345,34 @@ class Define_Nets(WorkflowStep): }) return host + # first step guards this one, so can't get here without at least empty + # models being populated by step one def get_context(self): context = super(Define_Nets, self).get_context() context.update({ 'form': NetworkDefinitionForm(), 'debug': settings.DEBUG, + 'resources': {}, + 'networks': {}, + 'vlans': [], + # remove others 'hosts': [], 'added_hosts': [], 'removed_hosts': [] }) - vlans = self.get_vlans() - if vlans: - context['vlans'] = vlans - try: - models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) - hosts = models.get("hosts", []) - # calculate if the selected hosts have changed - added_hosts = set() - host_set = set(self.repo_get(self.repo.GRB_LAST_HOSTLIST, [])) - if len(host_set): - new_host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']]) - context['removed_hosts'] = [h.split("*")[0] for h in (host_set - new_host_set)] - added_hosts.update([h.split("*")[0] for h in (new_host_set - host_set)]) - - # add all host info to context - for generic_host in hosts: - host = self.make_mx_host_dict(generic_host) - host_serialized = json.dumps(host) - context['hosts'].append(host_serialized) - if host['id'] in added_hosts: - context['added_hosts'].append(host_serialized) - bundle = models.get("bundle", False) - if bundle: - context['xml'] = bundle.xml or False - except Exception: - pass + models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS) # infallible, guarded by prior step + for resource in models['resources']: + d = self.make_mx_resource_dict(resource) + context['resources'][d['id']] = d + + for network in models['networks'].values(): + d = self.make_mx_network_dict(network) + context['networks'][d['id']] = d return context def post(self, post_data, user): - models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) - if 'hosts' in models: - host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']]) - self.repo_put(self.repo.GRB_LAST_HOSTLIST, host_set) try: xmlData = post_data.get("xml") self.updateModels(xmlData) @@ -212,42 +381,59 @@ class Define_Nets(WorkflowStep): except ResourceAvailabilityException: self.set_invalid("Public network not availble") except Exception as e: + traceback.print_exc() self.set_invalid("An error occurred when applying networks: " + str(e)) + def resetNetworks(self, networks: List[Network]): # potentially just pass template here? + for network in networks: + network.delete() + def updateModels(self, xmlData): - models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) - models["connections"] = {} - models['networks'] = {} - given_hosts, interfaces, networks = self.parseXml(xmlData) - existing_host_list = models.get("hosts", []) - existing_hosts = {} # maps id to host - for host in existing_host_list: - existing_hosts[host.resource.name] = host + models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) + given_hosts = None + interfaces = None + networks = None + try: + given_hosts, interfaces, networks = self.parseXml(xmlData) + except Exception as e: + print("tried to parse Xml, got exception instead:") + print(e) + + existing_rconfig_list = models.get("resources", []) + existing_rconfigs = {} # maps id to host + for rconfig in existing_rconfig_list: + existing_rconfigs["host_" + str(rconfig.id)] = rconfig - bundle = models.get("bundle", ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER))) + bundle = models.get("template") # hard fail if not in repo + + self.resetNetworks(models['networks'].values()) + models['networks'] = {} for net_id, net in networks.items(): - network = Network() - network.name = net['name'] - network.bundle = bundle - network.is_public = net['public'] + network = Network.objects.create( + name=net['name'], + bundle=bundle, + is_public=net['public']) + models['networks'][net_id] = network + network.save() for hostid, given_host in given_hosts.items(): - existing_host = existing_hosts[hostid[5:]] - for ifaceId in given_host['interfaces']: iface = interfaces[ifaceId] - if existing_host.resource.name not in models['connections']: - models['connections'][existing_host.resource.name] = {} - models['connections'][existing_host.resource.name][iface['profile_name']] = [] + + iface_config = InterfaceConfiguration.objects.get(id=iface['config_id']) + if iface_config.resource_config.template.id != bundle.id: + raise ValidationError("User does not own the template they are editing") + for connection in iface['connections']: network_id = connection['network'] net = models['networks'][network_id] connection = NetworkConnection(vlan_is_tagged=connection['tagged'], network=net) - models['connections'][existing_host.resource.name][iface['profile_name']].append(connection) - bundle.xml = xmlData - self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models) + connection.save() + iface_config.connections.add(connection) + iface_config.save() + self.repo_put(self.repo.RESOURCE_TEMPLATE_MODELS, models) def decomposeXml(self, xmlString): """ @@ -303,7 +489,7 @@ class Define_Nets(WorkflowStep): for cellId, cell in xml_hosts.items(): cell_json_str = cell.getAttribute("value") cell_json = json.loads(cell_json_str) - host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']} + host = {"interfaces": [], "name": cellId, "hostname": cell_json['name']} hosts[cellId] = host # parse networks @@ -324,7 +510,7 @@ class Define_Nets(WorkflowStep): parentId = cell.getAttribute('parent') cell_json_str = cell.getAttribute("value") cell_json = json.loads(cell_json_str) - iface = {"name": cellId, "connections": [], "profile_name": cell_json['name']} + iface = {"graph_id": cellId, "connections": [], "config_id": cell_json['id'], "profile_name": cell_json['name']} hosts[parentId]['interfaces'].append(cellId) interfaces[cellId] = iface @@ -346,9 +532,9 @@ class Define_Nets(WorkflowStep): network = networks[xml_ports[src]] if not tagged: - if interface['name'] in untagged_ifaces: + if interface['config_id'] in untagged_ifaces: raise InvalidVlanConfigurationException("More than one untagged vlan on an interface") - untagged_ifaces.add(interface['name']) + untagged_ifaces.add(interface['config_id']) # add connection to interface interface['connections'].append({"tagged": tagged, "network": network['id']}) @@ -362,12 +548,23 @@ class Resource_Meta_Info(WorkflowStep): description = "Please fill out the rest of the information about your resource" short_title = "pod info" + def update_confirmation(self): + confirm = self.repo_get(self.repo.CONFIRMATION, {}) + if "template" not in confirm: + confirm['template'] = {} + models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) + if "template" in models: + confirm['template']['description'] = models['template'].description + confirm['template']['name'] = models['template'].name + self.repo_put(self.repo.CONFIRMATION, confirm) + def get_context(self): context = super(Resource_Meta_Info, self).get_context() name = "" desc = "" - bundle = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("bundle", False) - if bundle and bundle.name: + models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, None) + bundle = models['template'] + if bundle: name = bundle.name desc = bundle.description context['form'] = ResourceMetaForm(initial={"bundle_name": name, "bundle_description": desc}) @@ -376,14 +573,14 @@ class Resource_Meta_Info(WorkflowStep): def post(self, post_data, user): form = ResourceMetaForm(post_data) if form.is_valid(): - models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) name = form.cleaned_data['bundle_name'] desc = form.cleaned_data['bundle_description'] - bundle = models.get("bundle", ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER))) + bundle = models['template'] # infallible bundle.name = name bundle.description = desc - models['bundle'] = bundle - self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models) + bundle.save() + self.repo_put(self.repo.RESOURCE_TEMPLATE_MODELS, models) confirm = self.repo_get(self.repo.CONFIRMATION) if "resource" not in confirm: confirm['resource'] = {} diff --git a/src/workflow/sw_bundle_workflow.py b/src/workflow/sw_bundle_workflow.py deleted file mode 100644 index 686f46f..0000000 --- a/src/workflow/sw_bundle_workflow.py +++ /dev/null @@ -1,196 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from django.forms import formset_factory - -from workflow.models import WorkflowStep -from workflow.forms import BasicMetaForm, HostSoftwareDefinitionForm -from workflow.booking_workflow import Abstract_Resource_Select -from resource_inventory.models import Image, ResourceConfiguration, ResourceTemplate - - -class SWConf_Resource_Select(Abstract_Resource_Select): - workflow_type = "configuration" - - -class Define_Software(WorkflowStep): - template = 'config_bundle/steps/define_software.html' - title = "Pick Software" - description = "Choose the opnfv and image of your machines" - short_title = "host config" - - def build_filter_data(self, hosts_data): - """ - Build list of Images to filter out. - - returns a 2D array of images to exclude - based on the ordering of the passed - hosts_data - """ - filter_data = [] - user = self.repo_get(self.repo.SESSION_USER) - lab = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE).lab - for i, host_data in enumerate(hosts_data): - host = ResourceConfiguration.objects.get(pk=host_data['host_id']) - wrong_owner = Image.objects.exclude(owner=user).exclude(public=True) - wrong_host = Image.objects.exclude(host_type=host.profile) - wrong_lab = Image.objects.exclude(from_lab=lab) - excluded_images = wrong_owner | wrong_host | wrong_lab - filter_data.append([]) - for image in excluded_images: - filter_data[i].append(image.pk) - return filter_data - - def create_hostformset(self, hostlist, data=None): - hosts_initial = [] - host_configs = self.repo_get(self.repo.CONFIG_MODELS, {}).get("host_configs", False) - if host_configs: - for config in host_configs: - hosts_initial.append({ - 'host_id': config.host.id, - 'host_name': config.host.resource.name, - 'headnode': config.is_head_node, - 'image': config.image - }) - else: - for host in hostlist: - hosts_initial.append({ - 'host_id': host.id, - 'host_name': host.resource.name - }) - - HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0) - filter_data = self.build_filter_data(hosts_initial) - - class SpecialHostFormset(HostFormset): - def get_form_kwargs(self, index): - kwargs = super(SpecialHostFormset, self).get_form_kwargs(index) - if index is not None: - kwargs['imageQS'] = Image.objects.exclude(pk__in=filter_data[index]) - return kwargs - - if data: - return SpecialHostFormset(data, initial=hosts_initial) - return SpecialHostFormset(initial=hosts_initial) - - def get_host_list(self, grb=None): - if grb is None: - grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False) - if not grb: - return [] - if grb.id: - return ResourceConfiguration.objects.filter(resource__bundle=grb) - generic_hosts = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("hosts", []) - return generic_hosts - - def get_context(self): - context = super(Define_Software, self).get_context() - - grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False) - - if grb: - context["grb"] = grb - formset = self.create_hostformset(self.get_host_list(grb)) - context["formset"] = formset - context['headnode'] = self.repo_get(self.repo.CONFIG_MODELS, {}).get("headnode_index", 1) - else: - context["error"] = "Please select a resource first" - self.set_invalid("Step requires information that is not yet provided by previous step") - - return context - - def post(self, post_data, user): - models = self.repo_get(self.repo.CONFIG_MODELS, {}) - if "bundle" not in models: - models['bundle'] = ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER)) - - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - - hosts = self.get_host_list() - models['headnode_index'] = post_data.get("headnode", 1) - formset = self.create_hostformset(hosts, data=post_data) - has_headnode = False - if formset.is_valid(): - models['host_configs'] = [] - confirm_hosts = [] - for i, form in enumerate(formset): - host = hosts[i] - image = form.cleaned_data['image'] - headnode = form.cleaned_data['headnode'] - if headnode: - has_headnode = True - bundle = models['bundle'] - hostConfig = ResourceConfiguration( - host=host, - image=image, - bundle=bundle, - is_head_node=headnode - ) - models['host_configs'].append(hostConfig) - confirm_hosts.append({ - "name": host.resource.name, - "image": image.name, - "headnode": headnode - }) - - if not has_headnode: - self.set_invalid('Must have one "Headnode" per POD') - return - - self.repo_put(self.repo.CONFIG_MODELS, models) - if "configuration" not in confirm: - confirm['configuration'] = {} - confirm['configuration']['hosts'] = confirm_hosts - self.repo_put(self.repo.CONFIRMATION, confirm) - self.set_valid("Completed") - else: - self.set_invalid("Please complete all fields") - - -class Config_Software(WorkflowStep): - template = 'config_bundle/steps/config_software.html' - title = "Other Info" - description = "Give your software config a name, description, and other stuff" - short_title = "config info" - - def get_context(self): - context = super(Config_Software, self).get_context() - - initial = {} - models = self.repo_get(self.repo.CONFIG_MODELS, {}) - bundle = models.get("bundle", False) - if bundle: - initial['name'] = bundle.name - initial['description'] = bundle.description - context["form"] = BasicMetaForm(initial=initial) - return context - - def post(self, post_data, user): - models = self.repo_get(self.repo.CONFIG_MODELS, {}) - if "bundle" not in models: - models['bundle'] = ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER)) - - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - if "configuration" not in confirm: - confirm['configuration'] = {} - - form = BasicMetaForm(post_data) - if form.is_valid(): - models['bundle'].name = form.cleaned_data['name'] - models['bundle'].description = form.cleaned_data['description'] - - confirm['configuration']['name'] = form.cleaned_data['name'] - confirm['configuration']['description'] = form.cleaned_data['description'] - self.set_valid("Complete") - else: - self.set_invalid("Please correct the errors shown below") - - self.repo_put(self.repo.CONFIG_MODELS, models) - self.repo_put(self.repo.CONFIRMATION, confirm) diff --git a/src/workflow/tests/test_steps.py b/src/workflow/tests/test_steps.py index 6101d4f..57bf6a3 100644 --- a/src/workflow/tests/test_steps.py +++ b/src/workflow/tests/test_steps.py @@ -180,7 +180,7 @@ class SoftwareSelectTestCase(SelectStepTestCase): def add_to_repo(self, repo): repo.el[repo.SESSION_USER] = self.user - repo.el[repo.SELECTED_GRESOURCE_BUNDLE] = self.conf.grb + repo.el[repo.SELECTED_RESOURCE_TEMPLATE] = self.conf.grb @classmethod def setUpTestData(cls): @@ -253,7 +253,7 @@ class DefineSoftwareTestCase(StepTestCase): } def add_to_repo(self, repo): - repo.el[repo.SELECTED_GRESOURCE_BUNDLE] = self.conf.grb + repo.el[repo.SELECTED_RESOURCE_TEMPLATE] = self.conf.grb @classmethod def setUpTestData(cls): diff --git a/src/workflow/views.py b/src/workflow/views.py index 9ff444d..9666d72 100644 --- a/src/workflow/views.py +++ b/src/workflow/views.py @@ -35,7 +35,7 @@ def remove_workflow(request): if not manager: return no_workflow(request) - has_more_workflows, result = manager.pop_workflow() + has_more_workflows, result = manager.pop_workflow(discard=True) if not has_more_workflows: # this was the last workflow, so delete the reference to it in the tracker del ManagerTracker.managers[request.session['manager_session']] diff --git a/src/workflow/workflow_factory.py b/src/workflow/workflow_factory.py index 03c8126..04ed280 100644 --- a/src/workflow/workflow_factory.py +++ b/src/workflow/workflow_factory.py @@ -1,5 +1,5 @@ ############################################################################## -# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, Sean Smith, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -9,8 +9,7 @@ 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.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info, Define_Software from workflow.snapshot_workflow import Select_Host_Step, Image_Meta_Step from workflow.opnfv_workflow import Pick_Installer, Assign_Network_Roles, Assign_Host_Roles, OPNFV_Resource_Select, MetaInfo from workflow.models import Confirmation_Step @@ -81,16 +80,11 @@ class WorkflowFactory(): resource_steps = [ Define_Hardware, + Define_Software, Define_Nets, Resource_Meta_Info, ] - config_steps = [ - SWConf_Resource_Select, - Define_Software, - Config_Software, - ] - snapshot_steps = [ Select_Host_Step, Image_Meta_Step, @@ -108,7 +102,6 @@ class WorkflowFactory(): workflow_types = [ self.booking_steps, self.resource_steps, - self.config_steps, self.snapshot_steps, self.opnfv_steps, ] diff --git a/src/workflow/workflow_manager.py b/src/workflow/workflow_manager.py index e31e14c..a48efe5 100644 --- a/src/workflow/workflow_manager.py +++ b/src/workflow/workflow_manager.py @@ -66,7 +66,7 @@ class SessionManager(): return reverse('booking:booking_detail', kwargs={'booking_id': self.result.id}) return "/" - def pop_workflow(self): + def pop_workflow(self, discard=False): multiple_wfs = len(self.workflows) > 1 if multiple_wfs: if self.workflows[-1].repository.el[Repository.RESULT]: # move result @@ -79,6 +79,8 @@ class SessionManager(): else: current_repo = prev_workflow.repository self.result = current_repo.el[current_repo.RESULT] + if discard: + current_repo.cancel() return multiple_wfs, self.result def status(self, request): @@ -164,14 +166,14 @@ class SessionManager(): confirmation = self.make_booking_confirm(booking) self.active_workflow().repository.el[self.active_workflow().repository.BOOKING_MODELS] = models self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirmation - self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = self.make_grb_models(booking.resource.template) - self.active_workflow().repository.el[self.active_workflow().repository.SELECTED_GRESOURCE_BUNDLE] = self.make_grb_models(booking.resource.template)['bundle'] + self.active_workflow().repository.el[self.active_workflow().repository.RESOURCE_TEMPLATE_MODELS] = self.make_grb_models(booking.resource.template) + self.active_workflow().repository.el[self.active_workflow().repository.SELECTED_RESOURCE_TEMPLATE] = self.make_grb_models(booking.resource.template)['bundle'] self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = self.make_config_models(booking.config_bundle) def prefill_resource(self, resource): models = self.make_grb_models(resource) confirm = self.make_grb_confirm(resource) - self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = models + self.active_workflow().repository.el[self.active_workflow().repository.RESOURCE_TEMPLATE_MODELS] = models self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm def prefill_config(self, config): @@ -180,10 +182,10 @@ class SessionManager(): self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = models self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm grb_models = self.make_grb_models(config.bundle) - self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = grb_models + self.active_workflow().repository.el[self.active_workflow().repository.RESOURCE_TEMPLATE_MODELS] = grb_models def make_grb_models(self, resource): - models = self.active_workflow().repository.el.get(self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS, {}) + models = self.active_workflow().repository.el.get(self.active_workflow().repository.RESOURCE_TEMPLATE_MODELS, {}) models['hosts'] = [] models['bundle'] = resource models['interfaces'] = {} |