summaryrefslogtreecommitdiffstats
path: root/dashboard/src/workflow/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'dashboard/src/workflow/models.py')
-rw-r--r--dashboard/src/workflow/models.py732
1 files changed, 732 insertions, 0 deletions
diff --git a/dashboard/src/workflow/models.py b/dashboard/src/workflow/models.py
new file mode 100644
index 0000000..6c6bd9a
--- /dev/null
+++ b/dashboard/src/workflow/models.py
@@ -0,0 +1,732 @@
+##############################################################################
+# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.shortcuts import render
+from django.contrib import messages
+from django.http import HttpResponse
+from django.utils import timezone
+
+import yaml
+import requests
+
+from workflow.forms import ConfirmationForm
+from api.models import JobFactory
+from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
+from resource_inventory.models import Image, GenericInterface, OPNFVConfig, HostOPNFVConfig, NetworkRole
+from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
+from notifier.manager import NotificationHandler
+from booking.models import Booking
+
+
+class BookingAuthManager():
+ LFN_PROJECTS = ["opnfv"] # TODO
+
+ def parse_github_url(self, url):
+ project_leads = []
+ try:
+ parts = url.split("/")
+ if "http" in parts[0]: # the url include http(s)://
+ parts = parts[2:]
+ if parts[-1] != "INFO.yaml":
+ return None
+ if parts[0] not in ["github.com", "raw.githubusercontent.com"]:
+ return None
+ if parts[1] not in self.LFN_PROJECTS:
+ return None
+ # now to download and parse file
+ if parts[3] == "blob":
+ parts[3] = "raw"
+ url = "https://" + "/".join(parts)
+ info_file = requests.get(url, timeout=15).text
+ info_parsed = yaml.load(info_file)
+ ptl = info_parsed.get('project_lead')
+ if ptl:
+ project_leads.append(ptl)
+ sub_ptl = info_parsed.get("subproject_lead")
+ if sub_ptl:
+ project_leads.append(sub_ptl)
+
+ except Exception:
+ pass
+
+ return project_leads
+
+ def parse_gerrit_url(self, url):
+ project_leads = []
+ try:
+ halfs = url.split("?")
+ parts = halfs[0].split("/")
+ args = halfs[1].split(";")
+ if "http" in parts[0]: # the url include http(s)://
+ parts = parts[2:]
+ if "f=INFO.yaml" not in args:
+ return None
+ if "gerrit.opnfv.org" not in parts[0]:
+ return None
+ try:
+ i = args.index("a=blob")
+ args[i] = "a=blob_plain"
+ except ValueError:
+ pass
+ # recreate url
+ halfs[1] = ";".join(args)
+ halfs[0] = "/".join(parts)
+ # now to download and parse file
+ url = "https://" + "?".join(halfs)
+ info_file = requests.get(url, timeout=15).text
+ info_parsed = yaml.load(info_file)
+ ptl = info_parsed.get('project_lead')
+ if ptl:
+ project_leads.append(ptl)
+ sub_ptl = info_parsed.get("subproject_lead")
+ if sub_ptl:
+ project_leads.append(sub_ptl)
+
+ except Exception:
+ return None
+
+ return project_leads
+
+ def parse_opnfv_git_url(self, url):
+ project_leads = []
+ try:
+ parts = url.split("/")
+ if "http" in parts[0]: # the url include http(s)://
+ parts = parts[2:]
+ if "INFO.yaml" not in parts[-1]:
+ return None
+ if "git.opnfv.org" not in parts[0]:
+ return None
+ if parts[-2] == "tree":
+ parts[-2] = "plain"
+ # now to download and parse file
+ url = "https://" + "/".join(parts)
+ info_file = requests.get(url, timeout=15).text
+ info_parsed = yaml.load(info_file)
+ ptl = info_parsed.get('project_lead')
+ if ptl:
+ project_leads.append(ptl)
+ sub_ptl = info_parsed.get("subproject_lead")
+ if sub_ptl:
+ project_leads.append(sub_ptl)
+
+ except Exception:
+ return None
+
+ return project_leads
+
+ def parse_url(self, info_url):
+ """
+ will return the PTL in the INFO file on success, or None
+ """
+ if "github" in info_url:
+ return self.parse_github_url(info_url)
+
+ if "gerrit.opnfv.org" in info_url:
+ return self.parse_gerrit_url(info_url)
+
+ if "git.opnfv.org" in info_url:
+ return self.parse_opnfv_git_url(info_url)
+
+ def booking_allowed(self, booking, repo):
+ """
+ This is the method that will have to change whenever the booking policy changes in the Infra
+ group / LFN. This is a nice isolation of that administration crap
+ currently checks if the booking uses multiple servers. if it does, then the owner must be a PTL,
+ which is checked using the provided info file
+ """
+ if booking.owner.userprofile.booking_privledge:
+ return True # admin override for this user
+ if Booking.objects.filter(owner=booking.owner, end__gt=timezone.now()).count() >= 3:
+ return False
+ if len(booking.resource.template.getHosts()) < 2:
+ return True # if they only have one server, we dont care
+ if repo.BOOKING_INFO_FILE not in repo.el:
+ return False # INFO file not provided
+ ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE))
+ for ptl in ptl_info:
+ if ptl['email'] == booking.owner.userprofile.email_addr:
+ return True
+ return False
+
+
+class WorkflowStepStatus(object):
+ UNTOUCHED = 0
+ INVALID = 100
+ VALID = 200
+
+
+class WorkflowStep(object):
+ template = 'bad_request.html'
+ title = "Generic Step"
+ description = "You were led here by mistake"
+ short_title = "error"
+ metastep = None
+ # phasing out metastep:
+
+ valid = WorkflowStepStatus.UNTOUCHED
+ message = ""
+
+ enabled = True
+
+ def cleanup(self):
+ raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete implemented cleanup() method")
+
+ def enable(self):
+ if not self.enabled:
+ self.enabled = True
+
+ def disable(self):
+ if self.enabled:
+ self.cleanup()
+ self.enabled = False
+
+ def set_invalid(self, message, code=WorkflowStepStatus.INVALID):
+ self.valid = code
+ self.message = message
+
+ def set_valid(self, message, code=WorkflowStepStatus.VALID):
+ self.valid = code
+ self.message = message
+
+ def to_json(self):
+ return {
+ 'title': self.short_title,
+ 'enabled': self.enabled,
+ 'valid': self.valid,
+ 'message': self.message,
+ }
+
+ def __init__(self, id, repo=None):
+ self.repo = repo
+ self.id = id
+
+ def get_context(self):
+ context = {}
+ context['step_number'] = self.repo_get('steps')
+ context['active_step'] = self.repo_get('active_step')
+ context['render_correct'] = "true"
+ context['step_title'] = self.title
+ context['description'] = self.description
+ return context
+
+ def render(self, request):
+ self.context = self.get_context()
+ return render(request, self.template, self.context)
+
+ def post_render(self, request):
+ return self.render(request)
+
+ def test_render(self, request):
+ if request.method == "POST":
+ return self.post_render(request)
+ return self.render(request)
+
+ def validate(self, request):
+ pass
+
+ def repo_get(self, key, default=None):
+ return self.repo.get(key, default, self.id)
+
+ def repo_put(self, key, value):
+ return self.repo.put(key, value, self.id)
+
+
+"""
+subclassing notes:
+ subclasses have to define the following class attributes:
+ self.select_repo_key: where the selected "object" or "bundle" is to be placed in the repo
+ self.form: the form to be used
+ alert_bundle_missing(): what message to display if a user does not select/selects an invalid object
+ get_form_queryset(): generate a queryset to be used to filter available items for the field
+ get_page_context(): return simple context such as page header and other info
+"""
+
+
+class AbstractSelectOrCreate(WorkflowStep):
+ template = 'dashboard/genericselect.html'
+ title = "Select a Bundle"
+ short_title = "select"
+ description = "Generic bundle selector step"
+
+ select_repo_key = None
+ form = None # subclasses are expected to use a form that is a subclass of SearchableSelectGenericForm
+
+ def alert_bundle_missing(self): # override in subclasses to change message if field isn't filled out
+ self.set_invalid("Please select a valid bundle")
+
+ def post_render(self, request):
+ context = self.get_context()
+ form = self.form(request.POST, queryset=self.get_form_queryset())
+ if form.is_valid():
+ bundle = form.get_validated_bundle()
+ if not bundle:
+ self.alert_bundle_missing()
+ return render(request, self.template, context)
+ self.repo_put(self.select_repo_key, bundle)
+ self.put_confirm_info(bundle)
+ self.set_valid("Step Completed")
+ else:
+ self.alert_bundle_missing()
+ messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
+
+ return self.render(request)
+
+ def get_context(self):
+ default = []
+
+ bundle = self.repo_get(self.select_repo_key, False)
+ if bundle:
+ default.append(bundle)
+
+ form = self.form(queryset=self.get_form_queryset(), initial=default)
+
+ context = {'form': form, **self.get_page_context()}
+ context.update(super().get_context())
+
+ return context
+
+ def get_page_context():
+ return {
+ 'select_type': 'generic',
+ 'select_type_title': 'Generic Bundle'
+ }
+
+
+class Confirmation_Step(WorkflowStep):
+ template = 'workflow/confirm.html'
+ title = "Confirm Changes"
+ description = "Does this all look right?"
+
+ short_title = "confirm"
+
+ def get_context(self):
+ context = super(Confirmation_Step, self).get_context()
+ context['form'] = ConfirmationForm()
+ context['confirmation_info'] = yaml.dump(
+ self.repo_get(self.repo.CONFIRMATION),
+ default_flow_style=False
+ ).strip()
+
+ return context
+
+ def flush_to_db(self):
+ errors = self.repo.make_models()
+ if errors:
+ return errors
+
+ def post_render(self, request):
+ form = ConfirmationForm(request.POST)
+ if form.is_valid():
+ data = form.cleaned_data['confirm']
+ context = self.get_context()
+ if data == "True":
+ context["bypassed"] = "true"
+ errors = self.flush_to_db()
+ if errors:
+ messages.add_message(request, messages.ERROR, "ERROR OCCURRED: " + errors)
+ else:
+ messages.add_message(request, messages.SUCCESS, "Confirmed")
+
+ return HttpResponse('')
+ elif data == "False":
+ context["bypassed"] = "true"
+ messages.add_message(request, messages.SUCCESS, "Canceled")
+ return render(request, self.template, context)
+ else:
+ pass
+
+ else:
+ pass
+
+
+class Repository():
+
+ EDIT = "editing"
+ MODELS = "models"
+ RESOURCE_SELECT = "resource_select"
+ CONFIRMATION = "confirmation"
+ SELECTED_GRESOURCE_BUNDLE = "selected generic bundle pk"
+ SELECTED_CONFIG_BUNDLE = "selected config bundle pk"
+ SELECTED_OPNFV_CONFIG = "selected opnfv deployment config"
+ GRESOURCE_BUNDLE_MODELS = "generic_resource_bundle_models"
+ GRESOURCE_BUNDLE_INFO = "generic_resource_bundle_info"
+ BOOKING = "booking"
+ LAB = "lab"
+ GRB_LAST_HOSTLIST = "grb_network_previous_hostlist"
+ BOOKING_FORMS = "booking_forms"
+ SWCONF_HOSTS = "swconf_hosts"
+ BOOKING_MODELS = "booking models"
+ CONFIG_MODELS = "configuration bundle models"
+ OPNFV_MODELS = "opnfv configuration models"
+ SESSION_USER = "session owner user account"
+ SESSION_MANAGER = "session manager for current session"
+ VALIDATED_MODEL_GRB = "valid grb config model instance in db"
+ VALIDATED_MODEL_CONFIG = "valid config model instance in db"
+ VALIDATED_MODEL_BOOKING = "valid booking model instance in db"
+ VLANS = "a list of vlans"
+ SNAPSHOT_MODELS = "the models for snapshotting"
+ SNAPSHOT_BOOKING_ID = "the booking id for snapshotting"
+ SNAPSHOT_NAME = "the name of the snapshot"
+ SNAPSHOT_DESC = "description of the snapshot"
+ BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking"
+
+ # migratory elements of segmented workflow
+ # each of these is the end result of a different workflow.
+ HAS_RESULT = "whether or not workflow has a result"
+ RESULT_KEY = "key for target index that result will be put into in parent"
+ RESULT = "result object from workflow"
+
+ def get_child_defaults(self):
+ return_tuples = []
+ for key in [self.SELECTED_GRESOURCE_BUNDLE, self.SESSION_USER]:
+ return_tuples.append((key, self.el.get(key)))
+ return return_tuples
+
+ def set_defaults(self, defaults):
+ for key, value in defaults:
+ self.el[key] = value
+
+ def get(self, key, default, id):
+
+ self.add_get_history(key, id)
+ return self.el.get(key, default)
+
+ def put(self, key, val, id):
+ self.add_put_history(key, id)
+ self.el[key] = val
+
+ def add_get_history(self, key, id):
+ self.add_history(key, id, self.get_history)
+
+ def add_put_history(self, key, id):
+ self.add_history(key, id, self.put_history)
+
+ def add_history(self, key, id, history):
+ if key not in history:
+ history[key] = [id]
+ else:
+ history[key].append(id)
+
+ def make_models(self):
+ if self.SNAPSHOT_MODELS in self.el:
+ errors = self.make_snapshot()
+ if errors:
+ return errors
+
+ # if GRB WF, create it
+ if self.GRESOURCE_BUNDLE_MODELS in self.el:
+ errors = self.make_generic_resource_bundle()
+ if errors:
+ return errors
+ else:
+ self.el[self.HAS_RESULT] = True
+ self.el[self.RESULT_KEY] = self.SELECTED_GRESOURCE_BUNDLE
+ return
+
+ if self.CONFIG_MODELS in self.el:
+ errors = self.make_software_config_bundle()
+ if errors:
+ return errors
+ else:
+ self.el[self.HAS_RESULT] = True
+ self.el[self.RESULT_KEY] = self.SELECTED_CONFIG_BUNDLE
+ return
+
+ if self.OPNFV_MODELS in self.el:
+ errors = self.make_opnfv_config()
+ if errors:
+ return errors
+ else:
+ self.el[self.HAS_RESULT] = True
+ self.el[self.RESULT_KEY] = self.SELECTED_OPNFV_CONFIG
+
+ if self.BOOKING_MODELS in self.el:
+ errors = self.make_booking()
+ if errors:
+ return errors
+ # create notification
+ booking = self.el[self.BOOKING_MODELS]['booking']
+ NotificationHandler.notify_new_booking(booking)
+
+ def make_snapshot(self):
+ owner = self.el[self.SESSION_USER]
+ models = self.el[self.SNAPSHOT_MODELS]
+ image = models.get('snapshot', Image())
+ booking_id = self.el.get(self.SNAPSHOT_BOOKING_ID)
+ if not booking_id:
+ return "SNAP, No booking ID provided"
+ booking = Booking.objects.get(pk=booking_id)
+ if booking.start > timezone.now() or booking.end < timezone.now():
+ return "Booking is not active"
+ name = self.el.get(self.SNAPSHOT_NAME)
+ if not name:
+ return "SNAP, no name provided"
+ host = models.get('host')
+ if not host:
+ return "SNAP, no host provided"
+ description = self.el.get(self.SNAPSHOT_DESC, "")
+ image.from_lab = booking.lab
+ image.name = name
+ image.description = description
+ image.public = False
+ image.lab_id = -1
+ image.owner = owner
+ image.host_type = host.profile
+ image.save()
+ try:
+ current_image = host.config.image
+ image.os = current_image.os
+ image.save()
+ except Exception:
+ pass
+ JobFactory.makeSnapshotTask(image, booking, host)
+
+ self.el[self.RESULT] = image
+ self.el[self.HAS_RESULT] = True
+
+ def make_generic_resource_bundle(self):
+ owner = self.el[self.SESSION_USER]
+ if self.GRESOURCE_BUNDLE_MODELS in self.el:
+ models = self.el[self.GRESOURCE_BUNDLE_MODELS]
+ if 'hosts' in models:
+ hosts = models['hosts']
+ else:
+ return "GRB has no hosts. CODE:0x0002"
+ if 'bundle' in models:
+ bundle = models['bundle']
+ else:
+ return "GRB, no bundle in models. CODE:0x0003"
+
+ try:
+ bundle.owner = owner
+ bundle.save()
+ except Exception as e:
+ return "GRB, saving bundle generated exception: " + str(e) + " CODE:0x0004"
+ try:
+ for host in hosts:
+ genericresource = host.resource
+ genericresource.bundle = bundle
+ genericresource.save()
+ host.resource = genericresource
+ host.save()
+ except Exception as e:
+ return "GRB, saving hosts generated exception: " + str(e) + " CODE:0x0005"
+
+ if 'networks' in models:
+ for net in models['networks'].values():
+ net.bundle = bundle
+ net.save()
+
+ if 'interfaces' in models:
+ for interface_set in models['interfaces'].values():
+ for interface in interface_set:
+ try:
+ interface.host = interface.host
+ interface.save()
+ except Exception:
+ return "GRB, saving interface " + str(interface) + " failed. CODE:0x0019"
+ else:
+ return "GRB, no interface set provided. CODE:0x001a"
+
+ if 'connections' in models:
+ for resource_name, mapping in models['connections'].items():
+ for profile_name, connection_set in mapping.items():
+ interface = GenericInterface.objects.get(
+ profile__name=profile_name,
+ host__resource__name=resource_name,
+ host__resource__bundle=models['bundle']
+ )
+ for connection in connection_set:
+ try:
+ connection.network = connection.network
+ connection.save()
+ interface.connections.add(connection)
+ except Exception as e:
+ return "GRB, saving vlan " + str(connection) + " failed. Exception: " + str(e) + ". CODE:0x0017"
+ else:
+ return "GRB, no vlan set provided. CODE:0x0018"
+
+ else:
+ return "GRB no models given. CODE:0x0001"
+
+ self.el[self.RESULT] = bundle
+ self.el[self.HAS_RESULT] = True
+ return False
+
+ def make_software_config_bundle(self):
+ models = self.el[self.CONFIG_MODELS]
+ if 'bundle' in models:
+ bundle = models['bundle']
+ bundle.bundle = self.el[self.SELECTED_GRESOURCE_BUNDLE]
+ try:
+ bundle.save()
+ except Exception as e:
+ return "SWC, saving bundle generated exception: " + str(e) + "CODE:0x0007"
+
+ else:
+ return "SWC, no bundle in models. CODE:0x0006"
+ if 'host_configs' in models:
+ host_configs = models['host_configs']
+ for host_config in host_configs:
+ host_config.bundle = host_config.bundle
+ host_config.host = host_config.host
+ try:
+ host_config.save()
+ except Exception as e:
+ return "SWC, saving host configs generated exception: " + str(e) + "CODE:0x0009"
+ else:
+ return "SWC, no host configs in models. CODE:0x0008"
+ if 'opnfv' in models:
+ opnfvconfig = models['opnfv']
+ opnfvconfig.bundle = opnfvconfig.bundle
+ if opnfvconfig.scenario not in opnfvconfig.installer.sup_scenarios.all():
+ return "SWC, scenario not supported by installer. CODE:0x000d"
+ try:
+ opnfvconfig.save()
+ except Exception as e:
+ return "SWC, saving opnfv config generated exception: " + str(e) + "CODE:0x000b"
+ else:
+ pass
+
+ self.el[self.RESULT] = bundle
+ return False
+
+ def make_booking(self):
+ models = self.el[self.BOOKING_MODELS]
+ owner = self.el[self.SESSION_USER]
+
+ if 'booking' in models:
+ booking = models['booking']
+ else:
+ return "BOOK, no booking model exists. CODE:0x000f"
+
+ selected_grb = None
+
+ if self.SELECTED_GRESOURCE_BUNDLE in self.el:
+ selected_grb = self.el[self.SELECTED_GRESOURCE_BUNDLE]
+ else:
+ return "BOOK, no selected resource. CODE:0x000e"
+
+ if self.SELECTED_CONFIG_BUNDLE not in self.el:
+ return "BOOK, no selected config bundle. CODE:0x001f"
+
+ booking.config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
+
+ if not booking.start:
+ return "BOOK, booking has no start. CODE:0x0010"
+ if not booking.end:
+ return "BOOK, booking has no end. CODE:0x0011"
+ if booking.end <= booking.start:
+ return "BOOK, end before/same time as start. CODE:0x0012"
+
+ if 'collaborators' in models:
+ collaborators = models['collaborators']
+ else:
+ return "BOOK, collaborators not defined. CODE:0x0013"
+ try:
+ resource_bundle = ResourceManager.getInstance().convertResourceBundle(selected_grb, config=booking.config_bundle)
+ except ResourceAvailabilityException as e:
+ return "BOOK, requested resources are not available. Exception: " + str(e) + " CODE:0x0014"
+ except ModelValidationException as e:
+ return "Error encountered when saving bundle. " + str(e) + " CODE: 0x001b"
+
+ booking.resource = resource_bundle
+ booking.owner = owner
+ booking.lab = selected_grb.lab
+
+ is_allowed = BookingAuthManager().booking_allowed(booking, self)
+ if not is_allowed:
+ return "BOOK, you are not allowed to book the requested resources"
+
+ try:
+ booking.save()
+ except Exception as e:
+ return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0015"
+
+ for collaborator in collaborators:
+ booking.collaborators.add(collaborator)
+
+ try:
+ booking.pdf = PDFTemplater.makePDF(booking)
+ booking.save()
+ except Exception as e:
+ return "BOOK, failed to create Pod Desriptor File: " + str(e)
+
+ try:
+ JobFactory.makeCompleteJob(booking)
+ except Exception as e:
+ return "BOOK, serializing for api generated exception: " + str(e) + " CODE:0xFFFF"
+
+ try:
+ booking.save()
+ except Exception as e:
+ return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016"
+
+ self.el[self.RESULT] = booking
+ self.el[self.HAS_RESULT] = True
+
+ def make_opnfv_config(self):
+ opnfv_models = self.el[self.OPNFV_MODELS]
+ config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
+ if not config_bundle:
+ return "No Configuration bundle selected"
+ info = opnfv_models.get("meta", {})
+ name = info.get("name", False)
+ desc = info.get("description", False)
+ if not (name and desc):
+ return "No name or description given"
+ installer = opnfv_models['installer_chosen']
+ if not installer:
+ return "No OPNFV Installer chosen"
+ scenario = opnfv_models['scenario_chosen']
+ if not scenario:
+ return "No OPNFV Scenario chosen"
+
+ opnfv_config = OPNFVConfig.objects.create(
+ bundle=config_bundle,
+ name=name,
+ description=desc,
+ installer=installer,
+ scenario=scenario
+ )
+
+ network_roles = opnfv_models['network_roles']
+ for net_role in network_roles:
+ opnfv_config.networks.add(
+ NetworkRole.objects.create(
+ name=net_role['role'],
+ network=net_role['network']
+ )
+ )
+
+ host_roles = opnfv_models['host_roles']
+ for host_role in host_roles:
+ config = config_bundle.hostConfigurations.get(
+ host__resource__name=host_role['host_name']
+ )
+ HostOPNFVConfig.objects.create(
+ role=host_role['role'],
+ host_config=config,
+ opnfv_config=opnfv_config
+ )
+
+ self.el[self.RESULT] = opnfv_config
+ self.el[self.HAS_RESULT] = True
+
+ def __init__(self):
+ self.el = {}
+ self.el[self.CONFIRMATION] = {}
+ self.el["active_step"] = 0
+ self.el[self.HAS_RESULT] = False
+ self.el[self.RESULT] = None
+ self.get_history = {}
+ self.put_history = {}