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.py220
-rw-r--r--dashboard/src/workflow/forms.py448
-rw-r--r--dashboard/src/workflow/models.py732
-rw-r--r--dashboard/src/workflow/opnfv_workflow.py299
-rw-r--r--dashboard/src/workflow/resource_bundle_workflow.py463
-rw-r--r--dashboard/src/workflow/snapshot_workflow.py119
-rw-r--r--dashboard/src/workflow/sw_bundle_workflow.py198
-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.py281
-rw-r--r--dashboard/src/workflow/tests/test_steps_render.py43
-rw-r--r--dashboard/src/workflow/tests/test_workflows.py100
-rw-r--r--dashboard/src/workflow/urls.py34
-rw-r--r--dashboard/src/workflow/views.py139
-rw-r--r--dashboard/src/workflow/workflow_factory.py134
-rw-r--r--dashboard/src/workflow/workflow_manager.py243
18 files changed, 3682 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..42372ce
--- /dev/null
+++ b/dashboard/src/workflow/booking_workflow.py
@@ -0,0 +1,220 @@
+##############################################################################
+# 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.utils import timezone
+
+from datetime import timedelta
+
+from booking.models import Booking
+from workflow.models import WorkflowStep, AbstractSelectOrCreate
+from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm, OPNFVSelectForm
+from resource_inventory.models import GenericResourceBundle, ConfigBundle, OPNFVConfig
+
+
+"""
+subclassing notes:
+ subclasses have to define the following class attributes:
+ self.repo_key: main output of step, where the selected/created single selector
+ result is placed at the end
+ self.confirm_key:
+"""
+
+
+class Abstract_Resource_Select(AbstractSelectOrCreate):
+ form = ResourceSelectorForm
+ template = 'dashboard/genericselect.html'
+ title = "Select Resource"
+ description = "Select a resource template to use for your deployment"
+ short_title = "pod select"
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.select_repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
+ self.confirm_key = self.workflow_type
+
+ def alert_bundle_missing(self):
+ self.set_invalid("Please select a valid resource bundle")
+
+ def get_form_queryset(self):
+ user = self.repo_get(self.repo.SESSION_USER)
+ qs = GenericResourceBundle.objects.filter(owner=user)
+ return qs
+
+ def get_page_context(self):
+ return {
+ 'select_type': 'resource',
+ 'select_type_title': 'Resource Bundle',
+ 'addable_type_num': 1
+ }
+
+ def put_confirm_info(self, bundle):
+ confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+ if self.confirm_key not in confirm_dict:
+ confirm_dict[self.confirm_key] = {}
+ confirm_dict[self.confirm_key]["Resource Template"] = bundle.name
+ self.repo_put(self.repo.CONFIRMATION, confirm_dict)
+
+
+class Booking_Resource_Select(Abstract_Resource_Select):
+ workflow_type = "booking"
+
+
+class SWConfig_Select(AbstractSelectOrCreate):
+ title = "Select Software Configuration"
+ description = "Choose the software and related configurations you want to have used for your deployment"
+ short_title = "pod config"
+ form = SWConfigSelectorForm
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE
+ self.confirm_key = "booking"
+
+ def alert_bundle_missing(self):
+ self.set_invalid("Please select a valid pod config")
+
+ def get_form_queryset(self):
+ user = self.repo_get(self.repo.SESSION_USER)
+ grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
+ qs = ConfigBundle.objects.filter(owner=user).filter(bundle=grb)
+ return qs
+
+ def put_confirm_info(self, bundle):
+ confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+ if self.confirm_key not in confirm_dict:
+ confirm_dict[self.confirm_key] = {}
+ confirm_dict[self.confirm_key]["Software Configuration"] = bundle.name
+ self.repo_put(self.repo.CONFIRMATION, confirm_dict)
+
+ def get_page_context(self):
+ return {
+ 'select_type': 'swconfig',
+ 'select_type_title': 'Software Config',
+ 'addable_type_num': 2
+ }
+
+
+class OPNFV_EnablePicker(object):
+ pass
+
+
+class OPNFV_Select(AbstractSelectOrCreate, OPNFV_EnablePicker):
+ title = "Choose an OPNFV Config"
+ description = "Choose or create a description of how you want to deploy OPNFV"
+ short_title = "opnfv config"
+ form = OPNFVSelectForm
+ enabled = False
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.select_repo_key = self.repo.SELECTED_OPNFV_CONFIG
+ self.confirm_key = "booking"
+
+ def alert_bundle_missing(self):
+ self.set_invalid("Please select a valid OPNFV config")
+
+ def get_form_queryset(self):
+ cb = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
+ qs = OPNFVConfig.objects.filter(bundle=cb)
+ return qs
+
+ def put_confirm_info(self, config):
+ confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+ if self.confirm_key not in confirm_dict:
+ confirm_dict[self.confirm_key] = {}
+ confirm_dict[self.confirm_key]["OPNFV Configuration"] = config.name
+ self.repo_put(self.repo.CONFIRMATION, confirm_dict)
+
+ def get_page_context(self):
+ return {
+ 'select_type': 'opnfv',
+ 'select_type_title': 'OPNFV Config',
+ 'addable_type_num': 4
+ }
+
+
+class Booking_Meta(WorkflowStep):
+ 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.userprofile)
+ except Exception:
+ pass
+
+ owner = self.repo_get(self.repo.SESSION_USER)
+
+ context['form'] = BookingMetaForm(initial=initial, user_initial=default, owner=owner)
+ return context
+
+ def post_render(self, request):
+ form = BookingMetaForm(data=request.POST, owner=request.user)
+
+ 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]
+
+ if form.cleaned_data["deploy_opnfv"]:
+ self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=True)
+ else:
+ self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=False)
+
+ userprofile_list = form.cleaned_data['users']
+ confirm['booking']['collaborators'] = []
+ for userprofile in userprofile_list:
+ models['collaborators'].append(userprofile.user)
+ confirm['booking']['collaborators'].append(userprofile.user.username)
+
+ info_file = form.cleaned_data.get("info_file", False)
+ if info_file:
+ 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.set_valid("Step Completed")
+ else:
+ messages.add_message(request, messages.ERROR, "Form didn't validate", fail_silently=True)
+ self.set_invalid("Please complete the fields highlighted in red to continue")
+ return self.render(request)
diff --git a/dashboard/src/workflow/forms.py b/dashboard/src/workflow/forms.py
new file mode 100644
index 0000000..ee44ecd
--- /dev/null
+++ b/dashboard/src/workflow/forms.py
@@ -0,0 +1,448 @@
+##############################################################################
+# 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, ValidationError
+from django.utils.safestring import mark_safe
+from django.template.loader import render_to_string
+from django.forms.widgets import NumberInput
+
+import json
+
+from account.models import Lab
+from account.models import UserProfile
+from resource_inventory.models import (
+ OPNFVRole,
+ Installer,
+ Scenario,
+)
+from booking.lib import get_user_items, get_user_field_opts
+
+
+class SearchableSelectMultipleWidget(widgets.SelectMultiple):
+ template_name = 'dashboard/searchable_select_multiple.html'
+
+ def __init__(self, attrs=None):
+ self.items = attrs['items']
+ self.show_from_noentry = attrs['show_from_noentry']
+ self.show_x_results = attrs['show_x_results']
+ self.results_scrollable = attrs['results_scrollable']
+ self.selectable_limit = attrs['selectable_limit']
+ self.placeholder = attrs['placeholder']
+ self.name = attrs['name']
+ self.initial = attrs.get("initial", [])
+
+ super(SearchableSelectMultipleWidget, self).__init__()
+
+ 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,
+ }
+
+
+class SearchableSelectMultipleField(forms.Field):
+ def __init__(self, *args, required=True, widget=None, label=None, disabled=False,
+ items=None, queryset=None, show_from_noentry=True, show_x_results=-1,
+ results_scrollable=False, selectable_limit=-1, placeholder="search here",
+ name="searchable_select", initial=[], **kwargs):
+ """from the documentation:
+ # required -- Boolean that specifies whether the field is required.
+ # True by default.
+ # widget -- A Widget class, or instance of a Widget class, that should
+ # be used for this Field when displaying it. Each Field has a
+ # default Widget that it'll use if you don't specify this. In
+ # most cases, the default widget is TextInput.
+ # label -- A verbose name for this field, for use in displaying this
+ # field in a form. By default, Django will use a "pretty"
+ # version of the form field name, if the Field is part of a
+ # Form.
+ # initial -- A value to use in this Field's initial display. This value
+ # is *not* used as a fallback if data isn't given.
+ # help_text -- An optional string to use as "help text" for this Field.
+ # error_messages -- An optional dictionary to override the default
+ # messages that the field will raise.
+ # show_hidden_initial -- Boolean that specifies if it is needed to render a
+ # hidden widget with initial value after widget.
+ # validators -- List of additional validators to use
+ # localize -- Boolean that specifies if the field should be localized.
+ # disabled -- Boolean that specifies whether the field is disabled, that
+ # is its widget is shown in the form but not editable.
+ # label_suffix -- Suffix to be added to the label. Overrides
+ # form's label_suffix.
+ """
+
+ self.widget = widget
+ if self.widget is None:
+ self.widget = SearchableSelectMultipleWidget(
+ attrs={
+ 'items': items,
+ 'initial': [obj.id for obj in initial],
+ 'show_from_noentry': show_from_noentry,
+ 'show_x_results': show_x_results,
+ 'results_scrollable': results_scrollable,
+ 'selectable_limit': selectable_limit,
+ 'placeholder': placeholder,
+ 'name': name,
+ 'disabled': disabled
+ }
+ )
+ self.disabled = disabled
+ self.queryset = queryset
+ self.selectable_limit = selectable_limit
+
+ super().__init__(disabled=disabled, **kwargs)
+
+ self.required = required
+
+ def clean(self, data):
+ data = data[0]
+ if not data:
+ if self.required:
+ raise ValidationError("Nothing was selected")
+ else:
+ return []
+ data_as_list = json.loads(data)
+ if self.selectable_limit != -1:
+ if len(data_as_list) > self.selectable_limit:
+ raise ValidationError("Too many items were selected")
+
+ items = []
+ for elem in data_as_list:
+ items.append(self.queryset.get(id=elem))
+
+ return items
+
+
+class SearchableSelectAbstractForm(forms.Form):
+ def __init__(self, *args, queryset=None, initial=[], **kwargs):
+ self.queryset = queryset
+ items = self.generate_items(self.queryset)
+ options = self.generate_options()
+
+ super(SearchableSelectAbstractForm, self).__init__(*args, **kwargs)
+ self.fields['searchable_select'] = SearchableSelectMultipleField(
+ initial=initial,
+ items=items,
+ queryset=self.queryset,
+ **options
+ )
+
+ def get_validated_bundle(self):
+ bundles = self.cleaned_data['searchable_select']
+ if len(bundles) < 1: # don't need to check for >1, as field does that for us
+ raise ValidationError("No bundle was selected")
+ return bundles[0]
+
+ def generate_items(self, queryset):
+ raise Exception("SearchableSelectAbstractForm does not implement concrete generate_items()")
+
+ def generate_options(self, disabled=False):
+ return {
+ 'show_from_noentry': True,
+ 'show_x_results': -1,
+ 'results_scrollable': True,
+ 'selectable_limit': 1,
+ 'placeholder': 'Search for a Bundle',
+ 'name': 'searchable_select',
+ 'disabled': False
+ }
+
+
+class SWConfigSelectorForm(SearchableSelectAbstractForm):
+ def generate_items(self, queryset):
+ items = {}
+
+ for bundle in queryset:
+ items[bundle.id] = {
+ 'expanded_name': bundle.name,
+ 'small_name': bundle.owner.username,
+ 'string': bundle.description,
+ 'id': bundle.id
+ }
+
+ return items
+
+
+class OPNFVSelectForm(SearchableSelectAbstractForm):
+ def generate_items(self, queryset):
+ items = {}
+
+ for config in queryset:
+ items[config.id] = {
+ 'expanded_name': config.name,
+ 'small_name': config.bundle.owner.username,
+ 'string': config.description,
+ 'id': config.id
+ }
+
+ return items
+
+
+class ResourceSelectorForm(SearchableSelectAbstractForm):
+ def generate_items(self, queryset):
+ items = {}
+
+ for bundle in queryset:
+ items[bundle.id] = {
+ 'expanded_name': bundle.name,
+ 'small_name': bundle.owner.username,
+ 'string': bundle.description,
+ 'id': bundle.id
+ }
+
+ return items
+
+
+class BookingMetaForm(forms.Form):
+
+ length = forms.IntegerField(
+ widget=NumberInput(
+ attrs={
+ "type": "range",
+ 'min': "1",
+ "max": "21",
+ "value": "1"
+ }
+ )
+ )
+ purpose = forms.CharField(max_length=1000)
+ project = forms.CharField(max_length=400)
+ info_file = forms.CharField(max_length=1000, required=False)
+ deploy_opnfv = forms.BooleanField(required=False)
+
+ def __init__(self, *args, user_initial=[], owner=None, **kwargs):
+ super(BookingMetaForm, self).__init__(**kwargs)
+
+ self.fields['users'] = SearchableSelectMultipleField(
+ queryset=UserProfile.objects.select_related('user').exclude(user=owner),
+ initial=user_initial,
+ items=get_user_items(exclude=owner),
+ required=False,
+ **get_user_field_opts()
+ )
+
+
+class MultipleSelectFilterWidget(forms.Widget):
+ def __init__(self, *args, display_objects=None, filter_items=None, neighbors=None, **kwargs):
+ super(MultipleSelectFilterWidget, self).__init__(*args, **kwargs)
+ self.display_objects = display_objects
+ self.filter_items = filter_items
+ self.neighbors = neighbors
+ self.template_name = "dashboard/multiple_select_filter_widget.html"
+
+ def render(self, name, value, attrs=None, renderer=None):
+ context = self.get_context(name, value, attrs)
+ html = render_to_string(self.template_name, context=context)
+ return mark_safe(html)
+
+ def get_context(self, name, value, attrs):
+ return {
+ 'display_objects': self.display_objects,
+ 'neighbors': self.neighbors,
+ 'filter_items': self.filter_items,
+ 'initial_value': value
+ }
+
+
+class MultipleSelectFilterField(forms.Field):
+
+ def __init__(self, **kwargs):
+ self.initial = kwargs.get("initial")
+ super().__init__(**kwargs)
+
+ def to_python(self, value):
+ return json.loads(value)
+
+
+class FormUtils:
+ @staticmethod
+ def getLabData(multiple_hosts=False):
+ """
+ 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
+ """
+ # javascript truthy variables
+ true = 1
+ false = 0
+ if multiple_hosts:
+ multiple_hosts = true
+ else:
+ multiple_hosts = false
+ labs = {}
+ hosts = {}
+ items = {}
+ neighbors = {}
+ for lab in Lab.objects.all():
+ lab_node = {
+ 'id': "lab_" + str(lab.lab_user.id),
+ 'model_id': lab.lab_user.id,
+ 'name': lab.name,
+ 'description': lab.description,
+ 'selected': false,
+ 'selectable': true,
+ 'follow': false,
+ 'multiple': false,
+ 'class': 'lab'
+ }
+ if multiple_hosts:
+ # "follow" this lab node to discover more hosts if allowed
+ lab_node['follow'] = true
+ items[lab_node['id']] = lab_node
+ neighbors[lab_node['id']] = []
+ labs[lab_node['id']] = lab_node
+
+ for host in lab.hostprofiles.all():
+ host_node = {
+ 'form': {"name": "host_name", "type": "text", "placeholder": "hostname"},
+ 'id': "host_" + str(host.id),
+ 'model_id': host.id,
+ 'name': host.name,
+ 'description': host.description,
+ 'selected': false,
+ 'selectable': true,
+ 'follow': false,
+ 'multiple': multiple_hosts,
+ 'class': 'host'
+ }
+ if multiple_hosts:
+ host_node['values'] = [] # place to store multiple values
+ items[host_node['id']] = host_node
+ neighbors[lab_node['id']].append(host_node['id'])
+ if host_node['id'] not in neighbors:
+ neighbors[host_node['id']] = []
+ neighbors[host_node['id']].append(lab_node['id'])
+ hosts[host_node['id']] = host_node
+
+ display_objects = [("lab", labs.values()), ("host", hosts.values())]
+
+ context = {
+ 'display_objects': display_objects,
+ 'neighbors': neighbors,
+ 'filter_items': items
+ }
+ return context
+
+
+class HardwareDefinitionForm(forms.Form):
+
+ def __init__(self, *args, **kwargs):
+ super(HardwareDefinitionForm, self).__init__(*args, **kwargs)
+ attrs = FormUtils.getLabData(multiple_hosts=True)
+ self.fields['filter_field'] = MultipleSelectFilterField(
+ widget=MultipleSelectFilterWidget(**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):
+ super(NetworkDefinitionForm, self).__init__(**kwargs)
+
+
+class NetworkConfigurationForm(forms.Form):
+ def __init__(self, *args, **kwargs):
+ super(NetworkConfigurationForm).__init__(**kwargs)
+
+
+class HostSoftwareDefinitionForm(forms.Form):
+
+ host_name = forms.CharField(max_length=200, disabled=True, required=False)
+ headnode = forms.BooleanField(required=False, widget=forms.HiddenInput)
+
+ def __init__(self, *args, **kwargs):
+ imageQS = kwargs.pop("imageQS")
+ super(HostSoftwareDefinitionForm, self).__init__(*args, **kwargs)
+ self.fields['image'] = forms.ModelChoiceField(queryset=imageQS)
+
+
+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 BasicMetaForm(forms.Form):
+ name = forms.CharField()
+ description = forms.CharField(widget=forms.Textarea)
+
+
+class ConfirmationForm(forms.Form):
+ fields = ['confirm']
+
+ confirm = forms.ChoiceField(
+ choices=(
+ (True, "Confirm"),
+ (False, "Cancel")
+ )
+ )
+
+
+class OPNFVSelectionForm(forms.Form):
+ installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=True)
+ scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=True)
+
+
+class OPNFVNetworkRoleForm(forms.Form):
+ role = forms.CharField(max_length=200, disabled=True, required=False)
+
+ def __init__(self, *args, config_bundle, **kwargs):
+ super(OPNFVNetworkRoleForm, self).__init__(*args, **kwargs)
+ self.fields['network'] = forms.ModelChoiceField(
+ queryset=config_bundle.bundle.networks.all()
+ )
+
+
+class OPNFVHostRoleForm(forms.Form):
+ host_name = forms.CharField(max_length=200, disabled=True, required=False)
+ role = forms.ModelChoiceField(queryset=OPNFVRole.objects.all().order_by("name").distinct("name"))
diff --git a/dashboard/src/workflow/models.py b/dashboard/src/workflow/models.py
new file mode 100644
index 0000000..6c6bd9a
--- /dev/null
+++ b/dashboard/src/workflow/models.py
@@ -0,0 +1,732 @@
+##############################################################################
+# 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.shortcuts import render
+from django.contrib import messages
+from django.http import HttpResponse
+from django.utils import timezone
+
+import yaml
+import requests
+
+from workflow.forms import ConfirmationForm
+from api.models import JobFactory
+from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
+from resource_inventory.models import Image, GenericInterface, OPNFVConfig, HostOPNFVConfig, NetworkRole
+from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
+from notifier.manager import NotificationHandler
+from booking.models import Booking
+
+
+class BookingAuthManager():
+ LFN_PROJECTS = ["opnfv"] # TODO
+
+ def parse_github_url(self, url):
+ project_leads = []
+ try:
+ parts = url.split("/")
+ if "http" in parts[0]: # 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 ptl:
+ project_leads.append(ptl)
+ sub_ptl = info_parsed.get("subproject_lead")
+ if sub_ptl:
+ project_leads.append(sub_ptl)
+
+ except Exception:
+ pass
+
+ return project_leads
+
+ def parse_gerrit_url(self, url):
+ project_leads = []
+ try:
+ halfs = url.split("?")
+ parts = halfs[0].split("/")
+ args = halfs[1].split(";")
+ if "http" in parts[0]: # the url include http(s)://
+ parts = parts[2:]
+ if "f=INFO.yaml" not in args:
+ return None
+ if "gerrit.opnfv.org" not in parts[0]:
+ return None
+ try:
+ i = args.index("a=blob")
+ args[i] = "a=blob_plain"
+ except ValueError:
+ pass
+ # recreate url
+ halfs[1] = ";".join(args)
+ halfs[0] = "/".join(parts)
+ # now to download and parse file
+ url = "https://" + "?".join(halfs)
+ info_file = requests.get(url, timeout=15).text
+ info_parsed = yaml.load(info_file)
+ ptl = info_parsed.get('project_lead')
+ if ptl:
+ project_leads.append(ptl)
+ sub_ptl = info_parsed.get("subproject_lead")
+ if sub_ptl:
+ project_leads.append(sub_ptl)
+
+ except Exception:
+ return None
+
+ return project_leads
+
+ def parse_opnfv_git_url(self, url):
+ project_leads = []
+ try:
+ parts = url.split("/")
+ if "http" in parts[0]: # the url include http(s)://
+ parts = parts[2:]
+ if "INFO.yaml" not in parts[-1]:
+ return None
+ if "git.opnfv.org" not in parts[0]:
+ return None
+ if parts[-2] == "tree":
+ parts[-2] = "plain"
+ # now to download and parse file
+ 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 ptl:
+ project_leads.append(ptl)
+ sub_ptl = info_parsed.get("subproject_lead")
+ if sub_ptl:
+ project_leads.append(sub_ptl)
+
+ except Exception:
+ return None
+
+ return project_leads
+
+ def parse_url(self, info_url):
+ """
+ will return the PTL in the INFO file on success, or None
+ """
+ if "github" in info_url:
+ return self.parse_github_url(info_url)
+
+ if "gerrit.opnfv.org" in info_url:
+ return self.parse_gerrit_url(info_url)
+
+ if "git.opnfv.org" in info_url:
+ return self.parse_opnfv_git_url(info_url)
+
+ 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 booking.owner.userprofile.booking_privledge:
+ return True # admin override for this user
+ if Booking.objects.filter(owner=booking.owner, end__gt=timezone.now()).count() >= 3:
+ return False
+ if len(booking.resource.template.getHosts()) < 2:
+ return True # if they only have one server, we dont care
+ if repo.BOOKING_INFO_FILE not in repo.el:
+ return False # INFO file not provided
+ ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE))
+ for ptl in ptl_info:
+ if ptl['email'] == booking.owner.userprofile.email_addr:
+ return True
+ return False
+
+
+class WorkflowStepStatus(object):
+ UNTOUCHED = 0
+ INVALID = 100
+ VALID = 200
+
+
+class WorkflowStep(object):
+ template = 'bad_request.html'
+ title = "Generic Step"
+ description = "You were led here by mistake"
+ short_title = "error"
+ metastep = None
+ # phasing out metastep:
+
+ valid = WorkflowStepStatus.UNTOUCHED
+ message = ""
+
+ enabled = True
+
+ def cleanup(self):
+ raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete implemented cleanup() method")
+
+ def enable(self):
+ if not self.enabled:
+ self.enabled = True
+
+ def disable(self):
+ if self.enabled:
+ self.cleanup()
+ self.enabled = False
+
+ def set_invalid(self, message, code=WorkflowStepStatus.INVALID):
+ self.valid = code
+ self.message = message
+
+ def set_valid(self, message, code=WorkflowStepStatus.VALID):
+ self.valid = code
+ self.message = message
+
+ def to_json(self):
+ return {
+ 'title': self.short_title,
+ 'enabled': self.enabled,
+ 'valid': self.valid,
+ 'message': self.message,
+ }
+
+ 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)
+
+
+"""
+subclassing notes:
+ subclasses have to define the following class attributes:
+ self.select_repo_key: where the selected "object" or "bundle" is to be placed in the repo
+ self.form: the form to be used
+ alert_bundle_missing(): what message to display if a user does not select/selects an invalid object
+ get_form_queryset(): generate a queryset to be used to filter available items for the field
+ get_page_context(): return simple context such as page header and other info
+"""
+
+
+class AbstractSelectOrCreate(WorkflowStep):
+ template = 'dashboard/genericselect.html'
+ title = "Select a Bundle"
+ short_title = "select"
+ description = "Generic bundle selector step"
+
+ select_repo_key = None
+ form = None # subclasses are expected to use a form that is a subclass of SearchableSelectGenericForm
+
+ def alert_bundle_missing(self): # override in subclasses to change message if field isn't filled out
+ self.set_invalid("Please select a valid bundle")
+
+ def post_render(self, request):
+ context = self.get_context()
+ form = self.form(request.POST, queryset=self.get_form_queryset())
+ if form.is_valid():
+ bundle = form.get_validated_bundle()
+ if not bundle:
+ self.alert_bundle_missing()
+ return render(request, self.template, context)
+ self.repo_put(self.select_repo_key, bundle)
+ self.put_confirm_info(bundle)
+ self.set_valid("Step Completed")
+ else:
+ self.alert_bundle_missing()
+ messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
+
+ return self.render(request)
+
+ def get_context(self):
+ default = []
+
+ bundle = self.repo_get(self.select_repo_key, False)
+ if bundle:
+ default.append(bundle)
+
+ form = self.form(queryset=self.get_form_queryset(), initial=default)
+
+ context = {'form': form, **self.get_page_context()}
+ context.update(super().get_context())
+
+ return context
+
+ def get_page_context():
+ return {
+ 'select_type': 'generic',
+ 'select_type_title': 'Generic Bundle'
+ }
+
+
+class Confirmation_Step(WorkflowStep):
+ template = 'workflow/confirm.html'
+ title = "Confirm Changes"
+ description = "Does this all look right?"
+
+ short_title = "confirm"
+
+ 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()
+
+ 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)
+ else:
+ messages.add_message(request, messages.SUCCESS, "Confirmed")
+
+ return HttpResponse('')
+ elif data == "False":
+ context["bypassed"] = "true"
+ messages.add_message(request, messages.SUCCESS, "Canceled")
+ return render(request, self.template, context)
+ else:
+ pass
+
+ else:
+ pass
+
+
+class Repository():
+
+ EDIT = "editing"
+ MODELS = "models"
+ RESOURCE_SELECT = "resource_select"
+ CONFIRMATION = "confirmation"
+ SELECTED_GRESOURCE_BUNDLE = "selected generic bundle pk"
+ SELECTED_CONFIG_BUNDLE = "selected config bundle pk"
+ SELECTED_OPNFV_CONFIG = "selected opnfv deployment config"
+ GRESOURCE_BUNDLE_MODELS = "generic_resource_bundle_models"
+ GRESOURCE_BUNDLE_INFO = "generic_resource_bundle_info"
+ BOOKING = "booking"
+ LAB = "lab"
+ GRB_LAST_HOSTLIST = "grb_network_previous_hostlist"
+ BOOKING_FORMS = "booking_forms"
+ SWCONF_HOSTS = "swconf_hosts"
+ BOOKING_MODELS = "booking models"
+ CONFIG_MODELS = "configuration bundle models"
+ OPNFV_MODELS = "opnfv configuration models"
+ SESSION_USER = "session owner user account"
+ SESSION_MANAGER = "session manager for current session"
+ 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"
+
+ # migratory elements of segmented workflow
+ # each of these is the end result of a different workflow.
+ HAS_RESULT = "whether or not workflow has a result"
+ RESULT_KEY = "key for target index that result will be put into in parent"
+ RESULT = "result object from workflow"
+
+ def get_child_defaults(self):
+ return_tuples = []
+ for key in [self.SELECTED_GRESOURCE_BUNDLE, self.SESSION_USER]:
+ return_tuples.append((key, self.el.get(key)))
+ return return_tuples
+
+ def set_defaults(self, defaults):
+ for key, value in defaults:
+ self.el[key] = value
+
+ 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 key not 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
+ else:
+ self.el[self.HAS_RESULT] = True
+ self.el[self.RESULT_KEY] = self.SELECTED_GRESOURCE_BUNDLE
+ return
+
+ if self.CONFIG_MODELS in self.el:
+ errors = self.make_software_config_bundle()
+ if errors:
+ return errors
+ else:
+ self.el[self.HAS_RESULT] = True
+ self.el[self.RESULT_KEY] = self.SELECTED_CONFIG_BUNDLE
+ return
+
+ if self.OPNFV_MODELS in self.el:
+ errors = self.make_opnfv_config()
+ if errors:
+ return errors
+ else:
+ self.el[self.HAS_RESULT] = True
+ self.el[self.RESULT_KEY] = self.SELECTED_OPNFV_CONFIG
+
+ if self.BOOKING_MODELS in self.el:
+ errors = self.make_booking()
+ if errors:
+ return errors
+ # create notification
+ booking = self.el[self.BOOKING_MODELS]['booking']
+ NotificationHandler.notify_new_booking(booking)
+
+ 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)
+ if booking.start > timezone.now() or booking.end < timezone.now():
+ return "Booking is not active"
+ 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()
+ try:
+ current_image = host.config.image
+ image.os = current_image.os
+ image.save()
+ except Exception:
+ pass
+ JobFactory.makeSnapshotTask(image, booking, host)
+
+ self.el[self.RESULT] = image
+ self.el[self.HAS_RESULT] = True
+
+ def make_generic_resource_bundle(self):
+ owner = self.el[self.SESSION_USER]
+ if self.GRESOURCE_BUNDLE_MODELS in self.el:
+ models = self.el[self.GRESOURCE_BUNDLE_MODELS]
+ if 'hosts' in models:
+ hosts = models['hosts']
+ else:
+ return "GRB has no hosts. CODE:0x0002"
+ if 'bundle' in models:
+ bundle = models['bundle']
+ else:
+ return "GRB, no bundle in models. CODE:0x0003"
+
+ try:
+ bundle.owner = owner
+ bundle.save()
+ except Exception as e:
+ return "GRB, saving bundle generated exception: " + str(e) + " CODE:0x0004"
+ try:
+ for host in hosts:
+ genericresource = host.resource
+ genericresource.bundle = bundle
+ genericresource.save()
+ host.resource = genericresource
+ host.save()
+ except Exception as e:
+ return "GRB, saving hosts generated exception: " + str(e) + " CODE:0x0005"
+
+ if 'networks' in models:
+ for net in models['networks'].values():
+ net.bundle = bundle
+ net.save()
+
+ if 'interfaces' in models:
+ for interface_set in models['interfaces'].values():
+ for interface in interface_set:
+ try:
+ interface.host = interface.host
+ interface.save()
+ except Exception:
+ return "GRB, saving interface " + str(interface) + " failed. CODE:0x0019"
+ else:
+ return "GRB, no interface set provided. CODE:0x001a"
+
+ if 'connections' in models:
+ for resource_name, mapping in models['connections'].items():
+ for profile_name, connection_set in mapping.items():
+ interface = GenericInterface.objects.get(
+ profile__name=profile_name,
+ host__resource__name=resource_name,
+ host__resource__bundle=models['bundle']
+ )
+ for connection in connection_set:
+ try:
+ connection.network = connection.network
+ connection.save()
+ interface.connections.add(connection)
+ except Exception as e:
+ return "GRB, saving vlan " + str(connection) + " failed. Exception: " + str(e) + ". CODE:0x0017"
+ else:
+ return "GRB, no vlan set provided. CODE:0x0018"
+
+ else:
+ return "GRB no models given. CODE:0x0001"
+
+ self.el[self.RESULT] = bundle
+ self.el[self.HAS_RESULT] = True
+ return False
+
+ def make_software_config_bundle(self):
+ models = self.el[self.CONFIG_MODELS]
+ if 'bundle' in models:
+ bundle = models['bundle']
+ bundle.bundle = self.el[self.SELECTED_GRESOURCE_BUNDLE]
+ 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 opnfvconfig.scenario not 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.RESULT] = bundle
+ return False
+
+ def make_booking(self):
+ models = self.el[self.BOOKING_MODELS]
+ owner = self.el[self.SESSION_USER]
+
+ if 'booking' in models:
+ booking = models['booking']
+ else:
+ return "BOOK, no booking model exists. CODE:0x000f"
+
+ selected_grb = None
+
+ if self.SELECTED_GRESOURCE_BUNDLE in self.el:
+ selected_grb = self.el[self.SELECTED_GRESOURCE_BUNDLE]
+ else:
+ return "BOOK, no selected resource. CODE:0x000e"
+
+ if self.SELECTED_CONFIG_BUNDLE not in self.el:
+ return "BOOK, no selected config bundle. CODE:0x001f"
+
+ booking.config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
+
+ if not booking.start:
+ return "BOOK, booking has no start. CODE:0x0010"
+ 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.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:
+ booking.pdf = PDFTemplater.makePDF(booking)
+ booking.save()
+ except Exception as e:
+ return "BOOK, failed to create Pod Desriptor File: " + str(e)
+
+ 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"
+
+ self.el[self.RESULT] = booking
+ self.el[self.HAS_RESULT] = True
+
+ def make_opnfv_config(self):
+ opnfv_models = self.el[self.OPNFV_MODELS]
+ config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
+ if not config_bundle:
+ return "No Configuration bundle selected"
+ info = opnfv_models.get("meta", {})
+ name = info.get("name", False)
+ desc = info.get("description", False)
+ if not (name and desc):
+ return "No name or description given"
+ installer = opnfv_models['installer_chosen']
+ if not installer:
+ return "No OPNFV Installer chosen"
+ scenario = opnfv_models['scenario_chosen']
+ if not scenario:
+ return "No OPNFV Scenario chosen"
+
+ opnfv_config = OPNFVConfig.objects.create(
+ bundle=config_bundle,
+ name=name,
+ description=desc,
+ installer=installer,
+ scenario=scenario
+ )
+
+ network_roles = opnfv_models['network_roles']
+ for net_role in network_roles:
+ opnfv_config.networks.add(
+ NetworkRole.objects.create(
+ name=net_role['role'],
+ network=net_role['network']
+ )
+ )
+
+ host_roles = opnfv_models['host_roles']
+ for host_role in host_roles:
+ config = config_bundle.hostConfigurations.get(
+ host__resource__name=host_role['host_name']
+ )
+ HostOPNFVConfig.objects.create(
+ role=host_role['role'],
+ host_config=config,
+ opnfv_config=opnfv_config
+ )
+
+ self.el[self.RESULT] = opnfv_config
+ self.el[self.HAS_RESULT] = True
+
+ def __init__(self):
+ self.el = {}
+ self.el[self.CONFIRMATION] = {}
+ self.el["active_step"] = 0
+ self.el[self.HAS_RESULT] = False
+ self.el[self.RESULT] = None
+ self.get_history = {}
+ self.put_history = {}
diff --git a/dashboard/src/workflow/opnfv_workflow.py b/dashboard/src/workflow/opnfv_workflow.py
new file mode 100644
index 0000000..7d499ec
--- /dev/null
+++ b/dashboard/src/workflow/opnfv_workflow.py
@@ -0,0 +1,299 @@
+##############################################################################
+# 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.forms import formset_factory
+
+from workflow.models import WorkflowStep, AbstractSelectOrCreate
+from resource_inventory.models import ConfigBundle, OPNFV_SETTINGS
+from workflow.forms import OPNFVSelectionForm, OPNFVNetworkRoleForm, OPNFVHostRoleForm, SWConfigSelectorForm, BasicMetaForm
+
+
+class OPNFV_Resource_Select(AbstractSelectOrCreate):
+ title = "Select Software Configuration"
+ description = "Choose the software bundle you wish to use as a base for your OPNFV configuration"
+ short_title = "software config"
+ form = SWConfigSelectorForm
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE
+
+ def get_form_queryset(self):
+ user = self.repo_get(self.repo.SESSION_USER)
+ qs = ConfigBundle.objects.filter(owner=user)
+ return qs
+
+ def put_confirm_info(self, bundle):
+ confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+ confirm_dict['software bundle'] = bundle.name
+ confirm_dict['hardware POD'] = bundle.bundle.name
+ self.repo_put(self.repo.CONFIRMATION, confirm_dict)
+
+ def get_page_context(self):
+ return {
+ 'select_type': 'swconfig',
+ 'select_type_title': 'Software Config',
+ 'addable_type_num': 2
+ }
+
+
+class Pick_Installer(WorkflowStep):
+ template = 'config_bundle/steps/pick_installer.html'
+ title = 'Pick OPNFV Installer'
+ description = 'Choose which OPNFV installer to use'
+ short_title = "opnfv installer"
+ modified_key = "installer_step"
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ installer = models.get("installer_chosen")
+ scenario = models.get("scenario_chosen")
+ if not (installer and scenario):
+ return
+ confirm['installer'] = installer.name
+ confirm['scenario'] = scenario.name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def get_context(self):
+ context = super(Pick_Installer, self).get_context()
+
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ initial = {
+ "installer": models.get("installer_chosen"),
+ "scenario": models.get("scenario_chosen")
+ }
+
+ context["form"] = OPNFVSelectionForm(initial=initial)
+ return context
+
+ def post_render(self, request):
+ form = OPNFVSelectionForm(request.POST)
+ if form.is_valid():
+ installer = form.cleaned_data['installer']
+ scenario = form.cleaned_data['scenario']
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ models['installer_chosen'] = installer
+ models['scenario_chosen'] = scenario
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+ self.set_valid("Step Completed")
+ else:
+ self.set_invalid("Please select an Installer and Scenario")
+
+ return self.render(request)
+
+
+class Assign_Network_Roles(WorkflowStep):
+ template = 'config_bundle/steps/assign_network_roles.html'
+ title = 'Pick Network Roles'
+ description = 'Choose what role each network should get'
+ short_title = "network roles"
+ modified_key = "net_roles_step"
+
+ """
+ to do initial filling, repo should have a "network_roles" array with the following structure for each element:
+ {
+ "role": <NetworkRole object ref>,
+ "network": <Network object ref>
+ }
+ """
+ def create_netformset(self, roles, config_bundle, data=None):
+ roles_initial = []
+ set_roles = self.repo_get(self.repo.OPNFV_MODELS, {}).get("network_roles")
+ if set_roles:
+ roles_initial = set_roles
+ else:
+ for role in OPNFV_SETTINGS.NETWORK_ROLES:
+ roles_initial.append({"role": role})
+
+ Formset = formset_factory(OPNFVNetworkRoleForm, extra=0)
+ kwargs = {
+ "initial": roles_initial,
+ "form_kwargs": {"config_bundle": config_bundle}
+ }
+ formset = None
+ if data:
+ formset = Formset(data, **kwargs)
+ else:
+ formset = Formset(**kwargs)
+ return formset
+
+ def get_context(self):
+ context = super(Assign_Network_Roles, self).get_context()
+ config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
+ if config_bundle is None:
+ context["unavailable"] = True
+ return context
+
+ roles = OPNFV_SETTINGS.NETWORK_ROLES
+ formset = self.create_netformset(roles, config_bundle)
+ context['formset'] = formset
+
+ return context
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ roles = models.get("network_roles")
+ if not roles:
+ return
+ confirm['network roles'] = {}
+ for role in roles:
+ confirm['network roles'][role['role']] = role['network'].name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
+ roles = OPNFV_SETTINGS.NETWORK_ROLES
+ net_role_formset = self.create_netformset(roles, config_bundle, data=request.POST)
+ if net_role_formset.is_valid():
+ results = []
+ for form in net_role_formset:
+ results.append({
+ "role": form.cleaned_data['role'],
+ "network": form.cleaned_data['network']
+ })
+ models['network_roles'] = results
+ self.set_valid("Completed")
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+ else:
+ self.set_invalid("Please complete all fields")
+ return self.render(request)
+
+
+class Assign_Host_Roles(WorkflowStep): # taken verbatim from Define_Software in sw workflow, merge the two?
+ template = 'config_bundle/steps/assign_host_roles.html'
+ title = 'Pick Host Roles'
+ description = "Choose the role each machine will have in your OPNFV pod"
+ short_title = "host roles"
+ modified_key = "host_roles_step"
+
+ def create_host_role_formset(self, hostlist=[], data=None):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ host_roles = models.get("host_roles", [])
+ if not host_roles:
+ for host in hostlist:
+ initial = {"host_name": host.resource.name}
+ host_roles.append(initial)
+ models['host_roles'] = host_roles
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+
+ HostFormset = formset_factory(OPNFVHostRoleForm, extra=0)
+
+ kwargs = {"initial": host_roles}
+ formset = None
+ if data:
+ formset = HostFormset(data, **kwargs)
+ else:
+ formset = HostFormset(**kwargs)
+
+ return formset
+
+ def get_context(self):
+ context = super(Assign_Host_Roles, self).get_context()
+ config = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
+ if config is None:
+ context['error'] = "Please select a Configuration on the first step"
+
+ formset = self.create_host_role_formset(hostlist=config.bundle.getHosts())
+ context['formset'] = formset
+
+ return context
+
+ def get_host_role_mapping(self, host_roles, hostname):
+ for obj in host_roles:
+ if hostname == obj['host_name']:
+ return obj
+ return None
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ roles = models.get("host_roles")
+ if not roles:
+ return
+ confirm['host roles'] = {}
+ for role in roles:
+ confirm['host roles'][role['host_name']] = role['role'].name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ formset = self.create_host_role_formset(data=request.POST)
+
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ host_roles = models.get("host_roles", [])
+
+ has_jumphost = False
+ if formset.is_valid():
+ for form in formset:
+ hostname = form.cleaned_data['host_name']
+ role = form.cleaned_data['role']
+ mapping = self.get_host_role_mapping(host_roles, hostname)
+ mapping['role'] = role
+ if "jumphost" in role.name.lower():
+ has_jumphost = True
+
+ models['host_roles'] = host_roles
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+
+ if not has_jumphost:
+ self.set_invalid('Must have at least one "Jumphost" per POD')
+ else:
+ self.set_valid("Completed")
+ else:
+ self.set_invalid("Please complete all fields")
+
+ return self.render(request)
+
+
+class MetaInfo(WorkflowStep):
+ template = 'config_bundle/steps/config_software.html'
+ title = "Other Info"
+ description = "Give your software config a name, description, and other stuff"
+ short_title = "config info"
+
+ def get_context(self):
+ context = super(MetaInfo, self).get_context()
+
+ initial = self.repo_get(self.repo.OPNFV_MODELS, {}).get("meta", {})
+ context["form"] = BasicMetaForm(initial=initial)
+ return context
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ meta = models.get("meta")
+ if not meta:
+ return
+ confirm['name'] = meta['name']
+ confirm['description'] = meta['description']
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ info = models.get("meta", {})
+
+ form = BasicMetaForm(request.POST)
+ if form.is_valid():
+ info['name'] = form.cleaned_data['name']
+ info['description'] = form.cleaned_data['description']
+ models['meta'] = info
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+ self.set_valid("Complete")
+ else:
+ self.set_invalid("Please correct the errors shown below")
+
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ return self.render(request)
diff --git a/dashboard/src/workflow/resource_bundle_workflow.py b/dashboard/src/workflow/resource_bundle_workflow.py
new file mode 100644
index 0000000..06737d2
--- /dev/null
+++ b/dashboard/src/workflow/resource_bundle_workflow.py
@@ -0,0 +1,463 @@
+##############################################################################
+# 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
+from django.conf import settings
+
+import json
+import re
+from xml.dom import minidom
+
+from workflow.models import WorkflowStep
+from account.models import Lab
+from workflow.forms import (
+ HardwareDefinitionForm,
+ NetworkDefinitionForm,
+ ResourceMetaForm,
+ GenericHostMetaForm
+)
+from resource_inventory.models import (
+ GenericResourceBundle,
+ GenericInterface,
+ GenericHost,
+ GenericResource,
+ HostProfile,
+ Network,
+ NetworkConnection
+)
+from dashboard.exceptions import (
+ InvalidVlanConfigurationException,
+ NetworkExistsException,
+ InvalidHostnameException,
+ NonUniqueHostnameException,
+ ResourceAvailabilityException
+)
+
+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 __init__(self, *args, **kwargs):
+ self.form = None
+ super().__init__(*args, **kwargs)
+
+ def get_context(self):
+ context = super(Define_Hardware, self).get_context()
+ context['form'] = self.form or HardwareDefinitionForm()
+ return context
+
+ def update_models(self, data):
+ data = data['filter_field']
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ models['hosts'] = [] # This will always clear existing data when this step changes
+ models['interfaces'] = {}
+ if "bundle" not in models:
+ models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))
+ host_data = data['host']
+ names = {}
+ for host_profile_dict in host_data.values():
+ id = host_profile_dict['id']
+ profile = HostProfile.objects.get(id=id)
+ # instantiate genericHost and store in repo
+ for name in host_profile_dict['values'].values():
+ if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
+ raise InvalidHostnameException("Invalid hostname: '" + name + "'")
+ if name in names:
+ raise NonUniqueHostnameException("All hosts must have unique names")
+ names[name] = True
+ 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['lab'].values():
+ if lab_dict['selected']:
+ models['bundle'].lab = Lab.objects.get(lab_user__id=lab_dict['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.set_valid("Step Completed")
+ else:
+ self.set_invalid("Please complete the fields highlighted in red to continue")
+ except Exception as e:
+ self.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:
+ return None
+
+ def make_mx_host_dict(self, generic_host):
+ host = {
+ 'id': generic_host.resource.name,
+ 'interfaces': [],
+ 'value': {
+ "name": generic_host.resource.name,
+ "description": generic_host.profile.description
+ }
+ }
+ for iface in generic_host.profile.interfaceprofile.all():
+ host['interfaces'].append({
+ "name": iface.name,
+ "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type
+ })
+ return host
+
+ def get_context(self):
+ context = super(Define_Nets, self).get_context()
+ context.update({
+ 'form': NetworkDefinitionForm(),
+ 'debug': settings.DEBUG,
+ 'hosts': [],
+ 'added_hosts': [],
+ 'removed_hosts': []
+ })
+ vlans = self.get_vlans()
+ if vlans:
+ context['vlans'] = vlans
+ try:
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ hosts = models.get("hosts", [])
+ # calculate if the selected hosts have changed
+ added_hosts = set()
+ host_set = set(self.repo_get(self.repo.GRB_LAST_HOSTLIST, []))
+ if len(host_set):
+ new_host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']])
+ context['removed_hosts'] = [h.split("*")[0] for h in (host_set - new_host_set)]
+ added_hosts.update([h.split("*")[0] for h in (new_host_set - host_set)])
+
+ # add all host info to context
+ for generic_host in hosts:
+ host = self.make_mx_host_dict(generic_host)
+ host_serialized = json.dumps(host)
+ context['hosts'].append(host_serialized)
+ if host['id'] in added_hosts:
+ context['added_hosts'].append(host_serialized)
+ bundle = models.get("bundle", False)
+ if bundle:
+ context['xml'] = bundle.xml or False
+
+ except Exception:
+ pass
+
+ return context
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ if 'hosts' in models:
+ host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']])
+ self.repo_put(self.repo.GRB_LAST_HOSTLIST, host_set)
+ try:
+ xmlData = request.POST.get("xml")
+ self.updateModels(xmlData)
+ # update model with xml
+ self.set_valid("Networks applied successfully")
+ except ResourceAvailabilityException:
+ self.set_invalid("Public network not availble")
+ except Exception as e:
+ self.set_invalid("An error occurred when applying networks: " + str(e))
+ return self.render(request)
+
+ def updateModels(self, xmlData):
+ models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
+ models["connections"] = {}
+ models['networks'] = {}
+ given_hosts, interfaces, networks = self.parseXml(xmlData)
+ existing_host_list = models.get("hosts", [])
+ existing_hosts = {} # maps id to host
+ for host in existing_host_list:
+ existing_hosts[host.resource.name] = host
+
+ bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
+
+ for net_id, net in networks.items():
+ network = Network()
+ network.name = net['name']
+ network.bundle = bundle
+ network.is_public = net['public']
+ models['networks'][net_id] = network
+
+ for hostid, given_host in given_hosts.items():
+ existing_host = existing_hosts[hostid[5:]]
+
+ for ifaceId in given_host['interfaces']:
+ iface = interfaces[ifaceId]
+ if existing_host.resource.name not in models['connections']:
+ models['connections'][existing_host.resource.name] = {}
+ models['connections'][existing_host.resource.name][iface['profile_name']] = []
+ for connection in iface['connections']:
+ network_id = connection['network']
+ net = models['networks'][network_id]
+ connection = NetworkConnection(vlan_is_tagged=connection['tagged'], network=net)
+ models['connections'][existing_host.resource.name][iface['profile_name']].append(connection)
+ bundle.xml = xmlData
+ self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
+
+ def decomposeXml(self, xmlString):
+ """
+ This function takes in an xml doc from our front end
+ and returns dictionaries that map cellIds to the xml
+ nodes themselves. There is no unpacking of the
+ xml objects, just grouping and organizing
+ """
+
+ connections = {}
+ networks = {}
+ hosts = {}
+ interfaces = {}
+ network_ports = {}
+
+ xmlDom = minidom.parseString(xmlString)
+ root = xmlDom.documentElement.firstChild
+ for cell in root.childNodes:
+ cellId = cell.getAttribute('id')
+ group = cellId.split("_")[0]
+ parentGroup = cell.getAttribute("parent").split("_")[0]
+ # place cell into correct group
+
+ if cell.getAttribute("edge"):
+ connections[cellId] = cell
+
+ elif "network" in group:
+ networks[cellId] = cell
+
+ elif "host" in group:
+ hosts[cellId] = cell
+
+ elif "host" in parentGroup:
+ interfaces[cellId] = cell
+
+ # make network ports also map to thier network
+ elif "network" in parentGroup:
+ network_ports[cellId] = cell.getAttribute("parent") # maps port ID to net ID
+
+ return connections, networks, hosts, interfaces, network_ports
+
+ # serialize and deserialize xml from mxGraph
+ def parseXml(self, xmlString):
+ networks = {} # maps net name to network object
+ hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks
+ interfaces = {} # maps id -> interface
+ untagged_ifaces = set() # used to check vlan config
+ network_names = set() # used to check network names
+ xml_connections, xml_nets, xml_hosts, xml_ifaces, xml_ports = self.decomposeXml(xmlString)
+
+ # parse Hosts
+ for cellId, cell in xml_hosts.items():
+ cell_json_str = cell.getAttribute("value")
+ cell_json = json.loads(cell_json_str)
+ host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']}
+ hosts[cellId] = host
+
+ # parse networks
+ for cellId, cell in xml_nets.items():
+ escaped_json_str = cell.getAttribute("value")
+ json_str = escaped_json_str.replace('&quot;', '"')
+ net_info = json.loads(json_str)
+ net_name = net_info['name']
+ public = net_info['public']
+ if net_name in network_names:
+ raise NetworkExistsException("Non unique network name found")
+ network = {"name": net_name, "public": public, "id": cellId}
+ networks[cellId] = network
+ network_names.add(net_name)
+
+ # parse interfaces
+ for cellId, cell in xml_ifaces.items():
+ parentId = cell.getAttribute('parent')
+ cell_json_str = cell.getAttribute("value")
+ cell_json = json.loads(cell_json_str)
+ iface = {"name": cellId, "connections": [], "profile_name": cell_json['name']}
+ hosts[parentId]['interfaces'].append(cellId)
+ interfaces[cellId] = iface
+
+ # parse connections
+ for cellId, cell in xml_connections.items():
+ 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 interfaces:
+ interface = interfaces[src]
+ network = networks[xml_ports[tgt]]
+ else:
+ interface = interfaces[tgt]
+ network = networks[xml_ports[src]]
+
+ if not tagged:
+ if interface['name'] in untagged_ifaces:
+ raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
+ untagged_ifaces.add(interface['name'])
+
+ # add connection to interface
+ interface['connections'].append({"tagged": tagged, "network": network['id']})
+
+ return hosts, interfaces, networks
+
+
+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.set_valid("Step Completed")
+
+ else:
+ self.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..5414784
--- /dev/null
+++ b/dashboard/src/workflow/snapshot_workflow.py
@@ -0,0 +1,119 @@
+##############################################################################
+# 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.utils import timezone
+import json
+
+from booking.models import Booking
+from resource_inventory.models import Host, Image
+from workflow.models import WorkflowStep
+from workflow.forms import BasicMetaForm, SnapshotHostSelectForm
+
+
+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 = timezone.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.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.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.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()
+ name = self.repo_get(self.repo.SNAPSHOT_NAME, False)
+ desc = self.repo_get(self.repo.SNAPSHOT_DESC, False)
+ form = None
+ if name and desc:
+ form = BasicMetaForm(initial={"name": name, "description": desc})
+ else:
+ form = BasicMetaForm()
+ context['form'] = form
+ return context
+
+ def post_render(self, request):
+ form = BasicMetaForm(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.set_valid("Success")
+ else:
+ self.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..0c558fc
--- /dev/null
+++ b/dashboard/src/workflow/sw_bundle_workflow.py
@@ -0,0 +1,198 @@
+##############################################################################
+# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.forms import formset_factory
+
+from workflow.models import WorkflowStep
+from workflow.forms import BasicMetaForm, HostSoftwareDefinitionForm
+from workflow.booking_workflow import Abstract_Resource_Select
+from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration
+
+
+class SWConf_Resource_Select(Abstract_Resource_Select):
+ workflow_type = "configuration"
+
+
+class Define_Software(WorkflowStep):
+ template = 'config_bundle/steps/define_software.html'
+ title = "Pick Software"
+ description = "Choose the opnfv and image of your machines"
+ short_title = "host config"
+
+ def build_filter_data(self, hosts_data):
+ """
+ returns a 2D array of images to exclude
+ based on the ordering of the passed
+ hosts_data
+ """
+ filter_data = []
+ user = self.repo_get(self.repo.SESSION_USER)
+ lab = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE).lab
+ for i, host_data in enumerate(hosts_data):
+ host = GenericHost.objects.get(pk=host_data['host_id'])
+ wrong_owner = Image.objects.exclude(owner=user).exclude(public=True)
+ wrong_host = Image.objects.exclude(host_type=host.profile)
+ wrong_lab = Image.objects.exclude(from_lab=lab)
+ excluded_images = wrong_owner | wrong_host | wrong_lab
+ filter_data.append([])
+ for image in excluded_images:
+ filter_data[i].append(image.pk)
+ return filter_data
+
+ def create_hostformset(self, hostlist, data=None):
+ hosts_initial = []
+ host_configs = self.repo_get(self.repo.CONFIG_MODELS, {}).get("host_configs", False)
+ if host_configs:
+ for config in host_configs:
+ hosts_initial.append({
+ 'host_id': config.host.id,
+ 'host_name': config.host.resource.name,
+ 'headnode': config.is_head_node,
+ 'image': config.image
+ })
+ else:
+ for host in hostlist:
+ hosts_initial.append({
+ 'host_id': host.id,
+ 'host_name': host.resource.name
+ })
+
+ HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0)
+ filter_data = self.build_filter_data(hosts_initial)
+
+ class SpecialHostFormset(HostFormset):
+ def get_form_kwargs(self, index):
+ kwargs = super(SpecialHostFormset, self).get_form_kwargs(index)
+ if index is not None:
+ kwargs['imageQS'] = Image.objects.exclude(pk__in=filter_data[index])
+ return kwargs
+
+ if data:
+ return SpecialHostFormset(data, initial=hosts_initial)
+ return SpecialHostFormset(initial=hosts_initial)
+
+ def get_host_list(self, grb=None):
+ if grb is None:
+ grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False)
+ if not grb:
+ return []
+ if grb.id:
+ return 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.SELECTED_GRESOURCE_BUNDLE, False)
+
+ if grb:
+ context["grb"] = grb
+ formset = self.create_hostformset(self.get_host_list(grb))
+ context["formset"] = formset
+ context['headnode'] = self.repo_get(self.repo.CONFIG_MODELS, {}).get("headnode_index", 1)
+ else:
+ context["error"] = "Please select a resource first"
+ self.set_invalid("Step requires information that is not yet provided by previous step")
+
+ return context
+
+ def post_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, {})
+
+ hosts = self.get_host_list()
+ models['headnode_index'] = request.POST.get("headnode", 1)
+ formset = self.create_hostformset(hosts, data=request.POST)
+ has_headnode = False
+ if formset.is_valid():
+ models['host_configs'] = []
+ confirm_hosts = []
+ for i, form in enumerate(formset):
+ host = hosts[i]
+ image = form.cleaned_data['image']
+ headnode = form.cleaned_data['headnode']
+ if headnode:
+ has_headnode = True
+ bundle = models['bundle']
+ hostConfig = HostConfiguration(
+ host=host,
+ image=image,
+ bundle=bundle,
+ is_head_node=headnode
+ )
+ models['host_configs'].append(hostConfig)
+ confirm_hosts.append({
+ "name": host.resource.name,
+ "image": image.name,
+ "headnode": headnode
+ })
+
+ if not has_headnode:
+ self.set_invalid('Must have one "Headnode" per POD')
+ return self.render(request)
+
+ self.repo_put(self.repo.CONFIG_MODELS, models)
+ if "configuration" not in confirm:
+ confirm['configuration'] = {}
+ confirm['configuration']['hosts'] = confirm_hosts
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+ self.set_valid("Completed")
+ else:
+ self.set_invalid("Please complete all fields")
+
+ return self.render(request)
+
+
+class Config_Software(WorkflowStep):
+ template = 'config_bundle/steps/config_software.html'
+ title = "Other Info"
+ description = "Give your software config a name, description, and other stuff"
+ short_title = "config info"
+
+ def get_context(self):
+ context = super(Config_Software, self).get_context()
+
+ initial = {}
+ models = self.repo_get(self.repo.CONFIG_MODELS, {})
+ bundle = models.get("bundle", False)
+ if bundle:
+ initial['name'] = bundle.name
+ initial['description'] = bundle.description
+ context["form"] = BasicMetaForm(initial=initial)
+ return context
+
+ def post_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, {})
+ if "configuration" not in confirm:
+ confirm['configuration'] = {}
+
+ form = BasicMetaForm(request.POST)
+ if form.is_valid():
+ models['bundle'].name = form.cleaned_data['name']
+ models['bundle'].description = form.cleaned_data['description']
+
+ confirm['configuration']['name'] = form.cleaned_data['name']
+ confirm['configuration']['description'] = form.cleaned_data['description']
+ self.set_valid("Complete")
+ else:
+ self.set_invalid("Please correct the errors shown below")
+
+ self.repo_put(self.repo.CONFIG_MODELS, models)
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ 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..380102a
--- /dev/null
+++ b/dashboard/src/workflow/tests/test_steps.py
@@ -0,0 +1,281 @@
+##############################################################################
+# 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
+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 Define_Hardware, Define_Nets, Resource_Meta_Info, Host_Meta_Info
+from workflow.sw_bundle_workflow import SWConf_Resource_Select, Define_Software, Config_Software
+from workflow.booking_workflow import Booking_Resource_Select, SWConfig_Select, Booking_Meta
+from django.http import QueryDict, HttpRequest
+from django.contrib.auth.models import User
+from resource_inventory.models import (
+ Scenario,
+ Installer,
+ OPNFVRole,
+ Image,
+ GenericResourceBundle,
+ GenericHost,
+ HostProfile,
+ GenericResource,
+ ConfigBundle
+)
+
+
+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..f3df8f2
--- /dev/null
+++ b/dashboard/src/workflow/tests/test_steps_render.py
@@ -0,0 +1,43 @@
+##############################################################################
+# 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..7a53521
--- /dev/null
+++ b/dashboard/src/workflow/tests/test_workflows.py
@@ -0,0 +1,100 @@
+##############################################################################
+# 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
+from workflow.workflow_factory import WorkflowFactory
+from dashboard.populate_db import Populator
+
+
+"""
+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..5a97904
--- /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
+from django.conf import settings
+
+from workflow.views import step_view, delete_session, manager_view, viewport_view
+from workflow.models import Repository
+from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info
+from workflow.booking_workflow import SWConfig_Select, Booking_Resource_Select, Booking_Meta
+
+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$', Booking_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..7ed9031
--- /dev/null
+++ b/dashboard/src/workflow/views.py
@@ -0,0 +1,139 @@
+##############################################################################
+# 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 HttpResponseGone, JsonResponse
+from django.shortcuts import render
+from django.urls import reverse
+
+import uuid
+
+from workflow.workflow_manager import ManagerTracker, SessionManager
+from booking.models import Booking
+
+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 get_redirect_response(result):
+ if not result:
+ return {}
+
+ # need to get type of result, and switch on the type
+ # since has_result, result must be populated with a valid object
+ if isinstance(result, Booking):
+ return {
+ 'redir_url': reverse('booking:booking_detail', kwargs={'booking_id': result.id})
+ }
+ else:
+ return {}
+
+
+def delete_session(request):
+ manager = attempt_auth(request)
+
+ if not manager:
+ return HttpResponseGone("No session found that relates to current request")
+
+ not_last_workflow, result = manager.pop_workflow()
+
+ if not_last_workflow: # this was not the last workflow, so don't redirect away
+ return JsonResponse({})
+ else:
+ del ManagerTracker.managers[request.session['manager_session']]
+ return JsonResponse(get_redirect_response(result))
+
+
+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:
+ if request.GET.get('step') == 'next':
+ manager.go_next()
+ elif request.GET.get('step') == 'prev':
+ manager.go_prev()
+ else:
+ raise Exception("requested action for new step had malformed contents: " + 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:
+ if not manager.pop_workflow():
+ del ManagerTracker.managers[request.session['manager_session']]
+
+ 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..03c8126
--- /dev/null
+++ b/dashboard/src/workflow/workflow_factory.py
@@ -0,0 +1,134 @@
+##############################################################################
+# 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 Booking_Resource_Select, SWConfig_Select, Booking_Meta, OPNFV_Select
+from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info
+from workflow.sw_bundle_workflow import Config_Software, Define_Software, SWConf_Resource_Select
+from workflow.snapshot_workflow import Select_Host_Step, Image_Meta_Step
+from workflow.opnfv_workflow import Pick_Installer, Assign_Network_Roles, Assign_Host_Roles, OPNFV_Resource_Select, MetaInfo
+from workflow.models import Confirmation_Step
+
+import uuid
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class MetaStep(object):
+
+ 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.hidden = False
+ 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 Workflow(object):
+ def __init__(self, steps, repository):
+ self.repository = repository
+ self.steps = steps
+ self.active_index = 0
+
+
+class WorkflowFactory():
+ booking_steps = [
+ Booking_Resource_Select,
+ SWConfig_Select,
+ Booking_Meta,
+ OPNFV_Select,
+ ]
+
+ 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,
+ ]
+
+ opnfv_steps = [
+ OPNFV_Resource_Select,
+ Pick_Installer,
+ Assign_Network_Roles,
+ Assign_Host_Roles,
+ MetaInfo
+ ]
+
+ def conjure(self, workflow_type=None, repo=None):
+ workflow_types = [
+ self.booking_steps,
+ self.resource_steps,
+ self.config_steps,
+ self.snapshot_steps,
+ self.opnfv_steps,
+ ]
+
+ steps = self.make_steps(workflow_types[workflow_type], repository=repo)
+ return steps
+
+ def create_workflow(self, workflow_type=None, repo=None):
+ steps = self.conjure(workflow_type, repo)
+ c_step = self.make_step(Confirmation_Step, repo)
+ steps.append(c_step)
+ return Workflow(steps, repo)
+
+ def make_steps(self, step_types, repository):
+ 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)
diff --git a/dashboard/src/workflow/workflow_manager.py b/dashboard/src/workflow/workflow_manager.py
new file mode 100644
index 0000000..80b8a67
--- /dev/null
+++ b/dashboard/src/workflow/workflow_manager.py
@@ -0,0 +1,243 @@
+##############################################################################
+# 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.http import JsonResponse
+
+from booking.models import Booking
+from workflow.workflow_factory import WorkflowFactory
+from workflow.models import Repository
+from resource_inventory.models import (
+ GenericResourceBundle,
+ ConfigBundle,
+ HostConfiguration,
+ OPNFVConfig
+)
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class SessionManager():
+ def active_workflow(self):
+ return self.workflows[-1]
+
+ def __init__(self, request=None):
+ self.workflows = []
+
+ self.owner = request.user
+
+ self.factory = WorkflowFactory()
+
+ def set_step_statuses(self, superclass_type, desired_enabled=True):
+ workflow = self.active_workflow()
+ steps = workflow.steps
+ for step in steps:
+ if isinstance(step, superclass_type):
+ if desired_enabled:
+ step.enable()
+ else:
+ step.disable()
+
+ def add_workflow(self, workflow_type=None, target_id=None, **kwargs):
+ if target_id is not None:
+ self.prefill_repo(target_id, workflow_type)
+
+ repo = Repository()
+ if(len(self.workflows) >= 1):
+ defaults = self.workflows[-1].repository.get_child_defaults()
+ repo.set_defaults(defaults)
+ repo.el[repo.HAS_RESULT] = False
+ repo.el[repo.SESSION_USER] = self.owner
+ repo.el[repo.SESSION_MANAGER] = self
+ self.workflows.append(
+ self.factory.create_workflow(
+ workflow_type=workflow_type,
+ repo=repo
+ )
+ )
+
+ def pop_workflow(self):
+ multiple_wfs = len(self.workflows) > 1
+ if multiple_wfs:
+ if self.workflows[-1].repository.el[Repository.RESULT]: # move result
+ key = self.workflows[-1].repository.el[Repository.RESULT_KEY]
+ result = self.workflows[-1].repository.el[Repository.RESULT]
+ self.workflows[-2].repository.el[key] = result
+ self.workflows.pop()
+ current_repo = self.workflows[-1].repository
+ return (multiple_wfs, current_repo.el[current_repo.RESULT])
+
+ def status(self, request):
+ try:
+ meta_json = []
+ for step in self.active_workflow().steps:
+ meta_json.append(step.to_json())
+ responsejson = {}
+ responsejson["steps"] = meta_json
+ responsejson["active"] = self.active_workflow().repository.el['active_step']
+ responsejson["workflow_count"] = len(self.workflows)
+ return JsonResponse(responsejson, safe=False)
+ except Exception:
+ 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.active_workflow().steps[self.active_workflow().active_index].post_render(request)
+ return self.active_workflow().steps[self.active_workflow().active_index].render(request)
+
+ def post_render(self, request):
+ return self.active_workflow().steps[self.active_workflow().active_index].post_render(request)
+
+ def get_active_step(self):
+ return self.active_workflow().steps[self.active_workflow().active_index]
+
+ def go_next(self, **kwargs):
+ # need to verify current step is valid to allow this
+ if self.get_active_step().valid < 200:
+ return
+ next_step = self.active_workflow().active_index + 1
+ if next_step >= len(self.active_workflow().steps):
+ raise Exception("Out of bounds request for step")
+ while not self.active_workflow().steps[next_step].enabled:
+ next_step += 1
+ self.active_workflow().repository.el['active_step'] = next_step
+ self.active_workflow().active_index = next_step
+
+ def go_prev(self, **kwargs):
+ prev_step = self.active_workflow().active_index - 1
+ if prev_step < 0:
+ raise Exception("Out of bounds request for step")
+ while not self.active_workflow().steps[prev_step].enabled:
+ prev_step -= 1
+ self.active_workflow().repository.el['active_step'] = prev_step
+ self.active_workflow().active_index = prev_step
+
+ 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.active_workflow().repository.el[self.active_workflow().repository.BOOKING_MODELS] = models
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirmation
+ self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = self.make_grb_models(booking.resource.template)
+ self.active_workflow().repository.el[self.active_workflow().repository.SELECTED_GRESOURCE_BUNDLE] = self.make_grb_models(booking.resource.template)['bundle']
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = self.make_config_models(booking.config_bundle)
+
+ def prefill_resource(self, resource):
+ models = self.make_grb_models(resource)
+ confirm = self.make_grb_confirm(resource)
+ self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = models
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm
+
+ def prefill_config(self, config):
+ models = self.make_config_models(config)
+ confirm = self.make_config_confirm(config)
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = models
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm
+ grb_models = self.make_grb_models(config.bundle)
+ self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = grb_models
+
+ def make_grb_models(self, resource):
+ models = self.active_workflow().repository.el.get(self.active_workflow().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.active_workflow().repository.el.get(self.active_workflow().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.active_workflow().repository.el.get(self.active_workflow().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.active_workflow().repository.el.get(self.active_workflow().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.active_workflow().repository.el.get(self.active_workflow().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.active_workflow().repository.el.get(self.active_workflow().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