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