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