summaryrefslogtreecommitdiffstats
path: root/dashboard/src/workflow
diff options
context:
space:
mode:
Diffstat (limited to 'dashboard/src/workflow')
-rw-r--r--dashboard/src/workflow/__init__.py8
-rw-r--r--dashboard/src/workflow/apps.py15
-rw-r--r--dashboard/src/workflow/booking_workflow.py295
-rw-r--r--dashboard/src/workflow/forms.py446
-rw-r--r--dashboard/src/workflow/models.py508
-rw-r--r--dashboard/src/workflow/resource_bundle_workflow.py427
-rw-r--r--dashboard/src/workflow/snapshot_workflow.py111
-rw-r--r--dashboard/src/workflow/sw_bundle_workflow.py238
-rw-r--r--dashboard/src/workflow/tests/__init__.py8
-rw-r--r--dashboard/src/workflow/tests/constants.py198
-rw-r--r--dashboard/src/workflow/tests/test_steps.py271
-rw-r--r--dashboard/src/workflow/tests/test_steps_render.py36
-rw-r--r--dashboard/src/workflow/tests/test_workflows.py96
-rw-r--r--dashboard/src/workflow/urls.py34
-rw-r--r--dashboard/src/workflow/views.py108
-rw-r--r--dashboard/src/workflow/workflow_factory.py149
-rw-r--r--dashboard/src/workflow/workflow_manager.py251
17 files changed, 3199 insertions, 0 deletions
diff --git a/dashboard/src/workflow/__init__.py b/dashboard/src/workflow/__init__.py
new file mode 100644
index 0000000..e0408fa
--- /dev/null
+++ b/dashboard/src/workflow/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
diff --git a/dashboard/src/workflow/apps.py b/dashboard/src/workflow/apps.py
new file mode 100644
index 0000000..adc2738
--- /dev/null
+++ b/dashboard/src/workflow/apps.py
@@ -0,0 +1,15 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.apps import AppConfig
+
+
+class WorkflowConfig(AppConfig):
+ name = 'workflow'
diff --git a/dashboard/src/workflow/booking_workflow.py b/dashboard/src/workflow/booking_workflow.py
new file mode 100644
index 0000000..52fe36b
--- /dev/null
+++ b/dashboard/src/workflow/booking_workflow.py
@@ -0,0 +1,295 @@
+##############################################################################
+# 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.contrib import messages
+from django.shortcuts import render
+from django.contrib.auth.models import User
+from django.utils import timezone
+
+import json
+from datetime import timedelta
+
+from account.models import UserProfile
+from booking.models import Booking
+from workflow.models import WorkflowStep
+from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm, ConfirmationForm
+from resource_inventory.models import GenericResourceBundle, ResourceBundle, ConfigBundle
+
+class Resource_Select(WorkflowStep):
+ template = 'booking/steps/resource_select.html'
+ title = "Select Resource"
+ description = "Select a resource template to use for your deployment"
+ short_title = "pod select"
+
+ def __init__(self, *args, **kwargs):
+ super(Resource_Select, self).__init__(*args, **kwargs)
+ self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
+ self.repo_check_key = False
+ self.confirm_key = "booking"
+
+ def get_default_entry(self):
+ return None
+
+ def get_context(self):
+ context = super(Resource_Select, self).get_context()
+ default = []
+ chosen_bundle = None
+ default_bundle = self.get_default_entry()
+ if default_bundle:
+ context['disabled'] = True
+ chosen_bundle = default_bundle
+ if chosen_bundle.id:
+ default.append(chosen_bundle.id)
+ else:
+ default.append("repo bundle")
+ else:
+ chosen_bundle = self.repo_get(self.repo_key, False)
+ if chosen_bundle:
+ if chosen_bundle.id:
+ default.append(chosen_bundle.id)
+ else:
+ default.append("repo bundle")
+
+ bundle = default_bundle
+ if not bundle:
+ bundle = chosen_bundle
+ edit = self.repo_get(self.repo.EDIT, False)
+ user = self.repo_get(self.repo.SESSION_USER)
+ context['form'] = ResourceSelectorForm(
+ data={"user": user},
+ chosen_resource=default,
+ bundle=bundle,
+ edit=edit
+ )
+ return context
+
+ def post_render(self, request):
+ form = ResourceSelectorForm(request.POST)
+ context = self.get_context()
+ if form.is_valid():
+ data = form.cleaned_data['generic_resource_bundle']
+ irint(str(data['user']))
+ data = data[2:-2]
+ if not data:
+ self.metastep.set_invalid("Please select a valid bundle")
+ return render(request, self.template, context)
+ selected_bundle = json.loads(data)
+ selected_id = selected_bundle[0]['id']
+ gresource_bundle = None
+ try:
+ selected_id = int(selected_id)
+ gresource_bundle = GenericResourceBundle.objects.get(id=selected_id)
+ except ValueError:
+ # we want the bundle in the repo
+ gresource_bundle = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS,{}).get("bundle", GenericResourceBundle())
+ self.repo_put(
+ self.repo_key,
+ gresource_bundle
+ )
+ confirm = self.repo_get(self.repo.CONFIRMATION)
+ if self.confirm_key not in confirm:
+ confirm[self.confirm_key] = {}
+ confirm[self.confirm_key]["resource name"] = gresource_bundle.name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+ messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
+ self.metastep.set_valid("Step Completed")
+ return render(request, self.template, context)
+ else:
+ messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
+ self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
+ return render(request, self.template, context)
+
+class Booking_Resource_Select(Resource_Select):
+
+ def __init__(self, *args, **kwargs):
+ super(Booking_Resource_Select, self).__init__(*args, **kwargs)
+ self.repo_key = self.repo.BOOKING_SELECTED_GRB
+ self.confirm_key = "booking"
+
+ def get_default_entry(self):
+ default = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("bundle")
+ mine = self.repo_get(self.repo_key)
+ if mine:
+ return None
+ try:
+ config_bundle = self.repo_get(self.repo.BOOKING_MODELS)['booking'].config_bundle
+ if default:
+ return default # select created grb, even if preselected config bundle
+ return config_bundle.bundle
+ except:
+ pass
+ return default
+
+ def get_context(self):
+ context = super(Booking_Resource_Select, self).get_context()
+ return context
+
+ def post_render(self, request):
+ response = super(Booking_Resource_Select, self).post_render(request)
+ models = self.repo_get(self.repo.BOOKING_MODELS, {})
+ if "booking" not in models:
+ models['booking'] = Booking()
+ booking = models['booking']
+ resource = self.repo_get(self.repo_key, False)
+ if resource:
+ try:
+ booking.resource.template = resource
+ except:
+ booking.resource = ResourceBundle(template=resource)
+ models['booking'] = booking
+ self.repo_put(self.repo.BOOKING_MODELS, models)
+ return response
+
+class SWConfig_Select(WorkflowStep):
+ template = 'booking/steps/swconfig_select.html'
+ title = "Select Software Configuration"
+ description = "Choose the software and related configurations you want to have used for your deployment"
+ short_title = "pod config"
+
+ def post_render(self, request):
+ form = SWConfigSelectorForm(request.POST)
+ if form.is_valid():
+
+ bundle_json = form.cleaned_data['software_bundle']
+ bundle_json = bundle_json[2:-2] # Stupid django string bug
+ if not bundle_json:
+ self.metastep.set_invalid("Please select a valid config")
+ return self.render(request)
+ bundle_json = json.loads(bundle_json)
+ bundle = None
+ try:
+ id = int(bundle_json[0]['id'])
+ bundle = ConfigBundle.objects.get(id=id)
+ except ValueError:
+ bundle = self.repo_get(self.repo.CONFIG_MODELS).get("bundle")
+
+ models = self.repo_get(self.repo.BOOKING_MODELS, {})
+ if "booking" not in models:
+ models['booking'] = Booking()
+ models['booking'].config_bundle = bundle
+ self.repo_put(self.repo.BOOKING_MODELS, models)
+ confirm = self.repo_get(self.repo.CONFIRMATION)
+ if "booking" not in confirm:
+ confirm['booking'] = {}
+ confirm['booking']["configuration name"] = bundle.name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+ self.metastep.set_valid("Step Completed")
+ messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
+ else:
+ self.metastep.set_invalid("Please select or create a valid config")
+ messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
+
+ return self.render(request)
+
+
+ def get_context(self):
+ context = super(SWConfig_Select, self).get_context()
+ default = []
+ bundle = None
+ chosen_bundle = None
+ created_bundle = self.repo_get(self.repo.CONFIG_MODELS, {}).get("bundle", False)
+ booking = self.repo_get(self.repo.BOOKING_MODELS, {}).get("booking", False)
+ try:
+ chosen_bundle = booking.config_bundle
+ default.append(chosen_bundle.id)
+ bundle=chosen_bundle
+ except:
+ if created_bundle:
+ default.append("repo bundle")
+ bundle = created_bundle
+ context['disabled'] = True
+ edit = self.repo_get(self.repo.EDIT, False)
+ grb = self.repo_get(self.repo.BOOKING_SELECTED_GRB)
+ context['form'] = SWConfigSelectorForm(chosen_software=default, bundle=bundle, edit=edit, resource=grb)
+ return context
+
+class Booking_Meta(WorkflowStep):
+ template = 'booking/steps/booking_meta.html'
+ title = "Extra Info"
+ description = "Tell us how long you want your booking, what it is for, and who else should have access to it"
+ short_title = "booking info"
+
+ def get_context(self):
+ context = super(Booking_Meta, self).get_context()
+ initial = {}
+ default = []
+ try:
+ models = self.repo_get(self.repo.BOOKING_MODELS, {})
+ booking = models.get("booking")
+ if booking:
+ initial['purpose'] = booking.purpose
+ initial['project'] = booking.project
+ initial['length'] = (booking.end - booking.start).days
+ info = self.repo_get(self.repo.BOOKING_INFO_FILE, False)
+ if info:
+ initial['info_file'] = info
+ users = models.get("collaborators", [])
+ for user in users:
+ default.append(user.id)
+ except Exception as e:
+ pass
+
+ default_user = self.repo_get(self.repo.SESSION_USER)
+ if default_user is None:
+ # TODO: error
+ default_user = "you"
+ else:
+ default_user = default_user.username
+
+ context['form'] = BookingMetaForm(initial=initial, chosen_users=default, default_user=default_user)
+ return context
+
+ def post_render(self, request):
+ form = BookingMetaForm(data=request.POST)
+ context = self.get_context()
+
+ forms = self.repo_get(self.repo.BOOKING_FORMS, {})
+
+ forms["meta_form"] = form
+ self.repo_put(self.repo.BOOKING_FORMS, forms)
+
+ if form.is_valid():
+ models = self.repo_get(self.repo.BOOKING_MODELS, {})
+ if "booking" not in models:
+ models['booking'] = Booking()
+ models['collaborators'] = []
+ confirm = self.repo_get(self.repo.CONFIRMATION)
+ if "booking" not in confirm:
+ confirm['booking'] = {}
+
+ models['booking'].start = timezone.now()
+ models['booking'].end = timezone.now() + timedelta(days=int(form.cleaned_data['length']))
+ models['booking'].purpose = form.cleaned_data['purpose']
+ models['booking'].project = form.cleaned_data['project']
+ for key in ['length', 'project', 'purpose']:
+ confirm['booking'][key] = form.cleaned_data[key]
+
+ user_data = form.cleaned_data['users']
+ confirm['booking']['collaborators'] = []
+ user_data = user_data[2:-2] #fixes malformed string from querydict
+ if user_data:
+ form_users = json.loads(user_data)
+ for user_json in form_users:
+ user = User.objects.get(pk=user_json['id'])
+ models['collaborators'].append(user)
+ confirm['booking']['collaborators'].append(user.username)
+
+ info_file = form.cleaned_data.get("info_file", False)
+ if info_file:
+ self.repo_put(self.repo.BOOKING_INFO_FILE, info_file)
+
+ self.repo_put(self.repo.BOOKING_MODELS, models)
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+ messages.add_message(request, messages.SUCCESS, 'Form Validated', fail_silently=True)
+ self.metastep.set_valid("Step Completed")
+ else:
+ messages.add_message(request, messages.ERROR, "Form didn't validate", fail_silently=True)
+ self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
+ context['form'] = form # TODO: store this form
+ return render(request, self.template, context)
diff --git a/dashboard/src/workflow/forms.py b/dashboard/src/workflow/forms.py
new file mode 100644
index 0000000..c770e38
--- /dev/null
+++ b/dashboard/src/workflow/forms.py
@@ -0,0 +1,446 @@
+##############################################################################
+# 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
+##############################################################################
+
+
+import django.forms as forms
+from django.forms import widgets
+from django.contrib.auth.models import User
+from django.utils.safestring import mark_safe
+from django.template.loader import render_to_string
+from django.core import serializers
+from django.forms.widgets import NumberInput
+from django.db.models import F
+
+import json
+
+from resource_inventory.models import *
+from account.models import Lab
+from account.models import UserProfile
+
+
+class SearchableSelectMultipleWidget(widgets.SelectMultiple):
+ template_name = 'dashboard/searchable_select_multiple.html'
+
+ def __init__(self, attrs=None):
+ self.items = attrs['set']
+ self.show_from_noentry = attrs['show_from_noentry']
+ self.show_x_results = attrs['show_x_results']
+ self.results_scrollable = attrs['scrollable']
+ self.selectable_limit = attrs['selectable_limit']
+ self.placeholder = attrs['placeholder']
+ self.name = attrs['name']
+ self.initial = attrs.get("initial", "")
+ self.default_entry = attrs.get("default_entry", "")
+ self.edit = attrs.get("edit", False)
+ self.wf_type = attrs.get("wf_type")
+
+ super(SearchableSelectMultipleWidget, self).__init__(attrs)
+
+ def render(self, name, value, attrs=None, renderer=None):
+
+ context = self.get_context(attrs)
+ return mark_safe(render_to_string(self.template_name, context))
+
+ def get_context(self,attrs):
+ return {'items':self.items,
+ 'name':self.name,
+ 'show_from_noentry':self.show_from_noentry,
+ 'show_x_results':self.show_x_results,
+ 'results_scrollable':self.results_scrollable,
+ 'selectable_limit':self.selectable_limit,
+ 'placeholder':self.placeholder,
+ 'initial':self.initial,
+ 'default_entry':self.default_entry,
+ 'edit': self.edit,
+ 'wf_type': self.wf_type
+ }
+
+class ResourceSelectorForm(forms.Form):
+
+ def __init__(self, data=None, **kwargs):
+ chosen_resource = ""
+ bundle = None
+ edit = False
+ if "chosen_resource" in kwargs:
+ chosen_resource = kwargs.pop("chosen_resource")
+ if "bundle" in kwargs:
+ bundle = kwargs.pop("bundle")
+ if "edit" in kwargs:
+ edit = kwargs.pop("edit")
+ super(ResourceSelectorForm, self).__init__(data=data,**kwargs)
+ queryset = GenericResourceBundle.objects.select_related("owner").all()
+ if data and 'user' in data:
+ queryset = queryset.filter(owner=data['user'])
+
+ attrs = self.build_search_widget_attrs(chosen_resource, bundle, edit, queryset)
+
+ self.fields['generic_resource_bundle'] = forms.CharField(
+ widget=SearchableSelectMultipleWidget(attrs=attrs)
+ )
+
+ def build_search_widget_attrs(self, chosen_resource, bundle, edit, queryset):
+ resources = {}
+ for res in queryset:
+ displayable = {}
+ displayable['small_name'] = res.name
+ if res.owner:
+ displayable['expanded_name'] = res.owner.username
+ else:
+ displayable['expanded_name'] = ""
+ displayable['string'] = res.description
+ displayable['id'] = res.id
+ resources[res.id] = displayable
+
+ if bundle:
+ displayable = {}
+ displayable['small_name'] = bundle.name
+ displayable['expanded_name'] = "Current bundle"
+ displayable['string'] = bundle.description
+ displayable['id'] = "repo bundle"
+ resources["repo bundle"] = displayable
+ attrs={
+ 'set': resources,
+ 'show_from_noentry': "true",
+ 'show_x_results': -1,
+ 'scrollable': "true",
+ 'selectable_limit': 1,
+ 'name': "generic_resource_bundle",
+ 'placeholder': "resource",
+ 'initial': chosen_resource,
+ 'edit': edit,
+ 'wf_type': 1
+ }
+ return attrs
+
+class SWConfigSelectorForm(forms.Form):
+
+ def __init__(self, *args, **kwargs):
+ chosen_software = ""
+ bundle = None
+ edit = False
+ resource = None
+ if "chosen_software" in kwargs:
+ chosen_software = kwargs.pop("chosen_software")
+
+ if "bundle" in kwargs:
+ bundle = kwargs.pop("bundle")
+ if "edit" in kwargs:
+ edit = kwargs.pop("edit")
+ if "resource" in kwargs:
+ resource = kwargs.pop("resource")
+ super(SWConfigSelectorForm, self).__init__(*args,**kwargs)
+ attrs = self.build_search_widget_attrs(chosen_software,bundle, edit, resource)
+ self.fields['software_bundle'] = forms.CharField(
+ widget=SearchableSelectMultipleWidget(attrs=attrs)
+ )
+
+ def build_search_widget_attrs(self, chosen, bundle, edit, resource):
+ configs = {}
+ queryset = ConfigBundle.objects.select_related('owner').all()
+ if resource:
+ queryset = queryset.filter(bundle=resource)
+
+ for config in queryset:
+ displayable = {}
+ displayable['small_name'] = config.name
+ displayable['expanded_name'] = config.owner.username
+ displayable['string'] = config.description
+ displayable['id'] = config.id
+ configs[config.id] = displayable
+
+ if bundle:
+ displayable = {}
+ displayable['small_name'] = bundle.name
+ displayable['expanded_name'] = "Current configuration"
+ displayable['string'] = bundle.description
+ displayable['id'] = "repo bundle"
+ configs['repo bundle'] = displayable
+
+ attrs={
+ 'set': configs,
+ 'show_from_noentry': "true",
+ 'show_x_results': -1,
+ 'scrollable': "true",
+ 'selectable_limit': 1,
+ 'name': "software_bundle",
+ 'placeholder': "config",
+ 'initial': chosen,
+ 'edit': edit,
+ 'wf_type': 2
+ }
+ return attrs
+
+class BookingMetaForm(forms.Form):
+
+ length = forms.IntegerField(widget=NumberInput(attrs={'type':'range', 'min':"0", "max":"21", "value":"0"}))
+ purpose = forms.CharField(max_length=1000)
+ project = forms.CharField(max_length=400)
+ info_file = forms.CharField(max_length=1000, required=False)
+
+ def __init__(self, data=None, *args, **kwargs):
+ chosen_users = []
+ if "default_user" in kwargs:
+ default_user = kwargs.pop("default_user")
+ else:
+ default_user = "you"
+ if "chosen_users" in kwargs:
+ chosen_users = kwargs.pop("chosen_users")
+ elif data and "users" in data:
+ chosen_users = data.getlist("users")
+ else:
+ pass
+
+ super(BookingMetaForm, self).__init__(data=data, **kwargs)
+
+ self.fields['users'] = forms.CharField(
+ widget=SearchableSelectMultipleWidget(
+ attrs=self.build_search_widget_attrs(chosen_users, default_user=default_user)
+ ),
+ required=False
+ )
+
+ def build_user_list(self):
+ """
+ returns a mapping of UserProfile ids to displayable objects expected by
+ searchable multiple select widget
+ """
+ try:
+ users = {}
+ d_qset = UserProfile.objects.select_related('user').all();
+ for userprofile in d_qset:
+ user = {
+ 'id':userprofile.user.id,
+ 'expanded_name':userprofile.full_name,
+ 'small_name':userprofile.user.username,
+ 'string':userprofile.email_addr
+ }
+
+ users[userprofile.user.id] = user
+
+ return users
+ except Exception as e:
+ pass
+
+ def build_search_widget_attrs(self, chosen_users, default_user="you"):
+
+ attrs={
+ 'set': self.build_user_list(),
+ 'show_from_noentry': "false",
+ 'show_x_results': 10,
+ 'scrollable': "false",
+ 'selectable_limit': -1,
+ 'name': "users",
+ 'placeholder': "username",
+ 'default_entry': default_user,
+ 'initial': chosen_users,
+ 'edit': False
+ }
+ return attrs
+
+class MultipleSelectFilterWidget(forms.Widget):
+ def __init__(self, attrs=None):
+ super(MultipleSelectFilterWidget, self).__init__(attrs)
+ self.attrs = attrs
+ self.template_name="dashboard/multiple_select_filter_widget.html"
+
+ def render(self, name, value, attrs=None, renderer=None):
+ attrs = self.attrs
+ self.context = self.get_context(name, value, attrs)
+ html = render_to_string(self.template_name, context=self.context)
+ return mark_safe(html)
+
+ def get_context(self, name, value, attrs):
+ return attrs
+
+class MultipleSelectFilterField(forms.Field):
+ def __init__( self, required=True, widget=None, label=None, initial=None,
+ help_text='', error_messages=None, show_hidden_initial=False,
+ validators=(), localize=False, disabled=False, label_suffix=None):
+ """from the documentation:
+ # required -- Boolean that specifies whether the field is required.
+ # True by default.
+ # widget -- A Widget class, or instance of a Widget class, that should
+ # be used for this Field when displaying it. Each Field has a
+ # default Widget that it'll use if you don't specify this. In
+ # most cases, the default widget is TextInput.
+ # label -- A verbose name for this field, for use in displaying this
+ # field in a form. By default, Django will use a "pretty"
+ # version of the form field name, if the Field is part of a
+ # Form.
+ # initial -- A value to use in this Field's initial display. This value
+ # is *not* used as a fallback if data isn't given.
+ # help_text -- An optional string to use as "help text" for this Field.
+ # error_messages -- An optional dictionary to override the default
+ # messages that the field will raise.
+ # show_hidden_initial -- Boolean that specifies if it is needed to render a
+ # hidden widget with initial value after widget.
+ # validators -- List of additional validators to use
+ # localize -- Boolean that specifies if the field should be localized.
+ # disabled -- Boolean that specifies whether the field is disabled, that
+ # is its widget is shown in the form but not editable.
+ # label_suffix -- Suffix to be added to the label. Overrides
+ # form's label_suffix.
+ """
+ #this is bad, but django forms are annoying
+ self.widget=widget
+ if self.widget is None:
+ self.widget = MultipleSelectFilterWidget()
+ super(MultipleSelectFilterField, self).__init__(
+ required=required,
+ widget=self.widget,
+ label=label,
+ initial=None,
+ help_text=help_text,
+ error_messages=error_messages,
+ show_hidden_initial=show_hidden_initial,
+ validators=validators,
+ localize=localize,
+ disabled=disabled,
+ label_suffix=label_suffix
+ )
+
+ def clean(data):
+ """
+ This method will raise a django.forms.ValidationError or return clean data
+ """
+ return data
+
+class FormUtils:
+ @staticmethod
+ def getLabData():
+ """
+ Gets all labs and thier host profiles and returns a serialized version the form can understand.
+ Should be rewritten with a related query to make it faster
+ Should be moved outside of global scope
+ """
+ labs = {}
+ hosts = {}
+ items = {}
+ mapping = {}
+ for lab in Lab.objects.all():
+ slab = {}
+ slab['id'] = "lab_" + str(lab.lab_user.id)
+ slab['name'] = lab.name
+ slab['description'] = lab.description
+ slab['selected'] = 0
+ slab['selectable'] = 1
+ slab['follow'] = 1
+ slab['multiple'] = 0
+ items[slab['id']] = slab
+ mapping[slab['id']] = []
+ labs[slab['id']] = slab
+ for host in lab.hostprofiles.all():
+ shost = {}
+ shost['forms'] = [{"name": "host_name", "type": "text", "placeholder": "hostname"}]
+ shost['id'] = "host_" + str(host.id)
+ shost['name'] = host.name
+ shost['description'] = host.description
+ shost['selected'] = 0
+ shost['selectable'] = 1
+ shost['follow'] = 0
+ shost['multiple'] = 1
+ items[shost['id']] = shost
+ mapping[slab['id']].append(shost['id'])
+ if shost['id'] not in mapping:
+ mapping[shost['id']] = []
+ mapping[shost['id']].append(slab['id'])
+ hosts[shost['id']] = shost
+
+ filter_objects = [("labs", labs.values()), ("hosts", hosts.values())]
+
+ context = {
+ 'filter_objects': filter_objects,
+ 'mapping': mapping,
+ 'items': items
+ }
+ return context
+
+class HardwareDefinitionForm(forms.Form):
+
+ def __init__(self, *args, **kwargs):
+ selection_data = kwargs.pop("selection_data", False)
+ super(HardwareDefinitionForm, self).__init__(*args, **kwargs)
+ attrs = FormUtils.getLabData()
+ attrs['selection_data'] = selection_data
+ self.fields['filter_field'] = MultipleSelectFilterField(
+ widget=MultipleSelectFilterWidget(
+ attrs=attrs
+ )
+ )
+
+class PodDefinitionForm(forms.Form):
+
+ fields = [ "xml" ]
+ xml = forms.CharField()
+
+class ResourceMetaForm(forms.Form):
+
+ bundle_name = forms.CharField(label="POD Name")
+ bundle_description = forms.CharField(label="POD Description", widget=forms.Textarea)
+
+class GenericHostMetaForm(forms.Form):
+
+ host_profile = forms.CharField(label="Host Type", disabled=True, required=False)
+ host_name = forms.CharField(label="Host Name")
+
+class NetworkDefinitionForm(forms.Form):
+ def __init__(self, *args, **kwargs):
+ fields = []
+
+ super(NetworkDefinitionForm, self).__init__(**kwargs)
+
+class NetworkConfigurationForm(forms.Form):
+ def __init__(self, *args, **kwargs):
+ fields = []
+
+ super(NetworkConfigurationForm).__init__(**kwargs)
+
+class HostSoftwareDefinitionForm(forms.Form):
+ fields = ["host_name", "role", "image"]
+
+ host_name = forms.CharField(max_length=200, disabled=True, required=False)
+ role = forms.ModelChoiceField(queryset=OPNFVRole.objects.all())
+ image = forms.ModelChoiceField(queryset=Image.objects.all())
+
+class SoftwareConfigurationForm(forms.Form):
+
+ name = forms.CharField(max_length=200)
+ description = forms.CharField(widget=forms.Textarea)
+ opnfv = forms.BooleanField(disabled=True, required=False)
+ installer = forms.ModelChoiceField(queryset=Installer.objects.all(), disabled=True, required=False)
+ scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), disabled=True, required=False)
+
+class WorkflowSelectionForm(forms.Form):
+ fields = ['workflow']
+
+ empty_permitted = False
+
+ workflow = forms.ChoiceField( choices=(
+ (0, 'Booking'),
+ (1, 'Resource Bundle'),
+ (2, 'Software Configuration')
+ ),
+ label="Choose Workflow",
+ initial='booking',
+ required=True)
+
+class SnapshotHostSelectForm(forms.Form):
+ host = forms.CharField()
+
+class SnapshotMetaForm(forms.Form):
+ name = forms.CharField()
+ description = forms.CharField()
+
+class ConfirmationForm(forms.Form):
+ fields = ['confirm']
+
+ confirm = forms.ChoiceField( choices=(
+ (True, "Confirm"),
+ (False, "Cancel"))
+ )
diff --git a/dashboard/src/workflow/models.py b/dashboard/src/workflow/models.py
new file mode 100644
index 0000000..e862957
--- /dev/null
+++ b/dashboard/src/workflow/models.py
@@ -0,0 +1,508 @@
+##############################################################################
+# 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.contrib.auth.models import User
+from django.db import models
+from django.shortcuts import render
+from django.contrib import messages
+
+import yaml
+import json
+import traceback
+import requests
+
+from workflow.forms import ConfirmationForm
+from api.models import *
+from dashboard.exceptions import *
+from resource_inventory.models import *
+from resource_inventory.resource_manager import ResourceManager
+
+
+class BookingAuthManager():
+ LFN_PROJECTS = ["opnfv"] # TODO
+
+ def parse_url(self, info_url):
+ """
+ will return the PTL in the INFO file on success, or None
+ """
+ try:
+ parts = info_url.split("/")
+ if parts[0].find("http") > -1: # the url include http(s)://
+ parts = parts[2:]
+ if parts[-1] != "INFO.yaml":
+ return None
+ if parts[0] not in ["github.com", "raw.githubusercontent.com"]:
+ return None
+ if parts[1] not in self.LFN_PROJECTS:
+ return None
+ # now to download and parse file
+ if parts[3] == "blob":
+ parts[3] = "raw"
+ url = "https://" + "/".join(parts)
+ info_file = requests.get(url, timeout=15).text
+ info_parsed = yaml.load(info_file)
+ ptl = info_parsed.get('project_lead')
+ if not ptl:
+ return None
+ return ptl
+
+ except Exception as e:
+ return None
+
+
+ def booking_allowed(self, booking, repo):
+ """
+ This is the method that will have to change whenever the booking policy changes in the Infra
+ group / LFN. This is a nice isolation of that administration crap
+ currently checks if the booking uses multiple servers. if it does, then the owner must be a PTL,
+ which is checked using the provided info file
+ """
+ if len(booking.resource.template.getHosts()) < 2:
+ return True #if they only have one server, we dont care
+ if booking.owner.userprofile.booking_privledge:
+ return True # admin override for this user
+ if repo.BOOKING_INFO_FILE not in repo.el:
+ return False # INFO file not provided
+ ptl_info = self.parse_url(repo.BOOKING_INFO_FILE)
+ return ptl_info and ptl_info == booking.owner.userprofile.email_addr
+
+
+
+class WorkflowStep(object):
+
+ template = 'bad_request.html'
+ title = "Generic Step"
+ description = "You were led here by mistake"
+ short_title = "error"
+ metastep = None
+
+ def __init__(self, id, repo=None):
+ self.repo = repo
+ self.id = id
+
+ def get_context(self):
+ context = {}
+ context['step_number'] = self.repo_get('steps')
+ context['active_step'] = self.repo_get('active_step')
+ context['render_correct'] = "true"
+ context['step_title'] = self.title
+ context['description'] = self.description
+ return context
+
+ def render(self, request):
+ self.context = self.get_context()
+ return render(request, self.template, self.context)
+
+ def post_render(self, request):
+ return self.render(request)
+
+ def test_render(self, request):
+ if request.method == "POST":
+ return self.post_render(request)
+ return self.render(request)
+
+ def validate(self, request):
+ pass
+
+ def repo_get(self, key, default=None):
+ return self.repo.get(key, default, self.id)
+
+ def repo_put(self, key, value):
+ return self.repo.put(key, value, self.id)
+
+class Confirmation_Step(WorkflowStep):
+ template = 'workflow/confirm.html'
+ title = "Confirm Changes"
+ description = "Does this all look right?"
+
+ def get_vlan_warning(self):
+ grb = self.repo_get(self.repo.BOOKING_SELECTED_GRB, False)
+ if not grb:
+ return 0
+ vlan_manager = grb.lab.vlan_manager
+ if vlan_manager is None:
+ return 0
+ hosts = grb.getHosts()
+ for host in hosts:
+ for interface in host.generic_interfaces.all():
+ for vlan in interface.vlans.all():
+ if vlan.public:
+ if not vlan_manager.public_vlan_is_available(vlan.vlan_id):
+ return 1
+ else:
+ if not vlan_manager.is_available(vlan.vlan_id):
+ return 1 # There is a problem with these vlans
+ return 0
+
+
+ def get_context(self):
+ context = super(Confirmation_Step, self).get_context()
+ context['form'] = ConfirmationForm()
+ context['confirmation_info'] = yaml.dump(
+ self.repo_get(self.repo.CONFIRMATION),
+ default_flow_style=False
+ ).strip()
+ context['vlan_warning'] = self.get_vlan_warning()
+
+ return context
+
+ def flush_to_db(self):
+ errors = self.repo.make_models()
+ if errors:
+ return errors
+
+ def post_render(self, request):
+ form = ConfirmationForm(request.POST)
+ if form.is_valid():
+ data = form.cleaned_data['confirm']
+ context = self.get_context()
+ if data == "True":
+ context["bypassed"] = "true"
+ errors = self.flush_to_db()
+ if errors:
+ messages.add_message(request, messages.ERROR, "ERROR OCCURRED: " + errors)
+ return render(request, self.template, context)
+ messages.add_message(request, messages.SUCCESS, "Confirmed")
+ return render(request, self.template, context)
+ elif data == "False":
+ context["bypassed"] = "true"
+ messages.add_message(request, messages.SUCCESS, "Canceled")
+ return render(request, self.template, context)
+ else:
+ pass
+
+ else:
+ if "vlan_input" in request.POST:
+ if request.POST.get("vlan_input") == "True":
+ self.translate_vlans()
+ return self.render(request)
+ pass
+
+ def translate_vlans(self):
+ grb = self.repo_get(self.repo.BOOKING_SELECTED_GRB, False)
+ if not grb:
+ return 0
+ vlan_manager = grb.lab.vlan_manager
+ if vlan_manager is None:
+ return 0
+ hosts = grb.getHosts()
+ for host in hosts:
+ for interface in host.generic_interfaces.all():
+ for vlan in interface.vlans.all():
+ if not vlan.public:
+ if not vlan_manager.is_available(vlan.vlan_id):
+ vlan.vlan_id = vlan_manager.get_vlan()
+ vlan.save()
+ else:
+ if not vlan_manager.public_vlan_is_available(vlan.vlan_id):
+ pub_vlan = vlan_manager.get_public_vlan()
+ vlan.vlan_id = pub_vlan.vlan
+ vlan.save()
+
+
+class Workflow():
+
+ steps = []
+ active_index = 0
+
+class Repository():
+
+ EDIT = "editing"
+ MODELS = "models"
+ RESOURCE_SELECT = "resource_select"
+ CONFIRMATION = "confirmation"
+ SELECTED_GRESOURCE_BUNDLE = "selected generic bundle pk"
+ GRESOURCE_BUNDLE_MODELS = "generic_resource_bundle_models"
+ GRESOURCE_BUNDLE_INFO = "generic_resource_bundle_info"
+ BOOKING = "booking"
+ LAB = "lab"
+ GRB_LAST_HOSTLIST = "grb_network_previous_hostlist"
+ BOOKING_FORMS = "booking_forms"
+ SWCONF_HOSTS = "swconf_hosts"
+ SWCONF_SELECTED_GRB = "swconf_selected_grb_pk"
+ BOOKING_SELECTED_GRB = "booking_selected_grb_pk"
+ BOOKING_MODELS = "booking models"
+ CONFIG_MODELS = "configuration bundle models"
+ SESSION_USER = "session owner user account"
+ VALIDATED_MODEL_GRB = "valid grb config model instance in db"
+ VALIDATED_MODEL_CONFIG = "valid config model instance in db"
+ VALIDATED_MODEL_BOOKING = "valid booking model instance in db"
+ VLANS = "a list of vlans"
+ SNAPSHOT_MODELS = "the models for snapshotting"
+ SNAPSHOT_BOOKING_ID = "the booking id for snapshotting"
+ SNAPSHOT_NAME = "the name of the snapshot"
+ SNAPSHOT_DESC = "description of the snapshot"
+ BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking"
+
+
+ def get(self, key, default, id):
+ self.add_get_history(key, id)
+ return self.el.get(key, default)
+
+ def put(self,key,val, id):
+ self.add_put_history(key, id)
+ self.el[key] = val
+
+ def add_get_history(self, key, id):
+ self.add_history(key, id, self.get_history)
+
+ def add_put_history(self, key, id):
+ self.add_history(key, id, self.put_history)
+
+ def add_history(self, key, id, history):
+ if not key in history:
+ history[key] = [id]
+ else:
+ history[key].append(id)
+
+ def make_models(self):
+ if self.SNAPSHOT_MODELS in self.el:
+ errors = self.make_snapshot()
+ if errors:
+ return errors
+ #if GRB WF, create it
+ if self.GRESOURCE_BUNDLE_MODELS in self.el:
+ errors = self.make_generic_resource_bundle()
+ if errors:
+ return errors
+
+ if self.CONFIG_MODELS in self.el:
+ errors = self.make_software_config_bundle()
+ if errors:
+ return errors
+
+ if self.BOOKING_MODELS in self.el:
+ errors = self.make_booking()
+ if errors:
+ return errors
+
+
+ def make_snapshot(self):
+ owner = self.el[self.SESSION_USER]
+ models = self.el[self.SNAPSHOT_MODELS]
+ image = models.get('snapshot', Image())
+ booking_id = self.el.get(self.SNAPSHOT_BOOKING_ID)
+ if not booking_id:
+ return "SNAP, No booking ID provided"
+ booking = Booking.objects.get(pk=booking_id)
+ name = self.el.get(self.SNAPSHOT_NAME)
+ if not name:
+ return "SNAP, no name provided"
+ host = models.get('host')
+ if not host:
+ return "SNAP, no host provided"
+ description = self.el.get(self.SNAPSHOT_DESC, "")
+ image.from_lab = booking.lab
+ image.name = name
+ image.description = description
+ image.public = False
+ image.lab_id = -1
+ image.owner = owner
+ image.host_type = host.profile
+ image.save()
+
+
+ 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 'interfaces' in models:
+ for interface_set in models['interfaces'].values():
+ for interface in interface_set:
+ try:
+ interface.host = interface.host
+ interface.save()
+ except Exception as e:
+ return "GRB, saving interface " + str(interface) + " failed. CODE:0x0019"
+ else:
+ return "GRB, no interface set provided. CODE:0x001a"
+
+ if 'vlans' in models:
+ for resource_name, mapping in models['vlans'].items():
+ for profile_name, vlan_set in mapping.items():
+ interface = GenericInterface.objects.get(
+ profile__name=profile_name,
+ host__resource__name=resource_name,
+ host__resource__bundle=models['bundle']
+ )
+ for vlan in vlan_set:
+ try:
+ vlan.save()
+ interface.vlans.add(vlan)
+ except Exception as e:
+ return "GRB, saving vlan " + str(vlan) + " failed. Exception: " + str(e) + ". CODE:0x0017"
+ else:
+ return "GRB, no vlan set provided. CODE:0x0018"
+
+
+ else:
+ return "GRB no models given. CODE:0x0001"
+
+ self.el[self.VALIDATED_MODEL_GRB] = bundle
+ return False
+
+
+ def make_software_config_bundle(self):
+ owner = self.el[self.SESSION_USER]
+ models = self.el[self.CONFIG_MODELS]
+ if 'bundle' in models:
+ bundle = models['bundle']
+ bundle.bundle = bundle.bundle
+ try:
+ bundle.save()
+ except Exception as e:
+ return "SWC, saving bundle generated exception: " + str(e) + "CODE:0x0007"
+
+ else:
+ return "SWC, no bundle in models. CODE:0x0006"
+ 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
+ try:
+ host_config.save()
+ except Exception as e:
+ return "SWC, saving host configs generated exception: " + str(e) + "CODE:0x0009"
+ else:
+ return "SWC, no host configs in models. CODE:0x0008"
+ if 'opnfv' in models:
+ opnfvconfig = models['opnfv']
+ opnfvconfig.bundle = opnfvconfig.bundle
+ if not opnfvconfig.scenario in opnfvconfig.installer.sup_scenarios.all():
+ return "SWC, scenario not supported by installer. CODE:0x000d"
+ try:
+ opnfvconfig.save()
+ except Exception as e:
+ return "SWC, saving opnfv config generated exception: " + str(e) + "CODE:0x000b"
+ else:
+ pass
+
+ self.el[self.VALIDATED_MODEL_CONFIG] = bundle
+ return False
+
+
+ def make_booking(self):
+ models = self.el[self.BOOKING_MODELS]
+ owner = self.el[self.SESSION_USER]
+
+ if self.BOOKING_SELECTED_GRB in self.el:
+ selected_grb = self.el[self.BOOKING_SELECTED_GRB]
+ else:
+ return "BOOK, no selected resource. CODE:0x000e"
+
+ if not self.reserve_vlans(selected_grb):
+ return "BOOK, vlans not available"
+
+ if 'booking' in models:
+ booking = models['booking']
+ else:
+ return "BOOK, no booking model exists. CODE:0x000f"
+
+ if not booking.start:
+ return "BOOK, booking has no start. CODE:0x0010"
+ if not booking.end:
+ return "BOOK, booking has no end. CODE:0x0011"
+ if booking.end <= booking.start:
+ return "BOOK, end before/same time as start. CODE:0x0012"
+
+ if 'collaborators' in models:
+ collaborators = models['collaborators']
+ else:
+ return "BOOK, collaborators not defined. CODE:0x0013"
+ try:
+ resource_bundle = ResourceManager.getInstance().convertResourceBundle(selected_grb, config=booking.config_bundle)
+ except ResourceAvailabilityException as e:
+ return "BOOK, requested resources are not available. Exception: " + str(e) + " CODE:0x0014"
+ except ModelValidationException as e:
+ return "Error encountered when saving bundle. " + str(e) + " CODE: 0x001b"
+
+ booking.resource = resource_bundle
+ booking.owner = owner
+ booking.config_bundle = booking.config_bundle
+ booking.lab = selected_grb.lab
+
+ is_allowed = BookingAuthManager().booking_allowed(booking, self)
+ if not is_allowed:
+ return "BOOK, you are not allowed to book the requested resources"
+
+ try:
+ booking.save()
+ except Exception as e:
+ return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0015"
+
+ for collaborator in collaborators:
+ booking.collaborators.add(collaborator)
+
+
+ try:
+ JobFactory.makeCompleteJob(booking)
+ except Exception as e:
+ return "BOOK, serializing for api generated exception: " + str(e) + " CODE:0xFFFF"
+
+ try:
+ booking.save()
+ except Exception as e:
+ return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016"
+
+ def reserve_vlans(self, grb):
+ """
+ True is success
+ """
+ vlans = []
+ public_vlan = None
+ vlan_manager = grb.lab.vlan_manager
+ if vlan_manager is None:
+ return True
+ for host in grb.getHosts():
+ for interface in host.generic_interfaces.all():
+ for vlan in interface.vlans.all():
+ if vlan.public:
+ public_vlan = vlan
+ else:
+ vlans.append(vlan.vlan_id)
+
+ try:
+ vlan_manager.reserve_vlans(vlans)
+ vlan_manager.reserve_public_vlan(public_vlan.vlan_id)
+ return True
+ except Exception as e:
+ return False
+
+
+ def __init__(self):
+ self.el = {}
+ self.el[self.CONFIRMATION] = {}
+ self.get_history = {}
+ self.put_history = {}
diff --git a/dashboard/src/workflow/resource_bundle_workflow.py b/dashboard/src/workflow/resource_bundle_workflow.py
new file mode 100644
index 0000000..63ce3bd
--- /dev/null
+++ b/dashboard/src/workflow/resource_bundle_workflow.py
@@ -0,0 +1,427 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.shortcuts import render
+from django.forms import formset_factory
+
+import json
+import re
+from xml.dom import minidom
+
+from workflow.models import WorkflowStep
+from workflow.forms import *
+from resource_inventory.models import *
+from dashboard.exceptions import *
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Define_Hardware(WorkflowStep):
+
+ template = 'resource/steps/define_hardware.html'
+ title = "Define Hardware"
+ description = "Choose the type and amount of machines you want"
+ short_title = "hosts"
+ def get_context(self):
+ context = super(Define_Hardware, self).get_context()
+ selection_data = {"hosts": {}, "labs": {}}
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ hosts = models.get("hosts", [])
+ for host in hosts:
+ profile_id = "host_" + str(host.profile.id)
+ if profile_id not in selection_data['hosts']:
+ selection_data['hosts'][profile_id] = []
+ selection_data['hosts'][profile_id].append({"host_name": host.resource.name, "class": profile_id})
+
+ if models.get("bundle", GenericResourceBundle()).lab:
+ selection_data['labs'] = {"lab_" + str(models.get("bundle").lab.lab_user.id): "true"}
+
+ form = HardwareDefinitionForm(
+ selection_data=selection_data
+ )
+ context['form'] = form
+ return context
+
+ def render(self, request):
+ self.context = self.get_context()
+ return render(request, self.template, self.context)
+
+
+ def update_models(self, data):
+ data = json.loads(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['interfaces'] = {}
+ if "bundle" not in models:
+ models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))
+ host_data = data['hosts']
+ names = {}
+ for host_dict in host_data:
+ id = host_dict['class']
+ # bit of formatting
+ id = int(id.split("_")[-1])
+ profile = HostProfile.objects.get(id=id)
+ # instantiate genericHost and store in repo
+ name = host_dict['host_name']
+ if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
+ raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
+ if name in names:
+ raise NonUniqueHostnameException("All hosts must have unique names")
+ names[name] = True
+ genericResource = GenericResource(bundle=models['bundle'], name=name)
+ genericHost = GenericHost(profile=profile, resource=genericResource)
+ models['hosts'].append(genericHost)
+ for interface_profile in profile.interfaceprofile.all():
+ genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
+ if genericHost.resource.name not in models['interfaces']:
+ models['interfaces'][genericHost.resource.name] = []
+ models['interfaces'][genericHost.resource.name].append(genericInterface)
+
+ # add selected lab to models
+ for lab_dict in data['labs']:
+ if list(lab_dict.values())[0]: # True for lab the user selected
+ lab_user_id = int(list(lab_dict.keys())[0].split("_")[-1])
+ models['bundle'].lab = Lab.objects.get(lab_user__id=lab_user_id)
+ break # if somehow we get two 'true' labs, we only use one
+
+ # return to repo
+ self.repo_put(self.repo.GRESOURCE_BUNDLE_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
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+
+ def post_render(self, request):
+ try:
+ self.form = HardwareDefinitionForm(request.POST)
+ if self.form.is_valid():
+ self.update_models(self.form.cleaned_data)
+ self.update_confirmation()
+ self.metastep.set_valid("Step Completed")
+ else:
+ self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
+ pass
+ except Exception as e:
+ self.metastep.set_invalid(str(e))
+ self.context = self.get_context()
+ return render(request, self.template, self.context)
+
+class Define_Nets(WorkflowStep):
+ template = 'resource/steps/pod_definition.html'
+ title = "Define Networks"
+ description = "Use the tool below to draw the network topology of your POD"
+ short_title = "networking"
+ form = NetworkDefinitionForm
+
+ def get_vlans(self):
+ vlans = self.repo_get(self.repo.VLANS)
+ if vlans:
+ return vlans
+ # try to grab some vlans from lab
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ if "bundle" not in models:
+ return None
+ lab = models['bundle'].lab
+ if lab is None or lab.vlan_manager is None:
+ return None
+ try:
+ vlans = lab.vlan_manager.get_vlan(count=lab.vlan_manager.block_size)
+ self.repo_put(self.repo.VLANS, vlans)
+ return vlans
+ except Exception as e:
+ return None
+
+ def get_context(self):
+ # TODO: render *primarily* on hosts in repo models
+ context = super(Define_Nets, self).get_context()
+ context['form'] = NetworkDefinitionForm()
+ try:
+ context['hosts'] = []
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ vlans = self.get_vlans()
+ if vlans:
+ context['vlans'] = vlans
+ hosts = models.get("hosts", [])
+ hostlist = self.repo_get(self.repo.GRB_LAST_HOSTLIST, None)
+ added_list = []
+ added_dict = {}
+ context['added_hosts'] = []
+ if not hostlist is None:
+ new_hostlist = []
+ for host in models['hosts']:
+ intcount = host.profile.interfaceprofile.count()
+ new_hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
+ context['removed_hosts'] = list(set(hostlist) - set(new_hostlist))
+ added_list = list(set(new_hostlist) - set(hostlist))
+ for hoststr in added_list:
+ key = hoststr.split("*")[0]
+ added_dict[key] = hoststr
+ for generic_host in hosts:
+ host_profile = generic_host.profile
+ host = {}
+ host['id'] = generic_host.resource.name
+ host['interfaces'] = []
+ for iface in host_profile.interfaceprofile.all():
+ host['interfaces'].append({
+ "name": iface.name,
+ "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type})
+ host['value'] = {"name": generic_host.resource.name}
+ host['value']['description'] = generic_host.profile.description
+ context['hosts'].append(json.dumps(host))
+ if host['id'] in added_dict:
+ context['added_hosts'].append(json.dumps(host))
+ bundle = models.get("bundle", False)
+ if bundle and bundle.xml:
+ context['xml'] = bundle.xml
+ else:
+ context['xml'] = False
+
+ except Exception as e:
+ pass
+ return context
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ if 'hosts' in models:
+ hostlist = []
+ for host in models['hosts']:
+ intcount = host.profile.interfaceprofile.count()
+ hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
+ self.repo_put(self.repo.GRB_LAST_HOSTLIST, hostlist)
+ try:
+ xmlData = request.POST.get("xml")
+ self.updateModels(xmlData)
+ # update model with xml
+ self.metastep.set_valid("Networks applied successfully")
+ except Exception as e:
+ self.metastep.set_invalid("An error occurred when applying networks")
+ return self.render(request)
+
+ def updateModels(self, xmlData):
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ models["vlans"] = {}
+ given_hosts, interfaces = self.parseXml(xmlData)
+ vlan_manager = models['bundle'].lab.vlan_manager
+ existing_host_list = models.get("hosts", [])
+ existing_hosts = {} # maps id to host
+ for host in existing_host_list:
+ existing_hosts[host.resource.name] = host
+
+ bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
+
+ for hostid, given_host in given_hosts.items():
+ existing_host = existing_hosts[hostid[5:]]
+
+ for ifaceId in given_host['interfaces']:
+ iface = interfaces[ifaceId]
+ iface_profile = existing_host.profile.interfaceprofile.get(name=iface['profile_name'])
+ if existing_host.resource.name not in models['vlans']:
+ models['vlans'][existing_host.resource.name] = {}
+ models['vlans'][existing_host.resource.name][iface['profile_name']] = []
+ for network in iface['networks']:
+ vlan_id = network['network']['vlan']
+ is_public = network['network']['public']
+ if is_public:
+ vlan_id = vlan_manager.get_public_vlan().vlan
+ vlan = Vlan(vlan_id=vlan_id, tagged=network['tagged'], public=is_public)
+ models['vlans'][existing_host.resource.name][iface['profile_name']].append(vlan)
+ bundle.xml = xmlData
+ self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
+
+ # serialize and deserialize xml from mxGraph
+ def parseXml(self, xmlString):
+ parent_nets = {} # map network ports to networks
+ networks = {} # maps net id to network object
+ hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks
+ interfaces = {} # maps id -> interface
+ xmlDom = minidom.parseString(xmlString)
+ root = xmlDom.documentElement.firstChild
+ connections = []
+ netids = {}
+ untagged_ints = {}
+ for cell in root.childNodes:
+ cellId = cell.getAttribute('id')
+
+ if cell.getAttribute("edge"):
+ #cell is a network connection
+ escaped_json_str = cell.getAttribute("value")
+ json_str = escaped_json_str.replace('&quot;', '"')
+ attributes = json.loads(json_str)
+ tagged = attributes['tagged']
+ interface = None
+ network = None
+ src = cell.getAttribute("source")
+ tgt = cell.getAttribute("target")
+ if src in parent_nets:
+ #src is a network port
+ network = networks[parent_nets[src]]
+ if tgt in untagged_ints and tagged==False:
+ raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
+ interface = interfaces[tgt]
+ untagged_ints[tgt] = True
+ else:
+ network = networks[parent_nets[tgt]]
+ if src in untagged_ints and tagged==False:
+ raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
+ interface = interfaces[src]
+ untagged_ints[src] = True
+ interface['networks'].append({"network": network, "tagged": tagged})
+
+ elif "network" in cellId: # cell is a network
+ escaped_json_str = cell.getAttribute("value")
+ json_str = escaped_json_str.replace('&quot;', '"')
+ net_info = json.loads(json_str)
+ nid = net_info['vlan_id']
+ public = net_info['public']
+ try:
+ int_netid = int(nid)
+ assert public or int_netid > 1, "Net id is 1 or lower"
+ assert int_netid < 4095, "Net id is 4095 or greater"
+ except Exception as e:
+ raise InvalidVlanConfigurationException("VLAN ID is not an integer more than 1 and less than 4095")
+ if nid in netids:
+ raise NetworkExistsException("Non unique network id found")
+ else:
+ pass
+ network = {"name": net_info['name'], "vlan": net_info['vlan_id'], "public": public}
+ netids[net_info['vlan_id']] = True
+ networks[cellId] = network
+
+ elif "host" in cellId: # cell is a host/machine
+ #TODO gather host info
+ cell_json_str = cell.getAttribute("value")
+ cell_json = json.loads(cell_json_str)
+ host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']}
+ hosts[cellId] = host
+
+ elif cell.hasAttribute("parent"):
+ parentId = cell.getAttribute('parent')
+ if "network" in parentId:
+ parent_nets[cellId] = parentId
+ elif "host" in parentId:
+ #TODO gather iface info
+ cell_json_str = cell.getAttribute("value")
+ cell_json = json.loads(cell_json_str)
+ iface = {"name": cellId, "networks": [], "profile_name": cell_json['name']}
+ hosts[parentId]['interfaces'].append(cellId)
+ interfaces[cellId] = iface
+ return hosts, interfaces
+
+
+class Resource_Meta_Info(WorkflowStep):
+ template = 'resource/steps/meta_info.html'
+ title = "Extra Info"
+ description = "Please fill out the rest of the information about your resource"
+ short_title = "pod info"
+
+ 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:
+ name = bundle.name
+ desc = bundle.description
+ context['form'] = ResourceMetaForm(initial={"bundle_name": name, "bundle_description": desc})
+ return context
+
+ def post_render(self, request):
+ form = ResourceMetaForm(request.POST)
+ if form.is_valid():
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ name = form.cleaned_data['bundle_name']
+ desc = form.cleaned_data['bundle_description']
+ bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
+ bundle.name = name
+ bundle.description = desc
+ models['bundle'] = bundle
+ self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
+ confirm = self.repo_get(self.repo.CONFIRMATION)
+ if "resource" not in confirm:
+ confirm['resource'] = {}
+ confirm_info = confirm['resource']
+ confirm_info["name"] = name
+ tmp = desc
+ if len(tmp) > 60:
+ tmp = tmp[:60] + "..."
+ confirm_info["description"] = tmp
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+ self.metastep.set_valid("Step Completed")
+
+ else:
+ self.metastep.set_invalid("Please correct the fields highlighted in red to continue")
+ pass
+ return self.render(request)
+
+
+class Host_Meta_Info(WorkflowStep):
+ template = "resource/steps/host_info.html"
+ title = "Host Info"
+ description = "We need a little bit of information about your chosen machines"
+ short_title = "host info"
+
+ def __init__(self, *args, **kwargs):
+ super(Host_Meta_Info, self).__init__(*args, **kwargs)
+ self.formset = formset_factory(GenericHostMetaForm, extra=0)
+
+ def get_context(self):
+ context = super(Host_Meta_Info, self).get_context()
+ GenericHostFormset = self.formset
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ initial_data = []
+ if "hosts" not in models:
+ context['error'] = "Please go back and select your hosts"
+ else:
+ for host in models['hosts']:
+ profile = host.profile.name
+ name = host.resource.name
+ if not name:
+ name = ""
+ initial_data.append({"host_profile": profile, "host_name": name})
+ context['formset'] = GenericHostFormset(initial=initial_data)
+ return context
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ if 'hosts' not in models:
+ models['hosts'] = []
+ hosts = models['hosts']
+ i = 0
+ confirm_hosts = []
+ GenericHostFormset = self.formset
+ formset = GenericHostFormset(request.POST)
+ if formset.is_valid():
+ for form in formset:
+ host = hosts[i]
+ host.resource.name = form.cleaned_data['host_name']
+ i += 1
+ confirm_hosts.append({"name": host.resource.name, "profile": host.profile.name})
+ models['hosts'] = hosts
+ self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ if "resource" not in confirm:
+ confirm['resource'] = {}
+ confirm['resource']['hosts'] = confirm_hosts
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+ else:
+ pass
+ return self.render(request)
diff --git a/dashboard/src/workflow/snapshot_workflow.py b/dashboard/src/workflow/snapshot_workflow.py
new file mode 100644
index 0000000..9d4b880
--- /dev/null
+++ b/dashboard/src/workflow/snapshot_workflow.py
@@ -0,0 +1,111 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import datetime
+import json
+
+from resource_inventory.models import *
+from workflow.models import *
+from workflow.forms import *
+
+class Select_Host_Step(WorkflowStep):
+ template = "snapshot_workflow/steps/select_host.html"
+ title = "Select Host"
+ description = "Choose which machine you want to snapshot"
+ short_title = "host"
+
+ def get_context(self):
+ context = super(Select_Host_Step, self).get_context()
+ context['form'] = SnapshotHostSelectForm()
+ booking_hosts = {}
+ now = datetime.datetime.now()
+ user = self.repo_get(self.repo.SESSION_USER)
+ bookings = Booking.objects.filter(start__lt=now, end__gt=now, owner=user)
+ for booking in bookings:
+ booking_hosts[booking.id] = {}
+ booking_hosts[booking.id]['purpose'] = booking.purpose
+ booking_hosts[booking.id]['start'] = booking.start.strftime("%Y-%m-%d")
+ booking_hosts[booking.id]['end'] = booking.end.strftime("%Y-%m-%d")
+ booking_hosts[booking.id]['hosts'] = []
+ for genericHost in booking.resource.template.getHosts():
+ booking_hosts[booking.id]['hosts'].append({"name": genericHost.resource.name})
+
+
+ context['booking_hosts'] = booking_hosts
+
+ chosen_host = self.repo_get(self.repo.SNAPSHOT_MODELS, {}).get("host")
+ if chosen_host:
+ chosen = {}
+ chosen['booking_id'] = self.repo_get(self.repo.SNAPSHOT_BOOKING_ID)
+ chosen['hostname'] = chosen_host.template.resource.name
+ context['chosen'] = chosen
+ return context
+
+ def post_render(self, request):
+ host_data = request.POST.get("host")
+ if not host_data:
+ self.metastep.set_invalid("Please select a host")
+ return self.render(request)
+ host = json.loads(host_data)
+ if 'name' not in host or 'booking' not in host:
+ self.metastep.set_invalid("Invalid host selected")
+ return self.render(request)
+ name = host['name']
+ booking_id = host['booking']
+ booking = Booking.objects.get(pk=booking_id)
+ host = Host.objects.get(bundle=booking.resource, template__resource__name=name)
+ models = self.repo_get(self.repo.SNAPSHOT_MODELS, {})
+ if "host" not in models:
+ models['host'] = host
+ if 'snapshot' not in models:
+ models['snapshot'] = Image()
+ self.repo_put(self.repo.SNAPSHOT_MODELS, models)
+ self.repo_put(self.repo.SNAPSHOT_BOOKING_ID, booking_id)
+
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ snap_confirm = confirm.get("snapshot", {})
+ snap_confirm['host'] = name
+ confirm['snapshot'] = snap_confirm
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+ self.metastep.set_valid("Success")
+ return self.render(request)
+
+class Image_Meta_Step(WorkflowStep):
+ template = "snapshot_workflow/steps/meta.html"
+ title = "Additional Information"
+ description = "We need some more info"
+ short_title = "info"
+
+ def get_context(self):
+ context = super(Image_Meta_Step, self).get_context()
+ context['form'] = SnapshotMetaForm()
+ return context
+
+
+ def post_render(self, request):
+ form = SnapshotMetaForm(request.POST)
+ if form.is_valid():
+ name = form.cleaned_data['name']
+ self.repo_put(self.repo.SNAPSHOT_NAME, name)
+ description = form.cleaned_data['description']
+ self.repo_put(self.repo.SNAPSHOT_DESC, description)
+
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ snap_confirm = confirm.get("snapshot", {})
+ snap_confirm['name'] = name
+ snap_confirm['description'] = description
+ confirm['snapshot'] = snap_confirm
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ self.metastep.set_valid("Success")
+ else:
+ self.metastep.set_invalid("Please Fill out the Form")
+
+ return self.render(request)
diff --git a/dashboard/src/workflow/sw_bundle_workflow.py b/dashboard/src/workflow/sw_bundle_workflow.py
new file mode 100644
index 0000000..0e9be95
--- /dev/null
+++ b/dashboard/src/workflow/sw_bundle_workflow.py
@@ -0,0 +1,238 @@
+##############################################################################
+# 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, modelformset_factory
+
+from workflow.models import WorkflowStep
+from workflow.forms import SoftwareConfigurationForm, HostSoftwareDefinitionForm
+from workflow.booking_workflow import Resource_Select
+from resource_inventory.models import *
+
+
+#resource selection step is reused from Booking workflow
+
+#TODO: change this: too hacky, just for presentation
+
+class SWConf_Resource_Select(Resource_Select):
+ def __init__(self, *args, **kwargs):
+ super(SWConf_Resource_Select, self).__init__(*args, **kwargs)
+ self.repo_key = self.repo.SWCONF_SELECTED_GRB
+ self.confirm_key = "configuration"
+
+ def get_default_entry(self):
+ booking_grb = self.repo_get(self.repo.BOOKING_SELECTED_GRB)
+ if booking_grb:
+ return booking_grb
+ created_grb = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("bundle", None)
+ return created_grb
+
+ def post_render(self, request):
+ response = super(SWConf_Resource_Select, self).post_render(request)
+ models = self.repo_get(self.repo.CONFIG_MODELS, {})
+ bundle = models.get("bundle", ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER)))
+ bundle.bundle = self.repo_get(self.repo_key) # super put grb here
+ models['bundle'] = bundle
+ self.repo_put(self.repo.CONFIG_MODELS, models)
+ return response
+
+class 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 create_hostformset(self, hostlist):
+ hosts_initial = []
+ host_configs = self.repo_get(self.repo.CONFIG_MODELS, {}).get("host_configs", False)
+ if host_configs:
+ for config in host_configs:
+ host_initial = {'host_id': config.host.id, 'host_name': config.host.resource.name}
+ host_initial['role'] = config.opnfvRole
+ host_initial['image'] = config.image
+ hosts_initial.append(host_initial)
+
+ else:
+ for host in hostlist:
+ host_initial = {'host_id': host.id, 'host_name': host.resource.name}
+
+ hosts_initial.append(host_initial)
+
+ HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0)
+ host_formset = HostFormset(initial=hosts_initial)
+
+ filter_data = {}
+ user = self.repo_get(self.repo.SESSION_USER)
+ i=0;
+ for host_data in hosts_initial:
+ host = GenericHost.objects.get(pk=host_data['host_id'])
+ excluded_images = Image.objects.exclude(owner=user).exclude(public=True)
+ excluded_images = excluded_images | Image.objects.exclude(host_type=host.profile)
+ lab = self.repo_get(self.repo.SWCONF_SELECTED_GRB).lab
+ excluded_images = excluded_images | Image.objects.exclude(from_lab=lab)
+ filter_data["id_form-" + str(i) + "-image"] = []
+ for image in excluded_images:
+ filter_data["id_form-" + str(i) + "-image"].append(image.name)
+ i += 1
+
+ return host_formset, filter_data
+
+ def get_host_list(self, grb=None):
+ if grb is None:
+ grb = self.repo_get(self.repo.SWCONF_SELECTED_GRB, False)
+ if not grb:
+ return []
+ if grb.id:
+ return GenericHost.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.SWCONF_SELECTED_GRB, False)
+
+ if grb:
+ context["grb"] = grb
+ formset, filter_data = self.create_hostformset(self.get_host_list(grb))
+ context["formset"] = formset
+ context["filter_data"] = filter_data
+ else:
+ context["error"] = "Please select a resource first"
+ self.metastep.set_invalid("Step requires information that is not yet provided by previous step")
+
+ return context
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.CONFIG_MODELS, {})
+ if "bundle" not in models:
+ models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER))
+
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+
+ HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0)
+ formset = HostFormset(request.POST)
+ hosts = self.get_host_list()
+ if formset.is_valid():
+ models['host_configs'] = []
+ i = 0
+ confirm_hosts = []
+ for form in formset:
+ host = hosts[i]
+ i += 1
+ image = form.cleaned_data['image']
+ # checks image compatability
+ grb = self.repo_get(self.repo.SWCONF_SELECTED_GRB)
+ lab = None
+ if grb:
+ lab = grb.lab
+ try:
+ owner = self.repo_get(self.repo.SESSION_USER)
+ q = Image.objects.filter(owner=owner) | Image.objects.filter(public=True)
+ q.filter(host_type=host.profile)
+ q.filter(from_lab=lab)
+ q.get(id=image.id) # will throw exception if image is not in q
+ except:
+ self.metastep.set_invalid("Image " + image.name + " is not compatible with host " + host.resource.name)
+ role = form.cleaned_data['role']
+ bundle = models['bundle']
+ hostConfig = HostConfiguration(
+ host=host,
+ image=image,
+ bundle=bundle,
+ opnfvRole=role
+ )
+ models['host_configs'].append(hostConfig)
+ confirm_host = {"name": host.resource.name, "image": image.name, "role": role.name}
+ confirm_hosts.append(confirm_host)
+
+ 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.metastep.set_valid("Completed")
+ else:
+ self.metastep.set_invalid("Please complete all fields")
+
+ return self.render(request)
+
+class Config_Software(WorkflowStep):
+ template = 'config_bundle/steps/config_software.html'
+ form = SoftwareConfigurationForm
+ context = {'workspace_form':form}
+ 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
+ opnfv = models.get("opnfv", False)
+ if opnfv:
+ initial['installer'] = opnfv.installer
+ initial['scenario'] = opnfv.scenario
+ else:
+ initial['opnfv'] = False
+ supported = {}
+ for installer in Installer.objects.all():
+ supported[str(installer)] = []
+ for scenario in installer.sup_scenarios.all():
+ supported[str(installer)].append(str(scenario))
+
+ context["form"] = SoftwareConfigurationForm(initial=initial)
+ context['supported'] = supported
+
+ return context
+
+ def post_render(self, request):
+ try:
+ models = self.repo_get(self.repo.CONFIG_MODELS, {})
+ if "bundle" not in models:
+ models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER))
+
+
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ if "configuration" not in confirm:
+ confirm['configuration'] = {}
+
+ form = self.form(request.POST)
+ if form.is_valid():
+ models['bundle'].name = form.cleaned_data['name']
+ models['bundle'].description = form.cleaned_data['description']
+ if form.cleaned_data['opnfv']:
+ installer = form.cleaned_data['installer']
+ scenario = form.cleaned_data['scenario']
+ opnfv = OPNFVConfig(
+ bundle=models['bundle'],
+ installer=installer,
+ scenario=scenario
+ )
+ models['opnfv'] = opnfv
+ confirm['configuration']['installer'] = form.cleaned_data['installer'].name
+ confirm['configuration']['scenario'] = form.cleaned_data['scenario'].name
+
+ confirm['configuration']['name'] = form.cleaned_data['name']
+ confirm['configuration']['description'] = form.cleaned_data['description']
+ self.metastep.set_valid("Complete")
+ else:
+ self.metastep.set_invalid("Please correct the errors shown below")
+
+ self.repo_put(self.repo.CONFIG_MODELS, models)
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ except Exception as e:
+ pass
+ return self.render(request)
diff --git a/dashboard/src/workflow/tests/__init__.py b/dashboard/src/workflow/tests/__init__.py
new file mode 100644
index 0000000..4f0437d
--- /dev/null
+++ b/dashboard/src/workflow/tests/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
diff --git a/dashboard/src/workflow/tests/constants.py b/dashboard/src/workflow/tests/constants.py
new file mode 100644
index 0000000..f94a949
--- /dev/null
+++ b/dashboard/src/workflow/tests/constants.py
@@ -0,0 +1,198 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+POD_XML = """<mxGraphModel>
+<root>
+<mxCell id="0"/>
+<mxCell id="1" parent="0"/>
+<mxCell id="host_null" value="Test profile 0" style="editable=0" vertex="1" connectable="0" parent="1">
+<mxGeometry x="75" y="150" width="110" height="90" as="geometry"/>
+</mxCell>
+<mxCell id="2" value="eno0" style="fillColor=blue;editable=0" vertex="1" parent="host_null">
+<mxGeometry x="90" y="5" width="20" height="20" as="geometry"/>
+</mxCell>
+<mxCell id="3" value="eno1" style="fillColor=blue;editable=0" vertex="1" parent="host_null">
+<mxGeometry x="90" y="30" width="20" height="20" as="geometry"/>
+</mxCell>
+<mxCell id="4" value="eno2" style="fillColor=blue;editable=0" vertex="1" parent="host_null">
+<mxGeometry x="90" y="55" width="20" height="20" as="geometry"/>
+</mxCell>
+<mxCell id="5" value="Test profile 3" style="editable=0" vertex="1" connectable="0" parent="1">
+<mxGeometry x="75" y="290" width="110" height="90" as="geometry"/>
+</mxCell>
+<mxCell id="6" value="eno0" style="fillColor=blue;editable=0" vertex="1" parent="5">
+<mxGeometry x="90" y="5" width="20" height="20" as="geometry"/>
+</mxCell>
+<mxCell id="7" value="eno1" style="fillColor=blue;editable=0" vertex="1" parent="5">
+<mxGeometry x="90" y="30" width="20" height="20" as="geometry"/>
+</mxCell>
+<mxCell id="8" value="eno2" style="fillColor=blue;editable=0" vertex="1" parent="5">
+<mxGeometry x="90" y="55" width="20" height="20" as="geometry"/>
+</mxCell>
+<mxCell id="network_0" value="{&quot;vlan_id&quot;:&quot;500&quot;,&quot;name&quot;:&quot;net&quot;}" style="fillColor=red" vertex="1" parent="1">
+<mxGeometry x="400" y="-20" width="10" height="2000" as="geometry"/>
+</mxCell>
+<mxCell id="9" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="10" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.02" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="11" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.04" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="12" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.06" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="13" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.08" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="14" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.1" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="15" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.12" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="16" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.14" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="17" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.16" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="18" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.18" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="19" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.2" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="20" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.22" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="21" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.24" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="22" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.26" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="23" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.28" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="24" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.3" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="25" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.32" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="26" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.34" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="27" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.36" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="28" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.38" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="29" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.4" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="30" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.42" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="31" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.44" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="32" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.46" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="33" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.48" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="34" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.5" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="35" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.52" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="36" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.54" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="37" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.56" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="38" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.58" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="39" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.6" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="40" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.62" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="41" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.64" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="42" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.66" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="43" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.68" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="44" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.7000000000000001" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="45" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.72" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="46" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.74" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="47" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.76" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="48" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.78" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="49" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.8" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="50" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.8200000000000001" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="51" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.84" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="52" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.86" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="53" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.88" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="54" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.9" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="55" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.92" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="56" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.9400000000000001" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="57" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.96" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="58" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0">
+<mxGeometry y="0.98" width="10" height="40" relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="59" value="{&quot;tagged&quot;:true}" style="strokeColor=red" edge="1" parent="1" source="2" target="13">
+<mxGeometry relative="1" as="geometry"/>
+</mxCell>
+<mxCell id="60" value="{&quot;tagged&quot;:false}" style="strokeColor=red" edge="1" parent="1" source="7" target="17">
+<mxGeometry relative="1" as="geometry"/>
+</mxCell>
+</root>
+</mxGraphModel>
+"""
diff --git a/dashboard/src/workflow/tests/test_steps.py b/dashboard/src/workflow/tests/test_steps.py
new file mode 100644
index 0000000..602d3dd
--- /dev/null
+++ b/dashboard/src/workflow/tests/test_steps.py
@@ -0,0 +1,271 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+from django.test import TestCase, client
+from dashboard.populate_db import Populator
+from workflow.tests import constants
+from workflow.workflow_factory import WorkflowFactory
+from workflow.models import Repository
+from workflow.resource_bundle_workflow import *
+from workflow.sw_bundle_workflow import *
+from workflow.booking_workflow import *
+from django.http import QueryDict, HttpRequest
+from django.contrib.auth.models import User
+from django.core.management import call_command
+from resource_inventory.models import *
+
+
+class BaseStepTestCase(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ Populator().populate()
+
+ def makeRepo(self):
+ repo = Repository()
+ repo.el[repo.SESSION_USER] = User.objects.filter(username="user 1").first()
+ return repo
+
+ def step_test(self, step_type, data):
+ step = WorkflowFactory().make_step(step_type, self.makeRepo())
+ formData = QueryDict(mutable=True)
+ formData.update(data)
+ request = HttpRequest()
+ request.POST = formData
+ response = step.post_render(request)
+ context = step.get_context()
+ return response, context
+
+
+class BookingResourceSelectTestCase(BaseStepTestCase):
+
+ def test_step_with_good_data(self):
+ grb_model = GenericResourceBundle.objects.filter(owner__username="user 1").first()
+ grb = [{"small_name": grb_model.name, "expanded_name": "user 1", "id": grb_model.id, "string": ""}]
+ grb = str(grb).replace("'", '"')
+ data = {"generic_resource_bundle": grb }
+ response, context = self.step_test(Booking_Resource_Select, data)
+ self.assertTrue(True)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {}
+ response, context = self.step_test(Booking_Resource_Select, data)
+
+ def test_step_with_empty_data(self):
+ data = {}
+ response, context = self.step_test(SWConfig_Select, data)
+
+class SoftwareConfigSelectTestCase(BaseStepTestCase):
+
+ def test_step_with_good_data(self):
+ config_model = ConfigBundle.objects.filter(owner__username="user 1").first()
+ config = [{"expanded_name":"user 1", "small_name":config_model.name, "id":config_model.id, "string":""}]
+ config = str(config).replace("'", '"')
+ data = {"software_bundle": config}
+ response, context = self.step_test(SWConfig_Select, data)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {}
+ response, context = self.step_test(SWConfig_Select, data)
+
+ def test_step_with_empty_data(self):
+ data = {}
+ response, context = self.step_test(SWConfig_Select, data)
+
+class BookingMetaTestCase(BaseStepTestCase):
+
+ def test_step_with_good_data(self):
+ data = {"length": 7, "project": "LaaS", "purpose": "testing"}
+ user2 = User.objects.get(username="user 2")
+ john = User.objects.get(username="johnsmith")
+ users = [
+ {"expanded_name":"", "id":user2.id, "small_name":user2.username, "string":user2.email},
+ {"expanded_name":"", "id":john.id, "small_name":john.username, "string":john.email}
+ ]
+ users = str(users).replace("'", '"')
+ data['users'] = users
+ response, context = self.step_test(Booking_Meta, data)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {}
+ response, context = self.step_test(Booking_Meta, data)
+
+ def test_step_with_empty_data(self):
+ data = {}
+ response, context = self.step_test(Booking_Meta, data)
+
+
+class DefineHardwareTestCase(BaseStepTestCase):
+
+ def test_step_with_good_data(self):
+ hosts = {"host_4": 1, "host_1": 1}
+ labs = {"lab_1":"true"}
+ data = {"hosts": hosts, "labs": labs}
+ response, context = self.step_test(Define_Hardware, data)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {}
+ response, context = self.step_test(Define_Hardware, data)
+
+ def test_step_with_empty_data(self):
+ data = {}
+ response, context = self.step_test(Define_Hardware, data)
+
+
+class HostMetaInfoTestCase(BaseStepTestCase):
+
+ def makeRepo(self):
+ """
+ override to provide step with needed host info
+ """
+ repo = super(HostMetaInfoTestCase, self).makeRepo()
+ # get models
+ models = {}
+ models['bundle'] = GenericResourceBundle()
+ # make generic hosts
+ gres1 = GenericResource(bundle=models['bundle'])
+ prof1 = HostProfile.objects.get(name="Test profile 0")
+ ghost1 = GenericHost(profile=prof1, resource=gres1)
+
+ gres2 = GenericResource(bundle=models['bundle'])
+ prof2 = HostProfile.objects.get(name="Test profile 3")
+ ghost2 = GenericHost(profile=prof2, resource=gres2)
+ models['hosts'] = [ghost1, ghost2]
+ repo.el[repo.GRESOURCE_BUNDLE_MODELS] = models
+ return repo
+
+ def test_step_with_good_data(self):
+ data = {"form-INITIAL_FORMS": 2, "form-MAX_NUM_FORMS": 1000}
+ data["form-MIN_NUM_FORMS"] = 0
+ data["form-TOTAL_FORMS"] = 2
+ data['form-0-host_name'] = "first host"
+ data['form-1-host_name'] = "second host"
+ response, context = self.step_test(Host_Meta_Info, data)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {"form-INITIAL_FORMS": 0, "form-MAX_NUM_FORMS": 1000}
+ data["form-MIN_NUM_FORMS"] = 0
+ data["form-TOTAL_FORMS"] = 0
+ response, context = self.step_test(Host_Meta_Info, data)
+
+ def test_step_with_empty_data(self):
+ data = {"form-INITIAL_FORMS": 0, "form-MAX_NUM_FORMS": 1000}
+ data["form-MIN_NUM_FORMS"] = 0
+ data["form-TOTAL_FORMS"] = 0
+ response, context = self.step_test(Host_Meta_Info, data)
+
+
+class DefineNetsTestCase(BaseStepTestCase):
+
+ def test_step_with_good_data(self):
+ xml = constants.POD_XML
+ data = {"xml": xml}
+ response, context = self.step_test(Define_Nets, data)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {}
+ response, context = self.step_test(Define_Nets, data)
+
+ def test_step_with_empty_data(self):
+ data = {}
+ response, context = self.step_test(Define_Nets, data)
+
+
+class ResourceMetaInfoTestCase(BaseStepTestCase):
+
+ def test_step_with_good_data(self):
+ data = {"bundle_description": "description", "bundle_name": "my testing bundle"}
+ response, context = self.step_test(Resource_Meta_Info, data)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {}
+ response, context = self.step_test(Resource_Meta_Info, data)
+
+ def test_step_with_empty_data(self):
+ data = {}
+ response, context = self.step_test(Resource_Meta_Info, data)
+
+
+class SWConfResourceSelectTestCase(BaseStepTestCase):
+
+ def test_step_with_good_data(self):
+ grb_model = GenericResourceBundle.objects.filter(owner__username="user 1").first()
+ grb = [{"small_name": grb_model.name, "expanded_name": "user 1", "id": grb_model.id, "string": ""}]
+ grb = str(grb).replace("'", '"')
+ data = {"generic_resource_bundle": grb }
+ response, context = self.step_test(SWConf_Resource_Select, data)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {}
+ response, context = self.step_test(SWConf_Resource_Select, data)
+
+ def test_step_with_empty_data(self):
+ data = {}
+ response, context = self.step_test(SWConf_Resource_Select, data)
+
+
+class DefineSoftwareTestCase(BaseStepTestCase):
+
+ def makeRepo(self):
+ """
+ put selected grb in repo for step
+ """
+ repo = super(DefineSoftwareTestCase, self).makeRepo()
+ grb = GenericResourceBundle.objects.filter(owner__username="user 1").first()
+ repo.el[repo.SWCONF_SELECTED_GRB] = grb
+ return repo
+
+
+ def test_step_with_good_data(self):
+ data = {"form-INITIAL_FORMS": 3, "form-MAX_NUM_FORMS": 1000}
+ data["form-MIN_NUM_FORMS"] = 0
+ data["form-TOTAL_FORMS"] = 3
+ an_image_id = Image.objects.get(name="a host image").id
+ another_image_id = Image.objects.get(name="another host image").id
+ control = OPNFVRole.objects.get(name="Controller")
+ compute = OPNFVRole.objects.get(name="Compute")
+ jumphost = OPNFVRole.objects.get(name="Jumphost")
+ data['form-0-image'] = an_image_id
+ data['form-1-image'] = an_image_id
+ data['form-2-image'] = another_image_id
+ data['form-0-role'] = compute.id
+ data['form-1-role'] = control.id
+ data['form-2-role'] = jumphost.id
+ response, context = self.step_test(Define_Software, data)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {"form-INITIAL_FORMS": 0, "form-MAX_NUM_FORMS": 1000}
+ data["form-MIN_NUM_FORMS"] = 0
+ data["form-TOTAL_FORMS"] = 0
+ response, context = self.step_test(Define_Software, data)
+
+ def test_step_with_empty_data(self):
+ data = {"form-INITIAL_FORMS": 0, "form-MAX_NUM_FORMS": 1000}
+ data["form-MIN_NUM_FORMS"] = 0
+ data["form-TOTAL_FORMS"] = 0
+ response, context = self.step_test(Define_Software, data)
+
+
+class ConfigSoftwareTestCase(BaseStepTestCase):
+
+ def test_step_with_good_data(self):
+ data = {"description": "description", "name": "namey"}
+ installer = Installer.objects.get(name="Fuel")
+ scenario = Scenario.objects.get(name="os-nosdn-nofeature-noha")
+ data['installer'] = installer.id
+ data['scenario'] = scenario.id
+ response, context = self.step_test(Config_Software, data)
+
+ def test_step_with_bad_data(self): # TODO
+ data = {}
+ response, context = self.step_test(Config_Software, data)
+
+ def test_step_with_empty_data(self):
+ data = {}
+ response, context = self.step_test(Config_Software, data)
+
diff --git a/dashboard/src/workflow/tests/test_steps_render.py b/dashboard/src/workflow/tests/test_steps_render.py
new file mode 100644
index 0000000..3da3b3d
--- /dev/null
+++ b/dashboard/src/workflow/tests/test_steps_render.py
@@ -0,0 +1,36 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+from django.test import TestCase, Client
+
+class SuperViewTestCase(TestCase):
+ url = "/"
+ client = Client()
+
+ def test_get(self):
+ response = self.client.get(self.url)
+ self.assertLess(response.status_code, 300)
+
+
+class DefineHardwareViewTestCase(SuperViewTestCase):
+ url = "/wf/workflow/step/define_hardware"
+
+class DefineNetworkViewTestCase(SuperViewTestCase):
+ url = "/wf/workflow/step/define_net"
+
+class ResourceMetaViewTestCase(SuperViewTestCase):
+ url = "/wf/workflow/step/resource_meta"
+
+class BookingMetaViewTestCase(SuperViewTestCase):
+ url = "/wf/workflow/step/booking_meta"
+
+class SoftwareSelectViewTestCase(SuperViewTestCase):
+ url = "/wf/workflow/step/software_select"
+
+class ResourceSelectViewTestCase(SuperViewTestCase):
+ url = "/wf/workflow/step/resource_select"
diff --git a/dashboard/src/workflow/tests/test_workflows.py b/dashboard/src/workflow/tests/test_workflows.py
new file mode 100644
index 0000000..71d0144
--- /dev/null
+++ b/dashboard/src/workflow/tests/test_workflows.py
@@ -0,0 +1,96 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+from django.test import TestCase, client
+from workflow.workflow_factory import WorkflowFactory
+from dashboard.populate_db import Populator
+from resource_inventory.models import *
+
+
+"""
+To start a workflow:
+ POST to /wf/workflow {"add": <wf_type_int>
+
+ types:
+ 0 - Booking
+ 1 - Resource
+ 2 - Config
+
+To remove a workflow:
+ POST to /wf/workflow {"cancel": ""}
+"""
+
+class WorkflowTestCase(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ Populator().populate()
+
+ def setUp(self):
+ self.clear_workflow()
+ self.create_workflow(self.wf_type)
+
+ def create_workflow(self, wf_type):
+ self.clear_workflow()
+
+ # creates workflow on backend
+ self.client.post("/", {"create": int(wf_type)}) # TODO: verify content type, etc
+
+ def clear_workflow(self):
+ session = self.client.session
+ for k in session.keys():
+ del session[k]
+ session.save()
+
+ def render_steps(self):
+ """
+ retrieves each step individually at /wf/workflow/step=<index>
+ """
+ for i in range(self.step_count):
+ # renders the step itself, not in an iframe
+ exception = None
+ try:
+ response = self.client.get("/wf/workflow/", {"step": str(i)})
+ self.assertLess(response.status_code, 300)
+ except Exception as e:
+ exception = e
+
+ self.assertIsNone(exception)
+
+class BookingWorkflowTestCase(WorkflowTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(BookingWorkflowTestCase, cls).setUpClass()
+ cls.step_count = len(WorkflowFactory.booking_steps)
+ cls.wf_type = 0
+
+ def test_steps_render(self):
+ super(BookingWorkflowTestCase, self).render_steps()
+
+class ResourceWorkflowTestCase(WorkflowTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(ResourceWorkflowTestCase, cls).setUpClass()
+ cls.step_count = len(WorkflowFactory.resource_steps)
+ cls.wf_type = 1
+
+ def test_steps_render(self):
+ super(ResourceWorkflowTestCase, self).render_steps()
+
+class ConfigWorkflowTestCase(WorkflowTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(ConfigWorkflowTestCase, cls).setUpClass()
+ cls.step_count = len(WorkflowFactory.config_steps)
+ cls.wf_type = 2
+
+ def test_steps_render(self):
+ super(ConfigWorkflowTestCase, self).render_steps()
diff --git a/dashboard/src/workflow/urls.py b/dashboard/src/workflow/urls.py
new file mode 100644
index 0000000..c7f8acb
--- /dev/null
+++ b/dashboard/src/workflow/urls.py
@@ -0,0 +1,34 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.conf.urls import url, include
+from django.conf import settings
+
+from workflow.views import *
+from workflow.models import *
+from workflow.resource_bundle_workflow import *
+from workflow.booking_workflow import *
+
+app_name = 'workflow'
+urlpatterns = [
+
+ url(r'^workflow/$', step_view, name='workflow'),
+ url(r'^workflow/finish/$', delete_session, name='delete_session'),
+ url(r'^manager/$', manager_view, name='manager'),
+ url(r'^$', viewport_view, name='viewport')
+]
+
+if settings.TESTING:
+ urlpatterns.append(url(r'^workflow/step/define_hardware$', Define_Hardware("", Repository()).test_render))
+ urlpatterns.append(url(r'^workflow/step/define_net$', Define_Nets("", Repository()).test_render))
+ urlpatterns.append(url(r'^workflow/step/resource_meta$', Resource_Meta_Info("", Repository()).test_render))
+ urlpatterns.append(url(r'^workflow/step/booking_meta$', Booking_Meta("", Repository()).test_render))
+ urlpatterns.append(url(r'^workflow/step/software_select$', SWConfig_Select("", Repository()).test_render))
+ urlpatterns.append(url(r'^workflow/step/resource_select$', Resource_Select("", Repository()).test_render))
diff --git a/dashboard/src/workflow/views.py b/dashboard/src/workflow/views.py
new file mode 100644
index 0000000..85e4eac
--- /dev/null
+++ b/dashboard/src/workflow/views.py
@@ -0,0 +1,108 @@
+##############################################################################
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.http import HttpResponse, HttpRequest, HttpResponseGone
+from django.urls import reverse
+from django.shortcuts import render, redirect
+from django import forms
+
+import uuid
+
+from workflow.forms import *
+from workflow.workflow_manager import *
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+def attempt_auth(request):
+ try:
+ manager = ManagerTracker.managers[request.session['manager_session']]
+
+ return manager
+
+ except KeyError:
+ return None
+
+def delete_session(request):
+ try:
+ manager = ManagerTracker.managers[request.session['manager_session']]
+ del ManagerTracker.managers[request.session['manager_session']]
+ return HttpResponse('')
+ except KeyError:
+ return None
+
+def step_view(request):
+ manager = attempt_auth(request)
+ if not manager:
+ #no manager found, redirect to "lost" page
+ return no_workflow(request)
+ if request.GET.get('step') is not None:
+ manager.goto(int(request.GET.get('step')))
+ return manager.render(request)
+
+def manager_view(request):
+ manager = attempt_auth(request)
+
+ if not manager:
+ return HttpResponseGone("No session found that relates to current request")
+
+ if request.method == 'GET': #no need for this statement if only intercepting post requests
+
+ #return general context for viewport page
+ return manager.status(request)
+
+ if request.method == 'POST':
+ if request.POST.get('add') is not None:
+ logger.debug("add found")
+ target_id = None
+ if 'target' in request.POST:
+ target_id=int(request.POST.get('target'))
+ manager.add_workflow(workflow_type=int(request.POST.get('add')), target_id=target_id)
+ elif request.POST.get('edit') is not None and request.POST.get('edit_id') is not None:
+ logger.debug("edit found")
+ manager.add_workflow(workflow_type=request.POST.get('edit'), edit_object=int(request.POST.get('edit_id')))
+ elif request.POST.get('cancel') is not None:
+ mgr = ManagerTracker.managers[request.session['manager_session']]
+ del ManagerTracker.managers[request.session['manager_session']]
+ del mgr
+
+ return manager.status(request)
+
+def viewport_view(request):
+ if not request.user.is_authenticated:
+ return login(request)
+
+ manager = attempt_auth(request)
+ if manager is None:
+ return no_workflow(request)
+
+ if request.method == 'GET':
+ return render(request, 'workflow/viewport-base.html')
+ else:
+ pass
+
+def create_session(wf_type, request):
+ wf = int(wf_type)
+ smgr = SessionManager(request=request)
+ smgr.add_workflow(workflow_type=wf, target_id=request.POST.get("target"))
+ manager_uuid = uuid.uuid4().hex
+ ManagerTracker.getInstance().managers[manager_uuid] = smgr
+
+ return manager_uuid
+
+def no_workflow(request):
+
+ logger.debug("There is no active workflow")
+
+ return render(request, 'workflow/no_workflow.html', {'title': "Not Found"})
+
+def login(request):
+ return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
diff --git a/dashboard/src/workflow/workflow_factory.py b/dashboard/src/workflow/workflow_factory.py
new file mode 100644
index 0000000..7c0dcb9
--- /dev/null
+++ b/dashboard/src/workflow/workflow_factory.py
@@ -0,0 +1,149 @@
+##############################################################################
+# 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 workflow.booking_workflow import *
+from workflow.resource_bundle_workflow import *
+from workflow.sw_bundle_workflow import *
+from workflow.snapshot_workflow import *
+from workflow.models import Workflow, Repository
+
+import uuid
+
+import logging
+logger = logging.getLogger(__name__)
+
+class BookingMetaWorkflow(object):
+ workflow_type = 0
+ color = "#0099ff"
+ is_child = False
+
+class ResourceMetaWorkflow(object):
+ workflow_type = 1
+ color = "#ff6600"
+
+class ConfigMetaWorkflow(object):
+ workflow_type = 2
+ color = "#00ffcc"
+
+class MetaRelation(object):
+ def __init__(self, *args, **kwargs):
+ self.color = "#cccccc"
+ self.parent = 0
+ self.children = []
+ self.depth = -1
+
+ def to_json(self):
+ return {
+ 'color': self.color,
+ 'parent': self.parent,
+ 'children': self.children,
+ 'depth': self.depth,
+ }
+
+class MetaStep(object):
+ #valid = 0 #0 is not checked, 1 is invalid, 2 is valid
+
+ UNTOUCHED = 0
+ INVALID = 100
+ VALID = 200
+
+ def set_invalid(self, message, code=100):
+ self.valid = code
+ self.message = message
+
+ def set_valid(self, message, code=200):
+ self.valid = code
+ self.message = message
+
+ def __init__(self, *args, **kwargs):
+ self.short_title = "error"
+ self.skip_step = 0
+ self.valid = 0
+ self.message = ""
+ self.id = uuid.uuid4()
+
+ def to_json(self):
+ return {
+ 'title': self.short_title,
+ 'skip': self.skip_step,
+ 'valid': self.valid,
+ 'message': self.message,
+ }
+
+ def __str__(self):
+ return "metastep: " + str(self.short_title)
+
+ def __hash__(self):
+ return hash(self.id)
+
+ def __eq__(self, other):
+ return self.id.int == other.id.int
+
+ def __ne__(self, other):
+ return self.id.int != other.id.int
+
+class WorkflowFactory():
+ #def __init__(self, *args, **kwargs):
+ booking_steps = [
+ Booking_Resource_Select,
+ SWConfig_Select,
+ Booking_Meta
+ ]
+
+ resource_steps = [
+ Define_Hardware,
+ Define_Nets,
+ Resource_Meta_Info,
+ ]
+
+ config_steps = [
+ SWConf_Resource_Select,
+ Define_Software,
+ Config_Software,
+ ]
+
+ snapshot_steps = [
+ Select_Host_Step,
+ Image_Meta_Step
+ ]
+
+ def conjure(self, workflow_type=None, repo=None):
+ workflow_types = [
+ self.booking_steps,
+ self.resource_steps,
+ self.config_steps,
+ self.snapshot_steps,
+ ]
+
+ steps = self.make_steps(workflow_types[workflow_type], repository=repo)
+ meta_steps = self.metaize(steps=steps, wf_type=workflow_type)
+ return steps, meta_steps
+
+ def make_steps(self, step_types, repository):
+ repository.el['steps'] += len(step_types)
+ steps = []
+ for step_type in step_types:
+ steps.append(self.make_step(step_type, repository))
+
+ return steps
+
+ def make_step(self, step_type, repository):
+ iden = step_type.description + step_type.title + step_type.template
+ return step_type(iden, repository)
+
+ def metaize(self, steps, wf_type):
+ meta_dict = []
+ for step in steps:
+ meta_step = MetaStep()
+ meta_step.short_title = step.short_title
+ meta_dict.append(meta_step)
+ step.metastep = meta_step
+
+ return meta_dict
diff --git a/dashboard/src/workflow/workflow_manager.py b/dashboard/src/workflow/workflow_manager.py
new file mode 100644
index 0000000..16fa468
--- /dev/null
+++ b/dashboard/src/workflow/workflow_manager.py
@@ -0,0 +1,251 @@
+##############################################################################
+# 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.db import models
+from django.contrib.auth.models import User
+from django.core import serializers
+from django.http import HttpResponse, JsonResponse
+
+import json
+import uuid
+import random
+
+from resource_inventory.models import *
+from booking.models import Booking
+from workflow.workflow_factory import WorkflowFactory, MetaStep, MetaRelation
+from workflow.models import Repository, Confirmation_Step
+
+import logging
+logger = logging.getLogger(__name__)
+
+class SessionManager():
+
+ def __init__(self, request=None):
+ self.repository = Repository()
+ self.repository.el[self.repository.SESSION_USER] = request.user
+ self.repository.el['active_step'] = 0
+ self.steps = []
+ self.factory = WorkflowFactory()
+ c_step = WorkflowFactory().make_step(Confirmation_Step, self.repository)
+ self.steps.append(c_step)
+ metaconfirm = MetaStep()
+ metaconfirm.index = 0
+ metaconfirm.short_title = "confirm"
+ self.repository.el['steps'] = 1;
+ self.metaworkflow = None
+ self.metaworkflows = []
+ self.metarelations = []
+ self.relationreverselookup = {}
+ self.initialized = False
+ self.active_index = 0
+ self.step_meta = [metaconfirm]
+ self.relation_depth = 0
+
+ def add_workflow(self, workflow_type=None, target_id=None, **kwargs):
+ if target_id is not None:
+ self.prefill_repo(target_id, workflow_type)
+ factory_steps, meta_info = self.factory.conjure(workflow_type=workflow_type, repo=self.repository)
+ offset = len(meta_info)
+ for relation in self.metarelations:
+ if relation.depth > self.relation_depth:
+ self.relation_depth = relation.depth
+ if relation.parent >= self.repository.el['active_step']:
+ relation.parent += offset
+ for i in range(0, len(relation.children)):
+ if relation.children[i] >= self.repository.el['active_step']:
+ relation.children[i] += offset
+ self.step_meta[self.active_index:self.active_index] = meta_info
+ self.steps[self.active_index:self.active_index] = factory_steps
+
+ if self.initialized:
+ relation = MetaRelation()
+ relation.parent = self.repository.el['active_step'] + offset
+ relation.depth = self.relationreverselookup[self.step_meta[relation.parent]].depth + 1
+ if relation.depth > self.relation_depth:
+ self.relation_depth = relation.depth
+ for i in range(self.repository.el['active_step'], offset + self.repository.el['active_step']):
+ relation.children.append(i)
+ self.relationreverselookup[self.step_meta[i]] = relation
+ relation.color = "#%06x" % random.randint(0, 0xFFFFFF)
+ self.metarelations.append(relation)
+ else:
+ relation = MetaRelation()
+ relation.depth = 0
+ relation.parent = 500000000000
+ for i in range(0, len(self.step_meta)):
+ relation.children.append(i)
+ self.relationreverselookup[self.step_meta[i]] = relation
+ self.metarelations.append(relation)
+ self.initialized = True
+
+
+ def status(self, request):
+ try:
+ workflows = []
+ steps = []
+ for step in self.step_meta:
+ steps.append(step.to_json())
+ parents = {}
+ children = {}
+ responsejson = {}
+ responsejson["steps"] = steps
+ responsejson["active"] = self.repository.el['active_step']
+ responsejson["relations"] = []
+ i = 0
+ for relation in self.metarelations:
+ responsejson["relations"].append(relation.to_json())
+ children[relation.parent] = i
+ for child in relation.children:
+ parents[child] = i
+ i += 1
+ responsejson['max_depth'] = self.relation_depth
+ responsejson['parents'] = parents
+ responsejson['children'] = children
+ return JsonResponse(responsejson, safe=False)
+ except Exception as e:
+ pass
+
+ def render(self, request, **kwargs):
+ #filter out when a step needs to handle post/form data
+ # if 'workflow' in post data, this post request was meant for me, not step
+ if request.method == 'POST' and request.POST.get('workflow', None) is None:
+ return self.steps[self.active_index].post_render(request)
+ return self.steps[self.active_index].render(request)
+
+ def post_render(self, request):
+ return self.steps[self.active_index].post_render(request)
+
+ def goto(self, num, **kwargs):
+ self.repository.el['active_step'] = int(num)
+ self.active_index = int(num)
+ #TODO: change to include some checking
+
+ def prefill_repo(self, target_id, workflow_type):
+ self.repository.el[self.repository.EDIT] = True
+ edit_object = None
+ if workflow_type == 0:
+ edit_object = Booking.objects.get(pk=target_id)
+ self.prefill_booking(edit_object)
+ elif workflow_type == 1:
+ edit_object = GenericResourceBundle.objects.get(pk=target_id)
+ self.prefill_resource(edit_object)
+ elif workflow_type == 2:
+ edit_object = ConfigBundle.objects.get(pk=target_id)
+ self.prefill_config(edit_object)
+
+
+ def prefill_booking(self, booking):
+ models = self.make_booking_models(booking)
+ confirmation = self.make_booking_confirm(booking)
+ self.repository.el[self.repository.BOOKING_MODELS] = models
+ self.repository.el[self.repository.CONFIRMATION] = confirmation
+ self.repository.el[self.repository.GRESOURCE_BUNDLE_MODELS] = self.make_grb_models(booking.resource.template)
+ self.repository.el[self.repository.BOOKING_SELECTED_GRB] = self.make_grb_models(booking.resource.template)['bundle']
+ self.repository.el[self.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.repository.el[self.repository.GRESOURCE_BUNDLE_MODELS] = models
+ self.repository.el[self.repository.CONFIRMATION] = confirm
+
+ def prefill_config(self, config):
+ models = self.make_config_models(config)
+ confirm = self.make_config_confirm(config)
+ self.repository.el[self.repository.CONFIG_MODELS] = models
+ self.repository.el[self.repository.CONFIRMATION] = confirm
+ grb_models = self.make_grb_models(config.bundle)
+ self.repository.el[self.repository.GRESOURCE_BUNDLE_MODELS] = grb_models
+ self.repository.el[self.repository.SWCONF_SELECTED_GRB] = config.bundle
+
+ def make_grb_models(self, resource):
+ models = self.repository.el.get(self.repository.GRESOURCE_BUNDLE_MODELS, {})
+ models['hosts'] = []
+ models['bundle'] = resource
+ models['interfaces'] = {}
+ models['vlans'] = {}
+ for host in resource.getHosts():
+ models['hosts'].append(host)
+ models['interfaces'][host.resource.name] = []
+ models['vlans'][host.resource.name] = {}
+ for interface in host.generic_interfaces.all():
+ models['interfaces'][host.resource.name].append(interface)
+ models['vlans'][host.resource.name][interface.profile.name] = []
+ for vlan in interface.vlans.all():
+ models['vlans'][host.resource.name][interface.profile.name].append(vlan)
+ return models
+
+ def make_grb_confirm(self, resource):
+ confirm = self.repository.el.get(self.repository.CONFIRMATION, {})
+ confirm['resource'] = {}
+ confirm['resource']['hosts'] = []
+ confirm['resource']['lab'] = resource.lab.lab_user.username
+ for host in resource.getHosts():
+ confirm['resource']['hosts'].append({"name": host.resource.name, "profile": host.profile.name})
+ return confirm
+
+ def make_config_models(self, config):
+ models = self.repository.el.get(self.repository.CONFIG_MODELS, {})
+ models['bundle'] = config
+ models['host_configs'] = []
+ for host_conf in HostConfiguration.objects.filter(bundle=config):
+ models['host_configs'].append(host_conf)
+ models['opnfv'] = OPNFVConfig.objects.filter(bundle=config).last()
+ return models
+
+ def make_config_confirm(self, config):
+ confirm = self.repository.el.get(self.repository.CONFIRMATION, {})
+ confirm['configuration'] = {}
+ confirm['configuration']['hosts'] = []
+ confirm['configuration']['name'] = config.name
+ confirm['configuration']['description'] = config.description
+ opnfv = OPNFVConfig.objects.filter(bundle=config).last()
+ confirm['configuration']['installer'] = opnfv.installer.name
+ confirm['configuration']['scenario'] = opnfv.scenario.name
+ for host_conf in HostConfiguration.objects.filter(bundle=config):
+ h = {"name": host_conf.host.resource.name, "image": host_conf.image.name, "role": host_conf.opnfvRole.name}
+ confirm['configuration']['hosts'].append(h)
+ return confirm
+
+ def make_booking_models(self, booking):
+ models = self.repository.el.get(self.repository.BOOKING_MODELS, {})
+ models['booking'] = booking
+ models['collaborators'] = []
+ for user in booking.collaborators.all():
+ models['collaborators'].append(user)
+ return models
+
+ def make_booking_confirm(self, booking):
+ confirm = self.repository.el.get(self.repository.CONFIRMATION, {})
+ confirm['booking'] = {}
+ confirm['booking']['length'] = (booking.end - booking.start).days
+ confirm['booking']['project'] = booking.project
+ confirm['booking']['purpose'] = booking.purpose
+ confirm['booking']['resource name'] = booking.resource.template.name
+ confirm['booking']['configuration name'] = booking.config_bundle.name
+ confirm['booking']['collaborators'] = []
+ for user in booking.collaborators.all():
+ confirm['booking']['collaborators'].append(user.username)
+ return confirm
+
+
+class ManagerTracker():
+ instance = None
+
+ managers = {}
+
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def getInstance():
+ if ManagerTracker.instance is None:
+ ManagerTracker.instance = ManagerTracker()
+ return ManagerTracker.instance