diff options
Diffstat (limited to 'dashboard/src/workflow')
-rw-r--r-- | dashboard/src/workflow/booking_workflow.py | 313 | ||||
-rw-r--r-- | dashboard/src/workflow/forms.py | 541 | ||||
-rw-r--r-- | dashboard/src/workflow/models.py | 457 | ||||
-rw-r--r-- | dashboard/src/workflow/opnfv_workflow.py | 299 | ||||
-rw-r--r-- | dashboard/src/workflow/resource_bundle_workflow.py | 387 | ||||
-rw-r--r-- | dashboard/src/workflow/snapshot_workflow.py | 27 | ||||
-rw-r--r-- | dashboard/src/workflow/sw_bundle_workflow.py | 224 | ||||
-rw-r--r-- | dashboard/src/workflow/urls.py | 4 | ||||
-rw-r--r-- | dashboard/src/workflow/views.py | 43 | ||||
-rw-r--r-- | dashboard/src/workflow/workflow_factory.py | 78 | ||||
-rw-r--r-- | dashboard/src/workflow/workflow_manager.py | 190 |
11 files changed, 1482 insertions, 1081 deletions
diff --git a/dashboard/src/workflow/booking_workflow.py b/dashboard/src/workflow/booking_workflow.py index cd12ab6..42372ce 100644 --- a/dashboard/src/workflow/booking_workflow.py +++ b/dashboard/src/workflow/booking_workflow.py @@ -8,209 +8,136 @@ ############################################################################## 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 booking.models import Booking -from workflow.models import WorkflowStep -from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm -from resource_inventory.models import GenericResourceBundle, ResourceBundle, ConfigBundle +from workflow.models import WorkflowStep, AbstractSelectOrCreate +from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm, OPNFVSelectForm +from resource_inventory.models import GenericResourceBundle, ConfigBundle, OPNFVConfig -class Resource_Select(WorkflowStep): - template = 'booking/steps/resource_select.html' +""" +subclassing notes: + subclasses have to define the following class attributes: + self.repo_key: main output of step, where the selected/created single selector + result is placed at the end + self.confirm_key: +""" + + +class Abstract_Resource_Select(AbstractSelectOrCreate): + form = ResourceSelectorForm + template = 'dashboard/genericselect.html' title = "Select Resource" description = "Select a resource template to use for your deployment" short_title = "pod select" def __init__(self, *args, **kwargs): - super(Resource_Select, self).__init__(*args, **kwargs) - self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE - self.repo_check_key = False - self.confirm_key = "booking" + super().__init__(*args, **kwargs) + self.select_repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE + self.confirm_key = self.workflow_type - def get_default_entry(self): - return None + def alert_bundle_missing(self): + self.set_invalid("Please select a valid resource bundle") - 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) + def get_form_queryset(self): 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'] - 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) + qs = GenericResourceBundle.objects.filter(owner=user) + return qs + def get_page_context(self): + return { + 'select_type': 'resource', + 'select_type_title': 'Resource Bundle', + 'addable_type_num': 1 + } -class Booking_Resource_Select(Resource_Select): + def put_confirm_info(self, bundle): + confirm_dict = self.repo_get(self.repo.CONFIRMATION) + if self.confirm_key not in confirm_dict: + confirm_dict[self.confirm_key] = {} + confirm_dict[self.confirm_key]["Resource Template"] = bundle.name + self.repo_put(self.repo.CONFIRMATION, confirm_dict) - 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 +class Booking_Resource_Select(Abstract_Resource_Select): + workflow_type = "booking" - 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' +class SWConfig_Select(AbstractSelectOrCreate): title = "Select Software Configuration" description = "Choose the software and related configurations you want to have used for your deployment" short_title = "pod config" + form = SWConfigSelectorForm - def post_render(self, request): - form = SWConfigSelectorForm(request.POST) - if form.is_valid(): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE + self.confirm_key = "booking" - 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") + def alert_bundle_missing(self): + self.set_invalid("Please select a valid pod config") - 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) + def get_form_queryset(self): + user = self.repo_get(self.repo.SESSION_USER) + grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE) + qs = ConfigBundle.objects.filter(owner=user).filter(bundle=grb) + return qs - return self.render(request) + def put_confirm_info(self, bundle): + confirm_dict = self.repo_get(self.repo.CONFIRMATION) + if self.confirm_key not in confirm_dict: + confirm_dict[self.confirm_key] = {} + confirm_dict[self.confirm_key]["Software Configuration"] = bundle.name + self.repo_put(self.repo.CONFIRMATION, confirm_dict) - def get_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 + def get_page_context(self): + return { + 'select_type': 'swconfig', + 'select_type_title': 'Software Config', + 'addable_type_num': 2 + } + + +class OPNFV_EnablePicker(object): + pass + + +class OPNFV_Select(AbstractSelectOrCreate, OPNFV_EnablePicker): + title = "Choose an OPNFV Config" + description = "Choose or create a description of how you want to deploy OPNFV" + short_title = "opnfv config" + form = OPNFVSelectForm + enabled = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.select_repo_key = self.repo.SELECTED_OPNFV_CONFIG + self.confirm_key = "booking" + + def alert_bundle_missing(self): + self.set_invalid("Please select a valid OPNFV config") + + def get_form_queryset(self): + cb = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) + qs = OPNFVConfig.objects.filter(bundle=cb) + return qs + + def put_confirm_info(self, config): + confirm_dict = self.repo_get(self.repo.CONFIRMATION) + if self.confirm_key not in confirm_dict: + confirm_dict[self.confirm_key] = {} + confirm_dict[self.confirm_key]["OPNFV Configuration"] = config.name + self.repo_put(self.repo.CONFIRMATION, confirm_dict) + + def get_page_context(self): + return { + 'select_type': 'opnfv', + 'select_type_title': 'OPNFV Config', + 'addable_type_num': 4 + } class Booking_Meta(WorkflowStep): @@ -235,23 +162,17 @@ class Booking_Meta(WorkflowStep): initial['info_file'] = info users = models.get("collaborators", []) for user in users: - default.append(user.id) + default.append(user.userprofile) except Exception: 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 + owner = self.repo_get(self.repo.SESSION_USER) - context['form'] = BookingMetaForm(initial=initial, chosen_users=default, default_user=default_user) + context['form'] = BookingMetaForm(initial=initial, user_initial=default, owner=owner) return context def post_render(self, request): - form = BookingMetaForm(data=request.POST) - context = self.get_context() + form = BookingMetaForm(data=request.POST, owner=request.user) forms = self.repo_get(self.repo.BOOKING_FORMS, {}) @@ -274,15 +195,16 @@ class Booking_Meta(WorkflowStep): for key in ['length', 'project', 'purpose']: confirm['booking'][key] = form.cleaned_data[key] - user_data = form.cleaned_data['users'] + if form.cleaned_data["deploy_opnfv"]: + self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=True) + else: + self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=False) + + userprofile_list = form.cleaned_data['users'] confirm['booking']['collaborators'] = [] - 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) + for userprofile in userprofile_list: + models['collaborators'].append(userprofile.user) + confirm['booking']['collaborators'].append(userprofile.user.username) info_file = form.cleaned_data.get("info_file", False) if info_file: @@ -291,9 +213,8 @@ class Booking_Meta(WorkflowStep): 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") + self.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) + self.set_invalid("Please complete the fields highlighted in red to continue") + return self.render(request) diff --git a/dashboard/src/workflow/forms.py b/dashboard/src/workflow/forms.py index feb32f2..ee44ecd 100644 --- a/dashboard/src/workflow/forms.py +++ b/dashboard/src/workflow/forms.py @@ -9,40 +9,37 @@ import django.forms as forms -from django.forms import widgets +from django.forms import widgets, ValidationError from django.utils.safestring import mark_safe from django.template.loader import render_to_string from django.forms.widgets import NumberInput +import json + from account.models import Lab from account.models import UserProfile from resource_inventory.models import ( - GenericResourceBundle, - ConfigBundle, OPNFVRole, - Image, Installer, - Scenario + Scenario, ) +from booking.lib import get_user_items, get_user_field_opts class SearchableSelectMultipleWidget(widgets.SelectMultiple): template_name = 'dashboard/searchable_select_multiple.html' def __init__(self, attrs=None): - self.items = attrs['set'] + self.items = attrs['items'] self.show_from_noentry = attrs['show_from_noentry'] self.show_x_results = attrs['show_x_results'] - self.results_scrollable = attrs['scrollable'] + self.results_scrollable = attrs['results_scrollable'] self.selectable_limit = attrs['selectable_limit'] self.placeholder = attrs['placeholder'] self.name = attrs['name'] - self.initial = attrs.get("initial", "") - self.default_entry = attrs.get("default_entry", "") - self.edit = attrs.get("edit", False) - self.wf_type = attrs.get("wf_type") + self.initial = attrs.get("initial", []) - super(SearchableSelectMultipleWidget, self).__init__(attrs) + super(SearchableSelectMultipleWidget, self).__init__() def render(self, name, value, attrs=None, renderer=None): @@ -59,127 +56,160 @@ class SearchableSelectMultipleWidget(widgets.SelectMultiple): '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): +class SearchableSelectMultipleField(forms.Field): + def __init__(self, *args, required=True, widget=None, label=None, disabled=False, + items=None, queryset=None, show_from_noentry=True, show_x_results=-1, + results_scrollable=False, selectable_limit=-1, placeholder="search here", + name="searchable_select", initial=[], **kwargs): + """from the documentation: + # required -- Boolean that specifies whether the field is required. + # True by default. + # widget -- A Widget class, or instance of a Widget class, that should + # be used for this Field when displaying it. Each Field has a + # default Widget that it'll use if you don't specify this. In + # most cases, the default widget is TextInput. + # label -- A verbose name for this field, for use in displaying this + # field in a form. By default, Django will use a "pretty" + # version of the form field name, if the Field is part of a + # Form. + # initial -- A value to use in this Field's initial display. This value + # is *not* used as a fallback if data isn't given. + # help_text -- An optional string to use as "help text" for this Field. + # error_messages -- An optional dictionary to override the default + # messages that the field will raise. + # show_hidden_initial -- Boolean that specifies if it is needed to render a + # hidden widget with initial value after widget. + # validators -- List of additional validators to use + # localize -- Boolean that specifies if the field should be localized. + # disabled -- Boolean that specifies whether the field is disabled, that + # is its widget is shown in the form but not editable. + # label_suffix -- Suffix to be added to the label. Overrides + # form's label_suffix. + """ - 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']) + self.widget = widget + if self.widget is None: + self.widget = SearchableSelectMultipleWidget( + attrs={ + 'items': items, + 'initial': [obj.id for obj in initial], + 'show_from_noentry': show_from_noentry, + 'show_x_results': show_x_results, + 'results_scrollable': results_scrollable, + 'selectable_limit': selectable_limit, + 'placeholder': placeholder, + 'name': name, + 'disabled': disabled + } + ) + self.disabled = disabled + self.queryset = queryset + self.selectable_limit = selectable_limit - attrs = self.build_search_widget_attrs(chosen_resource, bundle, edit, queryset) + super().__init__(disabled=disabled, **kwargs) - self.fields['generic_resource_bundle'] = forms.CharField( - widget=SearchableSelectMultipleWidget(attrs=attrs) - ) + self.required = required - 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 + def clean(self, data): + data = data[0] + if not data: + if self.required: + raise ValidationError("Nothing was selected") 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", + return [] + data_as_list = json.loads(data) + if self.selectable_limit != -1: + if len(data_as_list) > self.selectable_limit: + raise ValidationError("Too many items were selected") + + items = [] + for elem in data_as_list: + items.append(self.queryset.get(id=elem)) + + return items + + +class SearchableSelectAbstractForm(forms.Form): + def __init__(self, *args, queryset=None, initial=[], **kwargs): + self.queryset = queryset + items = self.generate_items(self.queryset) + options = self.generate_options() + + super(SearchableSelectAbstractForm, self).__init__(*args, **kwargs) + self.fields['searchable_select'] = SearchableSelectMultipleField( + initial=initial, + items=items, + queryset=self.queryset, + **options + ) + + def get_validated_bundle(self): + bundles = self.cleaned_data['searchable_select'] + if len(bundles) < 1: # don't need to check for >1, as field does that for us + raise ValidationError("No bundle was selected") + return bundles[0] + + def generate_items(self, queryset): + raise Exception("SearchableSelectAbstractForm does not implement concrete generate_items()") + + def generate_options(self, disabled=False): + return { + 'show_from_noentry': True, 'show_x_results': -1, - 'scrollable': "true", + 'results_scrollable': True, 'selectable_limit': 1, - 'name': "generic_resource_bundle", - 'placeholder': "resource", - 'initial': chosen_resource, - 'edit': edit, - 'wf_type': 1 + 'placeholder': 'Search for a Bundle', + 'name': 'searchable_select', + 'disabled': False } - return attrs -class SWConfigSelectorForm(forms.Form): +class SWConfigSelectorForm(SearchableSelectAbstractForm): + def generate_items(self, queryset): + items = {} - 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) - ) + for bundle in queryset: + items[bundle.id] = { + 'expanded_name': bundle.name, + 'small_name': bundle.owner.username, + 'string': bundle.description, + 'id': bundle.id + } - 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) + return items + + +class OPNFVSelectForm(SearchableSelectAbstractForm): + def generate_items(self, queryset): + items = {} 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 + items[config.id] = { + 'expanded_name': config.name, + 'small_name': config.bundle.owner.username, + 'string': config.description, + 'id': config.id + } + + return items + + +class ResourceSelectorForm(SearchableSelectAbstractForm): + def generate_items(self, queryset): + items = {} + + for bundle in queryset: + items[bundle.id] = { + 'expanded_name': bundle.name, + 'small_name': bundle.owner.username, + 'string': bundle.description, + 'id': bundle.id + } + + return items class BookingMetaForm(forms.Form): @@ -188,195 +218,126 @@ class BookingMetaForm(forms.Form): widget=NumberInput( attrs={ "type": "range", - 'min': "0", + 'min': "1", "max": "21", - "value": "0" + "value": "1" } ) ) purpose = forms.CharField(max_length=1000) project = forms.CharField(max_length=400) info_file = forms.CharField(max_length=1000, required=False) + deploy_opnfv = forms.BooleanField(required=False) - def __init__(self, data=None, *args, **kwargs): - chosen_users = [] - if "default_user" in kwargs: - default_user = kwargs.pop("default_user") - else: - default_user = "you" - self.default_user = default_user - if "chosen_users" in kwargs: - chosen_users = kwargs.pop("chosen_users") - elif data and "users" in data: - chosen_users = data.getlist("users") - else: - pass + def __init__(self, *args, user_initial=[], owner=None, **kwargs): + super(BookingMetaForm, self).__init__(**kwargs) - 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 + self.fields['users'] = SearchableSelectMultipleField( + queryset=UserProfile.objects.select_related('user').exclude(user=owner), + initial=user_initial, + items=get_user_items(exclude=owner), + required=False, + **get_user_field_opts() ) - 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().exclude(user__username=self.default_user) - 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: - 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", - 'initial': chosen_users, - 'edit': False - } - return attrs - class MultipleSelectFilterWidget(forms.Widget): - def __init__(self, attrs=None): - super(MultipleSelectFilterWidget, self).__init__(attrs) - self.attrs = attrs + def __init__(self, *args, display_objects=None, filter_items=None, neighbors=None, **kwargs): + super(MultipleSelectFilterWidget, self).__init__(*args, **kwargs) + self.display_objects = display_objects + self.filter_items = filter_items + self.neighbors = neighbors self.template_name = "dashboard/multiple_select_filter_widget.html" def render(self, name, value, attrs=None, renderer=None): - attrs = self.attrs - self.context = self.get_context(name, value, attrs) - html = render_to_string(self.template_name, context=self.context) + context = self.get_context(name, value, attrs) + html = render_to_string(self.template_name, context=context) return mark_safe(html) def get_context(self, name, value, attrs): - return attrs + return { + 'display_objects': self.display_objects, + 'neighbors': self.neighbors, + 'filter_items': self.filter_items, + 'initial_value': value + } class MultipleSelectFilterField(forms.Field): - def __init__(self, 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 __init__(self, **kwargs): + self.initial = kwargs.get("initial") + super().__init__(**kwargs) - def clean(data): - """ - This method will raise a django.forms.ValidationError or return clean data - """ - return data + def to_python(self, value): + return json.loads(value) class FormUtils: @staticmethod - def getLabData(): + def getLabData(multiple_hosts=False): """ Gets all labs and thier host profiles and returns a serialized version the form can understand. Should be rewritten with a related query to make it faster - Should be moved outside of global scope """ + # javascript truthy variables + true = 1 + false = 0 + if multiple_hosts: + multiple_hosts = true + else: + multiple_hosts = false labs = {} hosts = {} items = {} - mapping = {} + neighbors = {} 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 + lab_node = { + 'id': "lab_" + str(lab.lab_user.id), + 'model_id': lab.lab_user.id, + 'name': lab.name, + 'description': lab.description, + 'selected': false, + 'selectable': true, + 'follow': false, + 'multiple': false, + 'class': 'lab' + } + if multiple_hosts: + # "follow" this lab node to discover more hosts if allowed + lab_node['follow'] = true + items[lab_node['id']] = lab_node + neighbors[lab_node['id']] = [] + labs[lab_node['id']] = lab_node + for host in lab.hostprofiles.all(): - 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())] + host_node = { + 'form': {"name": "host_name", "type": "text", "placeholder": "hostname"}, + 'id': "host_" + str(host.id), + 'model_id': host.id, + 'name': host.name, + 'description': host.description, + 'selected': false, + 'selectable': true, + 'follow': false, + 'multiple': multiple_hosts, + 'class': 'host' + } + if multiple_hosts: + host_node['values'] = [] # place to store multiple values + items[host_node['id']] = host_node + neighbors[lab_node['id']].append(host_node['id']) + if host_node['id'] not in neighbors: + neighbors[host_node['id']] = [] + neighbors[host_node['id']].append(lab_node['id']) + hosts[host_node['id']] = host_node + + display_objects = [("lab", labs.values()), ("host", hosts.values())] context = { - 'filter_objects': filter_objects, - 'mapping': mapping, - 'items': items + 'display_objects': display_objects, + 'neighbors': neighbors, + 'filter_items': items } return context @@ -384,14 +345,10 @@ class FormUtils: 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 + attrs = FormUtils.getLabData(multiple_hosts=True) self.fields['filter_field'] = MultipleSelectFilterField( - widget=MultipleSelectFilterWidget( - attrs=attrs - ) + widget=MultipleSelectFilterWidget(**attrs) ) @@ -424,20 +381,14 @@ class NetworkConfigurationForm(forms.Form): 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()) + headnode = forms.BooleanField(required=False, widget=forms.HiddenInput) - -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) + def __init__(self, *args, **kwargs): + imageQS = kwargs.pop("imageQS") + super(HostSoftwareDefinitionForm, self).__init__(*args, **kwargs) + self.fields['image'] = forms.ModelChoiceField(queryset=imageQS) class WorkflowSelectionForm(forms.Form): @@ -461,9 +412,9 @@ class SnapshotHostSelectForm(forms.Form): host = forms.CharField() -class SnapshotMetaForm(forms.Form): +class BasicMetaForm(forms.Form): name = forms.CharField() - description = forms.CharField() + description = forms.CharField(widget=forms.Textarea) class ConfirmationForm(forms.Form): @@ -475,3 +426,23 @@ class ConfirmationForm(forms.Form): (False, "Cancel") ) ) + + +class OPNFVSelectionForm(forms.Form): + installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=True) + scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=True) + + +class OPNFVNetworkRoleForm(forms.Form): + role = forms.CharField(max_length=200, disabled=True, required=False) + + def __init__(self, *args, config_bundle, **kwargs): + super(OPNFVNetworkRoleForm, self).__init__(*args, **kwargs) + self.fields['network'] = forms.ModelChoiceField( + queryset=config_bundle.bundle.networks.all() + ) + + +class OPNFVHostRoleForm(forms.Form): + host_name = forms.CharField(max_length=200, disabled=True, required=False) + role = forms.ModelChoiceField(queryset=OPNFVRole.objects.all().order_by("name").distinct("name")) diff --git a/dashboard/src/workflow/models.py b/dashboard/src/workflow/models.py index 966582c..6c6bd9a 100644 --- a/dashboard/src/workflow/models.py +++ b/dashboard/src/workflow/models.py @@ -10,6 +10,8 @@ from django.shortcuts import render from django.contrib import messages +from django.http import HttpResponse +from django.utils import timezone import yaml import requests @@ -17,8 +19,9 @@ import requests from workflow.forms import ConfirmationForm from api.models import JobFactory from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException -from resource_inventory.models import Image, GenericInterface +from resource_inventory.models import Image, GenericInterface, OPNFVConfig, HostOPNFVConfig, NetworkRole from resource_inventory.resource_manager import ResourceManager +from resource_inventory.pdf_templater import PDFTemplater from notifier.manager import NotificationHandler from booking.models import Booking @@ -26,13 +29,11 @@ from booking.models import Booking class BookingAuthManager(): LFN_PROJECTS = ["opnfv"] # TODO - def parse_url(self, info_url): - """ - will return the PTL in the INFO file on success, or None - """ + def parse_github_url(self, url): + project_leads = [] try: - parts = info_url.split("/") - if parts[0].find("http") > -1: # the url include http(s):// + parts = url.split("/") + if "http" in parts[0]: # the url include http(s):// parts = parts[2:] if parts[-1] != "INFO.yaml": return None @@ -47,13 +48,94 @@ class BookingAuthManager(): info_file = requests.get(url, timeout=15).text info_parsed = yaml.load(info_file) ptl = info_parsed.get('project_lead') - if not ptl: + if ptl: + project_leads.append(ptl) + sub_ptl = info_parsed.get("subproject_lead") + if sub_ptl: + project_leads.append(sub_ptl) + + except Exception: + pass + + return project_leads + + def parse_gerrit_url(self, url): + project_leads = [] + try: + halfs = url.split("?") + parts = halfs[0].split("/") + args = halfs[1].split(";") + if "http" in parts[0]: # the url include http(s):// + parts = parts[2:] + if "f=INFO.yaml" not in args: + return None + if "gerrit.opnfv.org" not in parts[0]: return None - return ptl + try: + i = args.index("a=blob") + args[i] = "a=blob_plain" + except ValueError: + pass + # recreate url + halfs[1] = ";".join(args) + halfs[0] = "/".join(parts) + # now to download and parse file + url = "https://" + "?".join(halfs) + info_file = requests.get(url, timeout=15).text + info_parsed = yaml.load(info_file) + ptl = info_parsed.get('project_lead') + if ptl: + project_leads.append(ptl) + sub_ptl = info_parsed.get("subproject_lead") + if sub_ptl: + project_leads.append(sub_ptl) except Exception: return None + return project_leads + + def parse_opnfv_git_url(self, url): + project_leads = [] + try: + parts = url.split("/") + if "http" in parts[0]: # the url include http(s):// + parts = parts[2:] + if "INFO.yaml" not in parts[-1]: + return None + if "git.opnfv.org" not in parts[0]: + return None + if parts[-2] == "tree": + parts[-2] = "plain" + # now to download and parse file + url = "https://" + "/".join(parts) + info_file = requests.get(url, timeout=15).text + info_parsed = yaml.load(info_file) + ptl = info_parsed.get('project_lead') + if ptl: + project_leads.append(ptl) + sub_ptl = info_parsed.get("subproject_lead") + if sub_ptl: + project_leads.append(sub_ptl) + + except Exception: + return None + + return project_leads + + def parse_url(self, info_url): + """ + will return the PTL in the INFO file on success, or None + """ + if "github" in info_url: + return self.parse_github_url(info_url) + + if "gerrit.opnfv.org" in info_url: + return self.parse_gerrit_url(info_url) + + if "git.opnfv.org" in info_url: + return self.parse_opnfv_git_url(info_url) + def booking_allowed(self, booking, repo): """ This is the method that will have to change whenever the booking policy changes in the Infra @@ -61,23 +143,67 @@ class BookingAuthManager(): 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 Booking.objects.filter(owner=booking.owner, end__gt=timezone.now()).count() >= 3: + return False + if len(booking.resource.template.getHosts()) < 2: + return True # if they only have one server, we dont care if repo.BOOKING_INFO_FILE not in repo.el: return False # INFO file not provided - ptl_info = self.parse_url(repo.BOOKING_INFO_FILE) - return ptl_info and ptl_info == booking.owner.userprofile.email_addr + ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE)) + for ptl in ptl_info: + if ptl['email'] == booking.owner.userprofile.email_addr: + return True + return False -class WorkflowStep(object): +class WorkflowStepStatus(object): + UNTOUCHED = 0 + INVALID = 100 + VALID = 200 + +class WorkflowStep(object): template = 'bad_request.html' title = "Generic Step" description = "You were led here by mistake" short_title = "error" metastep = None + # phasing out metastep: + + valid = WorkflowStepStatus.UNTOUCHED + message = "" + + enabled = True + + def cleanup(self): + raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete implemented cleanup() method") + + def enable(self): + if not self.enabled: + self.enabled = True + + def disable(self): + if self.enabled: + self.cleanup() + self.enabled = False + + def set_invalid(self, message, code=WorkflowStepStatus.INVALID): + self.valid = code + self.message = message + + def set_valid(self, message, code=WorkflowStepStatus.VALID): + self.valid = code + self.message = message + + def to_json(self): + return { + 'title': self.short_title, + 'enabled': self.enabled, + 'valid': self.valid, + 'message': self.message, + } def __init__(self, id, repo=None): self.repo = repo @@ -114,29 +240,73 @@ class WorkflowStep(object): return self.repo.put(key, value, self.id) +""" +subclassing notes: + subclasses have to define the following class attributes: + self.select_repo_key: where the selected "object" or "bundle" is to be placed in the repo + self.form: the form to be used + alert_bundle_missing(): what message to display if a user does not select/selects an invalid object + get_form_queryset(): generate a queryset to be used to filter available items for the field + get_page_context(): return simple context such as page header and other info +""" + + +class AbstractSelectOrCreate(WorkflowStep): + template = 'dashboard/genericselect.html' + title = "Select a Bundle" + short_title = "select" + description = "Generic bundle selector step" + + select_repo_key = None + form = None # subclasses are expected to use a form that is a subclass of SearchableSelectGenericForm + + def alert_bundle_missing(self): # override in subclasses to change message if field isn't filled out + self.set_invalid("Please select a valid bundle") + + def post_render(self, request): + context = self.get_context() + form = self.form(request.POST, queryset=self.get_form_queryset()) + if form.is_valid(): + bundle = form.get_validated_bundle() + if not bundle: + self.alert_bundle_missing() + return render(request, self.template, context) + self.repo_put(self.select_repo_key, bundle) + self.put_confirm_info(bundle) + self.set_valid("Step Completed") + else: + self.alert_bundle_missing() + messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True) + + return self.render(request) + + def get_context(self): + default = [] + + bundle = self.repo_get(self.select_repo_key, False) + if bundle: + default.append(bundle) + + form = self.form(queryset=self.get_form_queryset(), initial=default) + + context = {'form': form, **self.get_page_context()} + context.update(super().get_context()) + + return context + + def get_page_context(): + return { + 'select_type': 'generic', + 'select_type_title': 'Generic Bundle' + } + + class Confirmation_Step(WorkflowStep): template = 'workflow/confirm.html' title = "Confirm Changes" description = "Does this all look right?" - 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 + short_title = "confirm" def get_context(self): context = super(Confirmation_Step, self).get_context() @@ -145,7 +315,6 @@ class Confirmation_Step(WorkflowStep): self.repo_get(self.repo.CONFIRMATION), default_flow_style=False ).strip() - context['vlan_warning'] = self.get_vlan_warning() return context @@ -164,9 +333,10 @@ class Confirmation_Step(WorkflowStep): 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) + else: + messages.add_message(request, messages.SUCCESS, "Confirmed") + + return HttpResponse('') elif data == "False": context["bypassed"] = "true" messages.add_message(request, messages.SUCCESS, "Canceled") @@ -175,39 +345,8 @@ class Confirmation_Step(WorkflowStep): 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(): @@ -216,6 +355,8 @@ class Repository(): RESOURCE_SELECT = "resource_select" CONFIRMATION = "confirmation" SELECTED_GRESOURCE_BUNDLE = "selected generic bundle pk" + SELECTED_CONFIG_BUNDLE = "selected config bundle pk" + SELECTED_OPNFV_CONFIG = "selected opnfv deployment config" GRESOURCE_BUNDLE_MODELS = "generic_resource_bundle_models" GRESOURCE_BUNDLE_INFO = "generic_resource_bundle_info" BOOKING = "booking" @@ -223,11 +364,11 @@ class Repository(): 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" + OPNFV_MODELS = "opnfv configuration models" SESSION_USER = "session owner user account" + SESSION_MANAGER = "session manager for current session" VALIDATED_MODEL_GRB = "valid grb config model instance in db" VALIDATED_MODEL_CONFIG = "valid config model instance in db" VALIDATED_MODEL_BOOKING = "valid booking model instance in db" @@ -238,7 +379,24 @@ class Repository(): SNAPSHOT_DESC = "description of the snapshot" BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking" + # migratory elements of segmented workflow + # each of these is the end result of a different workflow. + HAS_RESULT = "whether or not workflow has a result" + RESULT_KEY = "key for target index that result will be put into in parent" + RESULT = "result object from workflow" + + def get_child_defaults(self): + return_tuples = [] + for key in [self.SELECTED_GRESOURCE_BUNDLE, self.SESSION_USER]: + return_tuples.append((key, self.el.get(key))) + return return_tuples + + def set_defaults(self, defaults): + for key, value in defaults: + self.el[key] = value + def get(self, key, default, id): + self.add_get_history(key, id) return self.el.get(key, default) @@ -263,16 +421,33 @@ class Repository(): errors = self.make_snapshot() if errors: return errors + # if GRB WF, create it if self.GRESOURCE_BUNDLE_MODELS in self.el: errors = self.make_generic_resource_bundle() if errors: return errors + else: + self.el[self.HAS_RESULT] = True + self.el[self.RESULT_KEY] = self.SELECTED_GRESOURCE_BUNDLE + return if self.CONFIG_MODELS in self.el: errors = self.make_software_config_bundle() if errors: return errors + else: + self.el[self.HAS_RESULT] = True + self.el[self.RESULT_KEY] = self.SELECTED_CONFIG_BUNDLE + return + + if self.OPNFV_MODELS in self.el: + errors = self.make_opnfv_config() + if errors: + return errors + else: + self.el[self.HAS_RESULT] = True + self.el[self.RESULT_KEY] = self.SELECTED_OPNFV_CONFIG if self.BOOKING_MODELS in self.el: errors = self.make_booking() @@ -290,6 +465,8 @@ class Repository(): if not booking_id: return "SNAP, No booking ID provided" booking = Booking.objects.get(pk=booking_id) + if booking.start > timezone.now() or booking.end < timezone.now(): + return "Booking is not active" name = self.el.get(self.SNAPSHOT_NAME) if not name: return "SNAP, no name provided" @@ -305,6 +482,16 @@ class Repository(): image.owner = owner image.host_type = host.profile image.save() + try: + current_image = host.config.image + image.os = current_image.os + image.save() + except Exception: + pass + JobFactory.makeSnapshotTask(image, booking, host) + + self.el[self.RESULT] = image + self.el[self.HAS_RESULT] = True def make_generic_resource_bundle(self): owner = self.el[self.SESSION_USER] @@ -334,45 +521,52 @@ class Repository(): except Exception as e: return "GRB, saving hosts generated exception: " + str(e) + " CODE:0x0005" + if 'networks' in models: + for net in models['networks'].values(): + net.bundle = bundle + net.save() + if 'interfaces' in models: for interface_set in models['interfaces'].values(): for interface in interface_set: try: interface.host = interface.host interface.save() - except Exception as e: + except Exception: 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(): + if 'connections' in models: + for resource_name, mapping in models['connections'].items(): + for profile_name, connection_set in mapping.items(): interface = GenericInterface.objects.get( profile__name=profile_name, host__resource__name=resource_name, host__resource__bundle=models['bundle'] ) - for vlan in vlan_set: + for connection in connection_set: try: - vlan.save() - interface.vlans.add(vlan) + connection.network = connection.network + connection.save() + interface.connections.add(connection) except Exception as e: - return "GRB, saving vlan " + str(vlan) + " failed. Exception: " + str(e) + ". CODE:0x0017" + return "GRB, saving vlan " + str(connection) + " failed. Exception: " + str(e) + ". CODE:0x0017" else: return "GRB, no vlan set provided. CODE:0x0018" else: return "GRB no models given. CODE:0x0001" - self.el[self.VALIDATED_MODEL_GRB] = bundle + self.el[self.RESULT] = bundle + self.el[self.HAS_RESULT] = True return False def make_software_config_bundle(self): models = self.el[self.CONFIG_MODELS] if 'bundle' in models: bundle = models['bundle'] - bundle.bundle = bundle.bundle + bundle.bundle = self.el[self.SELECTED_GRESOURCE_BUNDLE] try: bundle.save() except Exception as e: @@ -403,26 +597,30 @@ class Repository(): else: pass - self.el[self.VALIDATED_MODEL_CONFIG] = bundle + self.el[self.RESULT] = 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" + selected_grb = None + + if self.SELECTED_GRESOURCE_BUNDLE in self.el: + selected_grb = self.el[self.SELECTED_GRESOURCE_BUNDLE] + else: + return "BOOK, no selected resource. CODE:0x000e" + + if self.SELECTED_CONFIG_BUNDLE not in self.el: + return "BOOK, no selected config bundle. CODE:0x001f" + + booking.config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE] + if not booking.start: return "BOOK, booking has no start. CODE:0x0010" if not booking.end: @@ -443,7 +641,6 @@ class Repository(): 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) @@ -459,6 +656,12 @@ class Repository(): booking.collaborators.add(collaborator) try: + booking.pdf = PDFTemplater.makePDF(booking) + booking.save() + except Exception as e: + return "BOOK, failed to create Pod Desriptor File: " + str(e) + + try: JobFactory.makeCompleteJob(booking) except Exception as e: return "BOOK, serializing for api generated exception: " + str(e) + " CODE:0xFFFF" @@ -468,32 +671,62 @@ class Repository(): 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: - return False + self.el[self.RESULT] = booking + self.el[self.HAS_RESULT] = True + + def make_opnfv_config(self): + opnfv_models = self.el[self.OPNFV_MODELS] + config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE] + if not config_bundle: + return "No Configuration bundle selected" + info = opnfv_models.get("meta", {}) + name = info.get("name", False) + desc = info.get("description", False) + if not (name and desc): + return "No name or description given" + installer = opnfv_models['installer_chosen'] + if not installer: + return "No OPNFV Installer chosen" + scenario = opnfv_models['scenario_chosen'] + if not scenario: + return "No OPNFV Scenario chosen" + + opnfv_config = OPNFVConfig.objects.create( + bundle=config_bundle, + name=name, + description=desc, + installer=installer, + scenario=scenario + ) + + network_roles = opnfv_models['network_roles'] + for net_role in network_roles: + opnfv_config.networks.add( + NetworkRole.objects.create( + name=net_role['role'], + network=net_role['network'] + ) + ) + + host_roles = opnfv_models['host_roles'] + for host_role in host_roles: + config = config_bundle.hostConfigurations.get( + host__resource__name=host_role['host_name'] + ) + HostOPNFVConfig.objects.create( + role=host_role['role'], + host_config=config, + opnfv_config=opnfv_config + ) + + self.el[self.RESULT] = opnfv_config + self.el[self.HAS_RESULT] = True def __init__(self): self.el = {} self.el[self.CONFIRMATION] = {} + self.el["active_step"] = 0 + self.el[self.HAS_RESULT] = False + self.el[self.RESULT] = None self.get_history = {} self.put_history = {} diff --git a/dashboard/src/workflow/opnfv_workflow.py b/dashboard/src/workflow/opnfv_workflow.py new file mode 100644 index 0000000..7d499ec --- /dev/null +++ b/dashboard/src/workflow/opnfv_workflow.py @@ -0,0 +1,299 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.forms import formset_factory + +from workflow.models import WorkflowStep, AbstractSelectOrCreate +from resource_inventory.models import ConfigBundle, OPNFV_SETTINGS +from workflow.forms import OPNFVSelectionForm, OPNFVNetworkRoleForm, OPNFVHostRoleForm, SWConfigSelectorForm, BasicMetaForm + + +class OPNFV_Resource_Select(AbstractSelectOrCreate): + title = "Select Software Configuration" + description = "Choose the software bundle you wish to use as a base for your OPNFV configuration" + short_title = "software config" + form = SWConfigSelectorForm + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE + + def get_form_queryset(self): + user = self.repo_get(self.repo.SESSION_USER) + qs = ConfigBundle.objects.filter(owner=user) + return qs + + def put_confirm_info(self, bundle): + confirm_dict = self.repo_get(self.repo.CONFIRMATION) + confirm_dict['software bundle'] = bundle.name + confirm_dict['hardware POD'] = bundle.bundle.name + self.repo_put(self.repo.CONFIRMATION, confirm_dict) + + def get_page_context(self): + return { + 'select_type': 'swconfig', + 'select_type_title': 'Software Config', + 'addable_type_num': 2 + } + + +class Pick_Installer(WorkflowStep): + template = 'config_bundle/steps/pick_installer.html' + title = 'Pick OPNFV Installer' + description = 'Choose which OPNFV installer to use' + short_title = "opnfv installer" + modified_key = "installer_step" + + def update_confirmation(self): + confirm = self.repo_get(self.repo.CONFIRMATION, {}) + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + installer = models.get("installer_chosen") + scenario = models.get("scenario_chosen") + if not (installer and scenario): + return + confirm['installer'] = installer.name + confirm['scenario'] = scenario.name + self.repo_put(self.repo.CONFIRMATION, confirm) + + def get_context(self): + context = super(Pick_Installer, self).get_context() + + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + initial = { + "installer": models.get("installer_chosen"), + "scenario": models.get("scenario_chosen") + } + + context["form"] = OPNFVSelectionForm(initial=initial) + return context + + def post_render(self, request): + form = OPNFVSelectionForm(request.POST) + if form.is_valid(): + installer = form.cleaned_data['installer'] + scenario = form.cleaned_data['scenario'] + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + models['installer_chosen'] = installer + models['scenario_chosen'] = scenario + self.repo_put(self.repo.OPNFV_MODELS, models) + self.update_confirmation() + self.set_valid("Step Completed") + else: + self.set_invalid("Please select an Installer and Scenario") + + return self.render(request) + + +class Assign_Network_Roles(WorkflowStep): + template = 'config_bundle/steps/assign_network_roles.html' + title = 'Pick Network Roles' + description = 'Choose what role each network should get' + short_title = "network roles" + modified_key = "net_roles_step" + + """ + to do initial filling, repo should have a "network_roles" array with the following structure for each element: + { + "role": <NetworkRole object ref>, + "network": <Network object ref> + } + """ + def create_netformset(self, roles, config_bundle, data=None): + roles_initial = [] + set_roles = self.repo_get(self.repo.OPNFV_MODELS, {}).get("network_roles") + if set_roles: + roles_initial = set_roles + else: + for role in OPNFV_SETTINGS.NETWORK_ROLES: + roles_initial.append({"role": role}) + + Formset = formset_factory(OPNFVNetworkRoleForm, extra=0) + kwargs = { + "initial": roles_initial, + "form_kwargs": {"config_bundle": config_bundle} + } + formset = None + if data: + formset = Formset(data, **kwargs) + else: + formset = Formset(**kwargs) + return formset + + def get_context(self): + context = super(Assign_Network_Roles, self).get_context() + config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) + if config_bundle is None: + context["unavailable"] = True + return context + + roles = OPNFV_SETTINGS.NETWORK_ROLES + formset = self.create_netformset(roles, config_bundle) + context['formset'] = formset + + return context + + def update_confirmation(self): + confirm = self.repo_get(self.repo.CONFIRMATION, {}) + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + roles = models.get("network_roles") + if not roles: + return + confirm['network roles'] = {} + for role in roles: + confirm['network roles'][role['role']] = role['network'].name + self.repo_put(self.repo.CONFIRMATION, confirm) + + def post_render(self, request): + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) + roles = OPNFV_SETTINGS.NETWORK_ROLES + net_role_formset = self.create_netformset(roles, config_bundle, data=request.POST) + if net_role_formset.is_valid(): + results = [] + for form in net_role_formset: + results.append({ + "role": form.cleaned_data['role'], + "network": form.cleaned_data['network'] + }) + models['network_roles'] = results + self.set_valid("Completed") + self.repo_put(self.repo.OPNFV_MODELS, models) + self.update_confirmation() + else: + self.set_invalid("Please complete all fields") + return self.render(request) + + +class Assign_Host_Roles(WorkflowStep): # taken verbatim from Define_Software in sw workflow, merge the two? + template = 'config_bundle/steps/assign_host_roles.html' + title = 'Pick Host Roles' + description = "Choose the role each machine will have in your OPNFV pod" + short_title = "host roles" + modified_key = "host_roles_step" + + def create_host_role_formset(self, hostlist=[], data=None): + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + host_roles = models.get("host_roles", []) + if not host_roles: + for host in hostlist: + initial = {"host_name": host.resource.name} + host_roles.append(initial) + models['host_roles'] = host_roles + self.repo_put(self.repo.OPNFV_MODELS, models) + + HostFormset = formset_factory(OPNFVHostRoleForm, extra=0) + + kwargs = {"initial": host_roles} + formset = None + if data: + formset = HostFormset(data, **kwargs) + else: + formset = HostFormset(**kwargs) + + return formset + + def get_context(self): + context = super(Assign_Host_Roles, self).get_context() + config = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) + if config is None: + context['error'] = "Please select a Configuration on the first step" + + formset = self.create_host_role_formset(hostlist=config.bundle.getHosts()) + context['formset'] = formset + + return context + + def get_host_role_mapping(self, host_roles, hostname): + for obj in host_roles: + if hostname == obj['host_name']: + return obj + return None + + def update_confirmation(self): + confirm = self.repo_get(self.repo.CONFIRMATION, {}) + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + roles = models.get("host_roles") + if not roles: + return + confirm['host roles'] = {} + for role in roles: + confirm['host roles'][role['host_name']] = role['role'].name + self.repo_put(self.repo.CONFIRMATION, confirm) + + def post_render(self, request): + formset = self.create_host_role_formset(data=request.POST) + + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + host_roles = models.get("host_roles", []) + + has_jumphost = False + if formset.is_valid(): + for form in formset: + hostname = form.cleaned_data['host_name'] + role = form.cleaned_data['role'] + mapping = self.get_host_role_mapping(host_roles, hostname) + mapping['role'] = role + if "jumphost" in role.name.lower(): + has_jumphost = True + + models['host_roles'] = host_roles + self.repo_put(self.repo.OPNFV_MODELS, models) + self.update_confirmation() + + if not has_jumphost: + self.set_invalid('Must have at least one "Jumphost" per POD') + else: + self.set_valid("Completed") + else: + self.set_invalid("Please complete all fields") + + return self.render(request) + + +class MetaInfo(WorkflowStep): + template = 'config_bundle/steps/config_software.html' + title = "Other Info" + description = "Give your software config a name, description, and other stuff" + short_title = "config info" + + def get_context(self): + context = super(MetaInfo, self).get_context() + + initial = self.repo_get(self.repo.OPNFV_MODELS, {}).get("meta", {}) + context["form"] = BasicMetaForm(initial=initial) + return context + + def update_confirmation(self): + confirm = self.repo_get(self.repo.CONFIRMATION, {}) + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + meta = models.get("meta") + if not meta: + return + confirm['name'] = meta['name'] + confirm['description'] = meta['description'] + self.repo_put(self.repo.CONFIRMATION, confirm) + + def post_render(self, request): + models = self.repo_get(self.repo.OPNFV_MODELS, {}) + info = models.get("meta", {}) + + form = BasicMetaForm(request.POST) + if form.is_valid(): + info['name'] = form.cleaned_data['name'] + info['description'] = form.cleaned_data['description'] + models['meta'] = info + self.repo_put(self.repo.OPNFV_MODELS, models) + self.update_confirmation() + self.set_valid("Complete") + else: + self.set_invalid("Please correct the errors shown below") + + self.repo_put(self.repo.OPNFV_MODELS, models) + return self.render(request) diff --git a/dashboard/src/workflow/resource_bundle_workflow.py b/dashboard/src/workflow/resource_bundle_workflow.py index 712c92b..06737d2 100644 --- a/dashboard/src/workflow/resource_bundle_workflow.py +++ b/dashboard/src/workflow/resource_bundle_workflow.py @@ -10,6 +10,7 @@ from django.shortcuts import render from django.forms import formset_factory +from django.conf import settings import json import re @@ -25,11 +26,12 @@ from workflow.forms import ( ) from resource_inventory.models import ( GenericResourceBundle, - Vlan, GenericInterface, GenericHost, GenericResource, - HostProfile + HostProfile, + Network, + NetworkConnection ) from dashboard.exceptions import ( InvalidVlanConfigurationException, @@ -50,65 +52,47 @@ class Define_Hardware(WorkflowStep): description = "Choose the type and amount of machines you want" short_title = "hosts" + def __init__(self, *args, **kwargs): + self.form = None + super().__init__(*args, **kwargs) + def get_context(self): context = super(Define_Hardware, self).get_context() - 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 + context['form'] = self.form or HardwareDefinitionForm() 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']) + data = data['filter_field'] models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) models['hosts'] = [] # This will always clear existing data when this step changes models['interfaces'] = {} if "bundle" not in models: models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)) - host_data = data['hosts'] + host_data = data['host'] names = {} - for host_dict in host_data: - id = host_dict['class'] - # bit of formatting - id = int(id.split("_")[-1]) + for host_profile_dict in host_data.values(): + id = host_profile_dict['id'] profile = HostProfile.objects.get(id=id) # instantiate genericHost and store in repo - 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) + for name in host_profile_dict['values'].values(): + if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name): + raise InvalidHostnameException("Invalid hostname: '" + name + "'") + if name in names: + raise NonUniqueHostnameException("All hosts must have unique names") + names[name] = True + genericResource = GenericResource(bundle=models['bundle'], name=name) + genericHost = GenericHost(profile=profile, resource=genericResource) + models['hosts'].append(genericHost) + for interface_profile in profile.interfaceprofile.all(): + genericInterface = GenericInterface(profile=interface_profile, host=genericHost) + if genericHost.resource.name not in models['interfaces']: + models['interfaces'][genericHost.resource.name] = [] + models['interfaces'][genericHost.resource.name].append(genericInterface) # add selected lab to models - for lab_dict in data['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) + for lab_dict in data['lab'].values(): + if lab_dict['selected']: + models['bundle'].lab = Lab.objects.get(lab_user__id=lab_dict['id']) break # if somehow we get two 'true' labs, we only use one # return to repo @@ -133,12 +117,11 @@ class Define_Hardware(WorkflowStep): if self.form.is_valid(): self.update_models(self.form.cleaned_data) self.update_confirmation() - self.metastep.set_valid("Step Completed") + self.set_valid("Step Completed") else: - self.metastep.set_invalid("Please complete the fields highlighted in red to continue") - pass + self.set_invalid("Please complete the fields highlighted in red to continue") except Exception as e: - self.metastep.set_invalid(str(e)) + self.set_invalid(str(e)) self.context = self.get_context() return render(request, self.template, self.context) @@ -168,53 +151,55 @@ class Define_Nets(WorkflowStep): except Exception: return None + def make_mx_host_dict(self, generic_host): + host = { + 'id': generic_host.resource.name, + 'interfaces': [], + 'value': { + "name": generic_host.resource.name, + "description": generic_host.profile.description + } + } + for iface in generic_host.profile.interfaceprofile.all(): + host['interfaces'].append({ + "name": iface.name, + "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type + }) + return host + def get_context(self): - # TODO: render *primarily* on hosts in repo models context = super(Define_Nets, self).get_context() - context['form'] = NetworkDefinitionForm() + context.update({ + 'form': NetworkDefinitionForm(), + 'debug': settings.DEBUG, + 'hosts': [], + 'added_hosts': [], + 'removed_hosts': [] + }) + vlans = self.get_vlans() + if vlans: + context['vlans'] = vlans 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 hostlist is not 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 + # calculate if the selected hosts have changed + added_hosts = set() + host_set = set(self.repo_get(self.repo.GRB_LAST_HOSTLIST, [])) + if len(host_set): + new_host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']]) + context['removed_hosts'] = [h.split("*")[0] for h in (host_set - new_host_set)] + added_hosts.update([h.split("*")[0] for h in (new_host_set - host_set)]) + + # add all host info to context for generic_host in hosts: - host_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)) + host = self.make_mx_host_dict(generic_host) + host_serialized = json.dumps(host) + context['hosts'].append(host_serialized) + if host['id'] in added_hosts: + context['added_hosts'].append(host_serialized) bundle = models.get("bundle", False) - if bundle and bundle.xml: - context['xml'] = bundle.xml - else: - context['xml'] = False + if bundle: + context['xml'] = bundle.xml or False except Exception: pass @@ -224,27 +209,24 @@ class Define_Nets(WorkflowStep): 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) + host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']]) + self.repo_put(self.repo.GRB_LAST_HOSTLIST, host_set) try: xmlData = request.POST.get("xml") self.updateModels(xmlData) # update model with xml - self.metastep.set_valid("Networks applied successfully") + self.set_valid("Networks applied successfully") except ResourceAvailabilityException: - self.metastep.set_invalid("Public network not availble") - except Exception: - self.metastep.set_invalid("An error occurred when applying networks") + self.set_invalid("Public network not availble") + except Exception as e: + self.set_invalid("An error occurred when applying networks: " + str(e)) return self.render(request) def updateModels(self, xmlData): models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) - models["vlans"] = {} - given_hosts, interfaces = self.parseXml(xmlData) - vlan_manager = models['bundle'].lab.vlan_manager + models["connections"] = {} + models['networks'] = {} + given_hosts, interfaces, networks = self.parseXml(xmlData) existing_host_list = models.get("hosts", []) existing_hosts = {} # maps id to host for host in existing_host_list: @@ -252,104 +234,133 @@ class Define_Nets(WorkflowStep): bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))) + for net_id, net in networks.items(): + network = Network() + network.name = net['name'] + network.bundle = bundle + network.is_public = net['public'] + models['networks'][net_id] = network + for hostid, given_host in given_hosts.items(): existing_host = existing_hosts[hostid[5:]] for ifaceId in given_host['interfaces']: iface = interfaces[ifaceId] - if existing_host.resource.name not in models['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: - public_net = vlan_manager.get_public_vlan() - if public_net is None: - raise ResourceAvailabilityException("No public networks available") - 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) + if existing_host.resource.name not in models['connections']: + models['connections'][existing_host.resource.name] = {} + models['connections'][existing_host.resource.name][iface['profile_name']] = [] + for connection in iface['connections']: + network_id = connection['network'] + net = models['networks'][network_id] + connection = NetworkConnection(vlan_is_tagged=connection['tagged'], network=net) + models['connections'][existing_host.resource.name][iface['profile_name']].append(connection) bundle.xml = xmlData self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models) - # 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 + def decomposeXml(self, xmlString): + """ + This function takes in an xml doc from our front end + and returns dictionaries that map cellIds to the xml + nodes themselves. There is no unpacking of the + xml objects, just grouping and organizing + """ + + connections = {} + networks = {} + hosts = {} + interfaces = {} + network_ports = {} + xmlDom = minidom.parseString(xmlString) root = xmlDom.documentElement.firstChild - netids = {} - untagged_ints = {} for cell in root.childNodes: cellId = cell.getAttribute('id') + group = cellId.split("_")[0] + parentGroup = cell.getAttribute("parent").split("_")[0] + # place cell into correct group if cell.getAttribute("edge"): - # 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 not tagged: - 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 not tagged: - 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: - 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 + connections[cellId] = cell + + elif "network" in group: + networks[cellId] = cell + + elif "host" in group: + hosts[cellId] = cell + + elif "host" in parentGroup: + interfaces[cellId] = cell + + # make network ports also map to thier network + elif "network" in parentGroup: + network_ports[cellId] = cell.getAttribute("parent") # maps port ID to net ID + + return connections, networks, hosts, interfaces, network_ports + + # serialize and deserialize xml from mxGraph + def parseXml(self, xmlString): + networks = {} # maps net name to network object + hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks + interfaces = {} # maps id -> interface + untagged_ifaces = set() # used to check vlan config + network_names = set() # used to check network names + xml_connections, xml_nets, xml_hosts, xml_ifaces, xml_ports = self.decomposeXml(xmlString) + + # parse Hosts + for cellId, cell in xml_hosts.items(): + cell_json_str = cell.getAttribute("value") + cell_json = json.loads(cell_json_str) + host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']} + hosts[cellId] = host + + # parse networks + for cellId, cell in xml_nets.items(): + escaped_json_str = cell.getAttribute("value") + json_str = escaped_json_str.replace('"', '"') + net_info = json.loads(json_str) + net_name = net_info['name'] + public = net_info['public'] + if net_name in network_names: + raise NetworkExistsException("Non unique network name found") + network = {"name": net_name, "public": public, "id": cellId} + networks[cellId] = network + network_names.add(net_name) + + # parse interfaces + for cellId, cell in xml_ifaces.items(): + parentId = cell.getAttribute('parent') + cell_json_str = cell.getAttribute("value") + cell_json = json.loads(cell_json_str) + iface = {"name": cellId, "connections": [], "profile_name": cell_json['name']} + hosts[parentId]['interfaces'].append(cellId) + interfaces[cellId] = iface + + # parse connections + for cellId, cell in xml_connections.items(): + escaped_json_str = cell.getAttribute("value") + json_str = escaped_json_str.replace('"', '"') + attributes = json.loads(json_str) + tagged = attributes['tagged'] + interface = None + network = None + src = cell.getAttribute("source") + tgt = cell.getAttribute("target") + if src in interfaces: + interface = interfaces[src] + network = networks[xml_ports[tgt]] + else: + interface = interfaces[tgt] + network = networks[xml_ports[src]] + + if not tagged: + if interface['name'] in untagged_ifaces: + raise InvalidVlanConfigurationException("More than one untagged vlan on an interface") + untagged_ifaces.add(interface['name']) + + # add connection to interface + interface['connections'].append({"tagged": tagged, "network": network['id']}) + + return hosts, interfaces, networks class Resource_Meta_Info(WorkflowStep): @@ -390,10 +401,10 @@ class Resource_Meta_Info(WorkflowStep): tmp = tmp[:60] + "..." confirm_info["description"] = tmp self.repo_put(self.repo.CONFIRMATION, confirm) - self.metastep.set_valid("Step Completed") + self.set_valid("Step Completed") else: - self.metastep.set_invalid("Please correct the fields highlighted in red to continue") + self.set_invalid("Please correct the fields highlighted in red to continue") pass return self.render(request) diff --git a/dashboard/src/workflow/snapshot_workflow.py b/dashboard/src/workflow/snapshot_workflow.py index 4ddc397..5414784 100644 --- a/dashboard/src/workflow/snapshot_workflow.py +++ b/dashboard/src/workflow/snapshot_workflow.py @@ -8,13 +8,13 @@ ############################################################################## -import datetime +from django.utils import timezone import json from booking.models import Booking from resource_inventory.models import Host, Image from workflow.models import WorkflowStep -from workflow.forms import SnapshotMetaForm, SnapshotHostSelectForm +from workflow.forms import BasicMetaForm, SnapshotHostSelectForm class Select_Host_Step(WorkflowStep): @@ -27,7 +27,7 @@ class Select_Host_Step(WorkflowStep): context = super(Select_Host_Step, self).get_context() context['form'] = SnapshotHostSelectForm() booking_hosts = {} - now = datetime.datetime.now() + now = timezone.now() user = self.repo_get(self.repo.SESSION_USER) bookings = Booking.objects.filter(start__lt=now, end__gt=now, owner=user) for booking in bookings: @@ -52,11 +52,11 @@ class Select_Host_Step(WorkflowStep): def post_render(self, request): host_data = request.POST.get("host") if not host_data: - self.metastep.set_invalid("Please select a host") + self.set_invalid("Please select a host") return self.render(request) host = json.loads(host_data) if 'name' not in host or 'booking' not in host: - self.metastep.set_invalid("Invalid host selected") + self.set_invalid("Invalid host selected") return self.render(request) name = host['name'] booking_id = host['booking'] @@ -75,7 +75,7 @@ class Select_Host_Step(WorkflowStep): snap_confirm['host'] = name confirm['snapshot'] = snap_confirm self.repo_put(self.repo.CONFIRMATION, confirm) - self.metastep.set_valid("Success") + self.set_valid("Success") return self.render(request) @@ -87,11 +87,18 @@ class Image_Meta_Step(WorkflowStep): def get_context(self): context = super(Image_Meta_Step, self).get_context() - context['form'] = SnapshotMetaForm() + name = self.repo_get(self.repo.SNAPSHOT_NAME, False) + desc = self.repo_get(self.repo.SNAPSHOT_DESC, False) + form = None + if name and desc: + form = BasicMetaForm(initial={"name": name, "description": desc}) + else: + form = BasicMetaForm() + context['form'] = form return context def post_render(self, request): - form = SnapshotMetaForm(request.POST) + form = BasicMetaForm(request.POST) if form.is_valid(): name = form.cleaned_data['name'] self.repo_put(self.repo.SNAPSHOT_NAME, name) @@ -105,8 +112,8 @@ class Image_Meta_Step(WorkflowStep): confirm['snapshot'] = snap_confirm self.repo_put(self.repo.CONFIRMATION, confirm) - self.metastep.set_valid("Success") + self.set_valid("Success") else: - self.metastep.set_invalid("Please Fill out the Form") + self.set_invalid("Please Fill out the Form") return self.render(request) diff --git a/dashboard/src/workflow/sw_bundle_workflow.py b/dashboard/src/workflow/sw_bundle_workflow.py index 26ade22..0c558fc 100644 --- a/dashboard/src/workflow/sw_bundle_workflow.py +++ b/dashboard/src/workflow/sw_bundle_workflow.py @@ -11,33 +11,13 @@ from django.forms import formset_factory from workflow.models import WorkflowStep -from workflow.forms import SoftwareConfigurationForm, HostSoftwareDefinitionForm -from workflow.booking_workflow import Resource_Select -from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration, Installer, OPNFVConfig +from workflow.forms import BasicMetaForm, HostSoftwareDefinitionForm +from workflow.booking_workflow import Abstract_Resource_Select +from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration -# resource selection step is reused from Booking workflow -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 SWConf_Resource_Select(Abstract_Resource_Select): + workflow_type = "configuration" class Define_Software(WorkflowStep): @@ -46,52 +26,61 @@ class Define_Software(WorkflowStep): description = "Choose the opnfv and image of your machines" short_title = "host config" - def create_hostformset(self, hostlist): + def build_filter_data(self, hosts_data): + """ + returns a 2D array of images to exclude + based on the ordering of the passed + hosts_data + """ + filter_data = [] + user = self.repo_get(self.repo.SESSION_USER) + lab = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE).lab + for i, host_data in enumerate(hosts_data): + host = GenericHost.objects.get(pk=host_data['host_id']) + wrong_owner = Image.objects.exclude(owner=user).exclude(public=True) + wrong_host = Image.objects.exclude(host_type=host.profile) + wrong_lab = Image.objects.exclude(from_lab=lab) + excluded_images = wrong_owner | wrong_host | wrong_lab + filter_data.append([]) + for image in excluded_images: + filter_data[i].append(image.pk) + return filter_data + + def create_hostformset(self, hostlist, data=None): hosts_initial = [] host_configs = self.repo_get(self.repo.CONFIG_MODELS, {}).get("host_configs", False) if host_configs: for config in host_configs: - 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) - + hosts_initial.append({ + 'host_id': config.host.id, + 'host_name': config.host.resource.name, + 'headnode': config.is_head_node, + 'image': config.image + }) else: for host in hostlist: - host_initial = {'host_id': host.id, 'host_name': host.resource.name} - - hosts_initial.append(host_initial) + hosts_initial.append({ + 'host_id': host.id, + 'host_name': host.resource.name + }) HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0) - host_formset = HostFormset(initial=hosts_initial) + filter_data = self.build_filter_data(hosts_initial) - filter_data = {} - user = self.repo_get(self.repo.SESSION_USER) - i = 0 - for host_data in hosts_initial: - host_profile = None - try: - host = GenericHost.objects.get(pk=host_data['host_id']) - host_profile = host.profile - except Exception: - for host in hostlist: - if host.resource.name == host_data['host_name']: - host_profile = host.profile - break - 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 + class SpecialHostFormset(HostFormset): + def get_form_kwargs(self, index): + kwargs = super(SpecialHostFormset, self).get_form_kwargs(index) + if index is not None: + kwargs['imageQS'] = Image.objects.exclude(pk__in=filter_data[index]) + return kwargs - return host_formset, filter_data + if data: + return SpecialHostFormset(data, initial=hosts_initial) + return SpecialHostFormset(initial=hosts_initial) def get_host_list(self, grb=None): if grb is None: - grb = self.repo_get(self.repo.SWCONF_SELECTED_GRB, False) + grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False) if not grb: return [] if grb.id: @@ -102,16 +91,16 @@ class Define_Software(WorkflowStep): def get_context(self): context = super(Define_Software, self).get_context() - grb = self.repo_get(self.repo.SWCONF_SELECTED_GRB, False) + grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False) if grb: context["grb"] = grb - formset, filter_data = self.create_hostformset(self.get_host_list(grb)) + formset = self.create_hostformset(self.get_host_list(grb)) context["formset"] = formset - context["filter_data"] = filter_data + context['headnode'] = self.repo_get(self.repo.CONFIG_MODELS, {}).get("headnode_index", 1) else: context["error"] = "Please select a resource first" - self.metastep.set_invalid("Step requires information that is not yet provided by previous step") + self.set_invalid("Step requires information that is not yet provided by previous step") return context @@ -122,58 +111,51 @@ class Define_Software(WorkflowStep): confirm = self.repo_get(self.repo.CONFIRMATION, {}) - HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0) - formset = HostFormset(request.POST) hosts = self.get_host_list() + models['headnode_index'] = request.POST.get("headnode", 1) + formset = self.create_hostformset(hosts, data=request.POST) + has_headnode = False if formset.is_valid(): models['host_configs'] = [] - i = 0 confirm_hosts = [] - for form in formset: + for i, form in enumerate(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'] + headnode = form.cleaned_data['headnode'] + if headnode: + has_headnode = True bundle = models['bundle'] hostConfig = HostConfiguration( host=host, image=image, bundle=bundle, - opnfvRole=role + is_head_node=headnode ) models['host_configs'].append(hostConfig) - confirm_host = {"name": host.resource.name, "image": image.name, "role": role.name} - confirm_hosts.append(confirm_host) + confirm_hosts.append({ + "name": host.resource.name, + "image": image.name, + "headnode": headnode + }) + + if not has_headnode: + self.set_invalid('Must have one "Headnode" per POD') + return self.render(request) self.repo_put(self.repo.CONFIG_MODELS, models) if "configuration" not in confirm: confirm['configuration'] = {} confirm['configuration']['hosts'] = confirm_hosts self.repo_put(self.repo.CONFIRMATION, confirm) - self.metastep.set_valid("Completed") + self.set_valid("Completed") else: - self.metastep.set_invalid("Please complete all fields") + self.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" @@ -187,58 +169,30 @@ class Config_Software(WorkflowStep): 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 - + context["form"] = BasicMetaForm(initial=initial) 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)) + 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'] = {} + 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") + form = BasicMetaForm(request.POST) + if form.is_valid(): + models['bundle'].name = form.cleaned_data['name'] + models['bundle'].description = form.cleaned_data['description'] - self.repo_put(self.repo.CONFIG_MODELS, models) - self.repo_put(self.repo.CONFIRMATION, confirm) + confirm['configuration']['name'] = form.cleaned_data['name'] + confirm['configuration']['description'] = form.cleaned_data['description'] + self.set_valid("Complete") + else: + self.set_invalid("Please correct the errors shown below") + + self.repo_put(self.repo.CONFIG_MODELS, models) + self.repo_put(self.repo.CONFIRMATION, confirm) - except Exception: - pass return self.render(request) diff --git a/dashboard/src/workflow/urls.py b/dashboard/src/workflow/urls.py index b131d84..5a97904 100644 --- a/dashboard/src/workflow/urls.py +++ b/dashboard/src/workflow/urls.py @@ -14,7 +14,7 @@ from django.conf import settings from workflow.views import step_view, delete_session, manager_view, viewport_view from workflow.models import Repository from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info -from workflow.booking_workflow import SWConfig_Select, Resource_Select, Booking_Meta +from workflow.booking_workflow import SWConfig_Select, Booking_Resource_Select, Booking_Meta app_name = 'workflow' urlpatterns = [ @@ -31,4 +31,4 @@ if settings.TESTING: 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)) + urlpatterns.append(url(r'^workflow/step/resource_select$', Booking_Resource_Select("", Repository()).test_render)) diff --git a/dashboard/src/workflow/views.py b/dashboard/src/workflow/views.py index e5ef5c6..7ed9031 100644 --- a/dashboard/src/workflow/views.py +++ b/dashboard/src/workflow/views.py @@ -8,12 +8,14 @@ ############################################################################## -from django.http import HttpResponse, HttpResponseGone +from django.http import HttpResponseGone, JsonResponse from django.shortcuts import render +from django.urls import reverse import uuid from workflow.workflow_manager import ManagerTracker, SessionManager +from booking.models import Booking import logging logger = logging.getLogger(__name__) @@ -29,12 +31,33 @@ def attempt_auth(request): return None +def get_redirect_response(result): + if not result: + return {} + + # need to get type of result, and switch on the type + # since has_result, result must be populated with a valid object + if isinstance(result, Booking): + return { + 'redir_url': reverse('booking:booking_detail', kwargs={'booking_id': result.id}) + } + else: + return {} + + def delete_session(request): - try: + manager = attempt_auth(request) + + if not manager: + return HttpResponseGone("No session found that relates to current request") + + not_last_workflow, result = manager.pop_workflow() + + if not_last_workflow: # this was not the last workflow, so don't redirect away + return JsonResponse({}) + else: del ManagerTracker.managers[request.session['manager_session']] - return HttpResponse('') - except Exception: - return None + return JsonResponse(get_redirect_response(result)) def step_view(request): @@ -43,7 +66,12 @@ def step_view(request): # 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'))) + if request.GET.get('step') == 'next': + manager.go_next() + elif request.GET.get('step') == 'prev': + manager.go_prev() + else: + raise Exception("requested action for new step had malformed contents: " + request.GET.get('step')) return manager.render(request) @@ -70,7 +98,8 @@ def manager_view(request): 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: - del ManagerTracker.managers[request.session['manager_session']] + if not manager.pop_workflow(): + del ManagerTracker.managers[request.session['manager_session']] return manager.status(request) diff --git a/dashboard/src/workflow/workflow_factory.py b/dashboard/src/workflow/workflow_factory.py index 9a42d86..03c8126 100644 --- a/dashboard/src/workflow/workflow_factory.py +++ b/dashboard/src/workflow/workflow_factory.py @@ -8,10 +8,12 @@ ############################################################################## -from workflow.booking_workflow import Booking_Resource_Select, SWConfig_Select, Booking_Meta +from workflow.booking_workflow import Booking_Resource_Select, SWConfig_Select, Booking_Meta, OPNFV_Select from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info from workflow.sw_bundle_workflow import Config_Software, Define_Software, SWConf_Resource_Select from workflow.snapshot_workflow import Select_Host_Step, Image_Meta_Step +from workflow.opnfv_workflow import Pick_Installer, Assign_Network_Roles, Assign_Host_Roles, OPNFV_Resource_Select, MetaInfo +from workflow.models import Confirmation_Step import uuid @@ -19,38 +21,6 @@ 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): UNTOUCHED = 0 @@ -69,6 +39,7 @@ class MetaStep(object): self.short_title = "error" self.skip_step = 0 self.valid = 0 + self.hidden = False self.message = "" self.id = uuid.uuid4() @@ -93,11 +64,19 @@ class MetaStep(object): return self.id.int != other.id.int +class Workflow(object): + def __init__(self, steps, repository): + self.repository = repository + self.steps = steps + self.active_index = 0 + + class WorkflowFactory(): booking_steps = [ Booking_Resource_Select, SWConfig_Select, - Booking_Meta + Booking_Meta, + OPNFV_Select, ] resource_steps = [ @@ -114,7 +93,15 @@ class WorkflowFactory(): snapshot_steps = [ Select_Host_Step, - Image_Meta_Step + Image_Meta_Step, + ] + + opnfv_steps = [ + OPNFV_Resource_Select, + Pick_Installer, + Assign_Network_Roles, + Assign_Host_Roles, + MetaInfo ] def conjure(self, workflow_type=None, repo=None): @@ -123,14 +110,19 @@ class WorkflowFactory(): self.resource_steps, self.config_steps, self.snapshot_steps, + self.opnfv_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 + return steps + + def create_workflow(self, workflow_type=None, repo=None): + steps = self.conjure(workflow_type, repo) + c_step = self.make_step(Confirmation_Step, repo) + steps.append(c_step) + return Workflow(steps, repo) def make_steps(self, step_types, repository): - repository.el['steps'] += len(step_types) steps = [] for step_type in step_types: steps.append(self.make_step(step_type, repository)) @@ -140,13 +132,3 @@ class WorkflowFactory(): def make_step(self, step_type, repository): iden = step_type.description + step_type.title + step_type.template return step_type(iden, repository) - - def metaize(self, steps, wf_type): - meta_dict = [] - for step in steps: - meta_step = MetaStep() - meta_step.short_title = step.short_title - meta_dict.append(meta_step) - step.metastep = meta_step - - return meta_dict diff --git a/dashboard/src/workflow/workflow_manager.py b/dashboard/src/workflow/workflow_manager.py index 95fefbf..80b8a67 100644 --- a/dashboard/src/workflow/workflow_manager.py +++ b/dashboard/src/workflow/workflow_manager.py @@ -10,11 +10,9 @@ from django.http import JsonResponse -import random - from booking.models import Booking -from workflow.workflow_factory import WorkflowFactory, MetaStep, MetaRelation -from workflow.models import Repository, Confirmation_Step +from workflow.workflow_factory import WorkflowFactory +from workflow.models import Repository from resource_inventory.models import ( GenericResourceBundle, ConfigBundle, @@ -27,86 +25,64 @@ logger = logging.getLogger(__name__) class SessionManager(): + def active_workflow(self): + return self.workflows[-1] 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.workflows = [] + + self.owner = request.user + 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 set_step_statuses(self, superclass_type, desired_enabled=True): + workflow = self.active_workflow() + steps = workflow.steps + for step in steps: + if isinstance(step, superclass_type): + if desired_enabled: + step.enable() + else: + step.disable() def add_workflow(self, workflow_type=None, target_id=None, **kwargs): if target_id is not None: self.prefill_repo(target_id, workflow_type) - 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 + + repo = Repository() + if(len(self.workflows) >= 1): + defaults = self.workflows[-1].repository.get_child_defaults() + repo.set_defaults(defaults) + repo.el[repo.HAS_RESULT] = False + repo.el[repo.SESSION_USER] = self.owner + repo.el[repo.SESSION_MANAGER] = self + self.workflows.append( + self.factory.create_workflow( + workflow_type=workflow_type, + repo=repo + ) + ) + + def pop_workflow(self): + multiple_wfs = len(self.workflows) > 1 + if multiple_wfs: + if self.workflows[-1].repository.el[Repository.RESULT]: # move result + key = self.workflows[-1].repository.el[Repository.RESULT_KEY] + result = self.workflows[-1].repository.el[Repository.RESULT] + self.workflows[-2].repository.el[key] = result + self.workflows.pop() + current_repo = self.workflows[-1].repository + return (multiple_wfs, current_repo.el[current_repo.RESULT]) def status(self, request): try: - steps = [] - for step in self.step_meta: - steps.append(step.to_json()) - parents = {} - children = {} + meta_json = [] + for step in self.active_workflow().steps: + meta_json.append(step.to_json()) 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 + responsejson["steps"] = meta_json + responsejson["active"] = self.active_workflow().repository.el['active_step'] + responsejson["workflow_count"] = len(self.workflows) return JsonResponse(responsejson, safe=False) except Exception: pass @@ -115,16 +91,35 @@ class SessionManager(): # 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) + return self.active_workflow().steps[self.active_workflow().active_index].post_render(request) + return self.active_workflow().steps[self.active_workflow().active_index].render(request) def post_render(self, request): - return self.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 + return self.active_workflow().steps[self.active_workflow().active_index].post_render(request) + + def get_active_step(self): + return self.active_workflow().steps[self.active_workflow().active_index] + + def go_next(self, **kwargs): + # need to verify current step is valid to allow this + if self.get_active_step().valid < 200: + return + next_step = self.active_workflow().active_index + 1 + if next_step >= len(self.active_workflow().steps): + raise Exception("Out of bounds request for step") + while not self.active_workflow().steps[next_step].enabled: + next_step += 1 + self.active_workflow().repository.el['active_step'] = next_step + self.active_workflow().active_index = next_step + + def go_prev(self, **kwargs): + prev_step = self.active_workflow().active_index - 1 + if prev_step < 0: + raise Exception("Out of bounds request for step") + while not self.active_workflow().steps[prev_step].enabled: + prev_step -= 1 + self.active_workflow().repository.el['active_step'] = prev_step + self.active_workflow().active_index = prev_step def prefill_repo(self, target_id, workflow_type): self.repository.el[self.repository.EDIT] = True @@ -142,29 +137,28 @@ class SessionManager(): 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) + self.active_workflow().repository.el[self.active_workflow().repository.BOOKING_MODELS] = models + self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirmation + self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = self.make_grb_models(booking.resource.template) + self.active_workflow().repository.el[self.active_workflow().repository.SELECTED_GRESOURCE_BUNDLE] = self.make_grb_models(booking.resource.template)['bundle'] + self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = self.make_config_models(booking.config_bundle) def prefill_resource(self, resource): models = self.make_grb_models(resource) confirm = self.make_grb_confirm(resource) - self.repository.el[self.repository.GRESOURCE_BUNDLE_MODELS] = models - self.repository.el[self.repository.CONFIRMATION] = confirm + self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = models + self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm def prefill_config(self, config): models = self.make_config_models(config) confirm = self.make_config_confirm(config) - self.repository.el[self.repository.CONFIG_MODELS] = models - self.repository.el[self.repository.CONFIRMATION] = confirm + self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = models + self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm grb_models = self.make_grb_models(config.bundle) - self.repository.el[self.repository.GRESOURCE_BUNDLE_MODELS] = grb_models - self.repository.el[self.repository.SWCONF_SELECTED_GRB] = config.bundle + self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = grb_models def make_grb_models(self, resource): - models = self.repository.el.get(self.repository.GRESOURCE_BUNDLE_MODELS, {}) + models = self.active_workflow().repository.el.get(self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS, {}) models['hosts'] = [] models['bundle'] = resource models['interfaces'] = {} @@ -181,7 +175,7 @@ class SessionManager(): return models def make_grb_confirm(self, resource): - confirm = self.repository.el.get(self.repository.CONFIRMATION, {}) + confirm = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIRMATION, {}) confirm['resource'] = {} confirm['resource']['hosts'] = [] confirm['resource']['lab'] = resource.lab.lab_user.username @@ -190,7 +184,7 @@ class SessionManager(): return confirm def make_config_models(self, config): - models = self.repository.el.get(self.repository.CONFIG_MODELS, {}) + models = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIG_MODELS, {}) models['bundle'] = config models['host_configs'] = [] for host_conf in HostConfiguration.objects.filter(bundle=config): @@ -199,7 +193,7 @@ class SessionManager(): return models def make_config_confirm(self, config): - confirm = self.repository.el.get(self.repository.CONFIRMATION, {}) + confirm = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIRMATION, {}) confirm['configuration'] = {} confirm['configuration']['hosts'] = [] confirm['configuration']['name'] = config.name @@ -213,7 +207,7 @@ class SessionManager(): return confirm def make_booking_models(self, booking): - models = self.repository.el.get(self.repository.BOOKING_MODELS, {}) + models = self.active_workflow().repository.el.get(self.active_workflow().repository.BOOKING_MODELS, {}) models['booking'] = booking models['collaborators'] = [] for user in booking.collaborators.all(): @@ -221,7 +215,7 @@ class SessionManager(): return models def make_booking_confirm(self, booking): - confirm = self.repository.el.get(self.repository.CONFIRMATION, {}) + confirm = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIRMATION, {}) confirm['booking'] = {} confirm['booking']['length'] = (booking.end - booking.start).days confirm['booking']['project'] = booking.project |