diff options
Diffstat (limited to 'src/workflow')
-rw-r--r-- | src/workflow/__init__.py | 8 | ||||
-rw-r--r-- | src/workflow/apps.py | 15 | ||||
-rw-r--r-- | src/workflow/booking_workflow.py | 220 | ||||
-rw-r--r-- | src/workflow/forms.py | 448 | ||||
-rw-r--r-- | src/workflow/models.py | 732 | ||||
-rw-r--r-- | src/workflow/opnfv_workflow.py | 299 | ||||
-rw-r--r-- | src/workflow/resource_bundle_workflow.py | 463 | ||||
-rw-r--r-- | src/workflow/snapshot_workflow.py | 119 | ||||
-rw-r--r-- | src/workflow/sw_bundle_workflow.py | 198 | ||||
-rw-r--r-- | src/workflow/tests/__init__.py | 8 | ||||
-rw-r--r-- | src/workflow/tests/constants.py | 198 | ||||
-rw-r--r-- | src/workflow/tests/test_steps.py | 281 | ||||
-rw-r--r-- | src/workflow/tests/test_steps_render.py | 43 | ||||
-rw-r--r-- | src/workflow/tests/test_workflows.py | 100 | ||||
-rw-r--r-- | src/workflow/urls.py | 34 | ||||
-rw-r--r-- | src/workflow/views.py | 139 | ||||
-rw-r--r-- | src/workflow/workflow_factory.py | 134 | ||||
-rw-r--r-- | src/workflow/workflow_manager.py | 243 |
18 files changed, 3682 insertions, 0 deletions
diff --git a/src/workflow/__init__.py b/src/workflow/__init__.py new file mode 100644 index 0000000..e0408fa --- /dev/null +++ b/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/src/workflow/apps.py b/src/workflow/apps.py new file mode 100644 index 0000000..adc2738 --- /dev/null +++ b/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/src/workflow/booking_workflow.py b/src/workflow/booking_workflow.py new file mode 100644 index 0000000..42372ce --- /dev/null +++ b/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/src/workflow/forms.py b/src/workflow/forms.py new file mode 100644 index 0000000..ee44ecd --- /dev/null +++ b/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/src/workflow/models.py b/src/workflow/models.py new file mode 100644 index 0000000..6c6bd9a --- /dev/null +++ b/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/src/workflow/opnfv_workflow.py b/src/workflow/opnfv_workflow.py new file mode 100644 index 0000000..7d499ec --- /dev/null +++ b/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/src/workflow/resource_bundle_workflow.py b/src/workflow/resource_bundle_workflow.py new file mode 100644 index 0000000..06737d2 --- /dev/null +++ b/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('"', '"') + 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('"', '"') + 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/src/workflow/snapshot_workflow.py b/src/workflow/snapshot_workflow.py new file mode 100644 index 0000000..5414784 --- /dev/null +++ b/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/src/workflow/sw_bundle_workflow.py b/src/workflow/sw_bundle_workflow.py new file mode 100644 index 0000000..0c558fc --- /dev/null +++ b/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/src/workflow/tests/__init__.py b/src/workflow/tests/__init__.py new file mode 100644 index 0000000..4f0437d --- /dev/null +++ b/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/src/workflow/tests/constants.py b/src/workflow/tests/constants.py new file mode 100644 index 0000000..f94a949 --- /dev/null +++ b/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="{"vlan_id":"500","name":"net"}" 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="{"tagged":true}" style="strokeColor=red" edge="1" parent="1" source="2" target="13"> +<mxGeometry relative="1" as="geometry"/> +</mxCell> +<mxCell id="60" value="{"tagged":false}" style="strokeColor=red" edge="1" parent="1" source="7" target="17"> +<mxGeometry relative="1" as="geometry"/> +</mxCell> +</root> +</mxGraphModel> +""" diff --git a/src/workflow/tests/test_steps.py b/src/workflow/tests/test_steps.py new file mode 100644 index 0000000..380102a --- /dev/null +++ b/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/src/workflow/tests/test_steps_render.py b/src/workflow/tests/test_steps_render.py new file mode 100644 index 0000000..f3df8f2 --- /dev/null +++ b/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/src/workflow/tests/test_workflows.py b/src/workflow/tests/test_workflows.py new file mode 100644 index 0000000..7a53521 --- /dev/null +++ b/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/src/workflow/urls.py b/src/workflow/urls.py new file mode 100644 index 0000000..5a97904 --- /dev/null +++ b/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/src/workflow/views.py b/src/workflow/views.py new file mode 100644 index 0000000..7ed9031 --- /dev/null +++ b/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/src/workflow/workflow_factory.py b/src/workflow/workflow_factory.py new file mode 100644 index 0000000..03c8126 --- /dev/null +++ b/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/src/workflow/workflow_manager.py b/src/workflow/workflow_manager.py new file mode 100644 index 0000000..80b8a67 --- /dev/null +++ b/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 |