Merge resource branch 93/70193/4
authorSawyer Bergeron <sbergeron@iol.unh.edu>
Fri, 15 May 2020 18:58:37 +0000 (14:58 -0400)
committerSawyer Bergeron <sbergeron@iol.unh.edu>
Fri, 15 May 2020 21:42:23 +0000 (17:42 -0400)
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:
src/account/views.py
src/api/models.py
src/booking/forms.py
src/booking/quick_deployer.py
src/booking/tests/test_models.py
src/booking/tests/test_quick_booking.py
src/booking/views.py
src/dashboard/testing_utils.py
src/dashboard/views.py
src/resource_inventory/admin.py
src/resource_inventory/migrations/0013_auto_20200218_1536.py
src/resource_inventory/migrations/0015_resourcetemplate_copy_of.py [new file with mode: 0644]
src/resource_inventory/models.py
src/resource_inventory/pdf_templater.py
src/resource_inventory/resource_manager.py
src/static/js/dashboard.js
src/templates/akraino/booking/booking_table.html [new file with mode: 0644]
src/templates/akraino/booking/quick_deploy.html
src/templates/base/account/configuration_list.html
src/templates/base/account/resource_list.html
src/templates/base/base.html
src/templates/base/booking/quick_deploy.html
src/templates/base/dashboard/lab_detail.html
src/templates/base/resource/steps/pod_definition.html
src/workflow/booking_workflow.py
src/workflow/forms.py
src/workflow/models.py
src/workflow/resource_bundle_workflow.py
src/workflow/sw_bundle_workflow.py [deleted file]
src/workflow/tests/test_steps.py
src/workflow/views.py
src/workflow/workflow_factory.py
src/workflow/workflow_manager.py

index a8bb02b..d1cc813 100644 (file)
@@ -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('')
index e41a44d..addc02d 100644 (file)
@@ -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
index b9c9231..886f0f6 100644 (file)
@@ -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.
index 951ff47..9cfc465 100644 (file)
@@ -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)
 
index c8c8ea8..37eb655 100644 (file)
 
 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
             )
         )
 
index 5ba1744..f405047 100644 (file)
@@ -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": {
index daaf026..3c95e07 100644 (file)
@@ -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
index b7272ea..d7a346e 100644 (file)
@@ -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'
index 498bd9d..2ace2d4 100644 (file)
@@ -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
         }
     )
 
index 13afd99..439dad3 100644 (file)
@@ -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])
index d9dcbd6..053453b 100644 (file)
@@ -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 (file)
index 0000000..322dc00
--- /dev/null
@@ -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'),
+        ),
+    ]
index 7115ece..d1b7a75 100644 (file)
@@ -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)
 
 
 """
index 367ba43..27a264e 100644 (file)
@@ -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:
index 4310f8c..4d539bd 100644 (file)
@@ -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(
index 6bff8d9..10c7d84 100644 (file)
@@ -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 (file)
index 0000000..4afb4d2
--- /dev/null
@@ -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>
index 56a4791..80354d9 100644 (file)
@@ -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">
index 206c203..fee6e83 100644 (file)
         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"); }
index 65b46f1..33ccaff 100644 (file)
 {% 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");
         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"); }
index 663741a..cc6d38d 100644 (file)
                                     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>
index ad9adf2..70b9869 100644 (file)
@@ -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>
index a12c5da..3d90a51 100644 (file)
                         <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 %}
index 4b8b296..83c4fcb 100644 (file)
     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'),
index 00fa0f9..128f179 100644 (file)
@@ -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
 
index a8d3413..4220dea 100644 (file)
@@ -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):
index df00d21..173fdba 100644 (file)
@@ -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"
 
index 89baae7..391d33e 100644 (file)
@@ -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 (file)
index 686f46f..0000000
+++ /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)
index 6101d4f..57bf6a3 100644 (file)
@@ -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):
index 9ff444d..9666d72 100644 (file)
@@ -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']]
index 03c8126..04ed280 100644 (file)
@@ -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,
         ]
index e31e14c..a48efe5 100644 (file)
@@ -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'] = {}