aboutsummaryrefslogtreecommitdiffstats
path: root/src/workflow/models.py
diff options
context:
space:
mode:
authorJustin Choquette <jchoquette@iol.unh.edu>2023-06-08 12:46:53 -0400
committerJustin Choquette <jchoquette@iol.unh.edu>2023-07-21 13:17:51 -0400
commita09db9f287a02873c0226759f8ea444bb304cd59 (patch)
tree59e744e4b998973a808abbae2d21fbdd6201d829 /src/workflow/models.py
parent8ddc7e820e120f1dde4e901d3cb6f1dd3f281e65 (diff)
LaaS 3.0 Almost MVP
Change-Id: Ided9a43cf3088bb58a233dc459711c03f43e11b8 Signed-off-by: Justin Choquette <jchoquette@iol.unh.edu>
Diffstat (limited to 'src/workflow/models.py')
-rw-r--r--src/workflow/models.py687
1 files changed, 1 insertions, 686 deletions
diff --git a/src/workflow/models.py b/src/workflow/models.py
index e065202..f69ee85 100644
--- a/src/workflow/models.py
+++ b/src/workflow/models.py
@@ -5,689 +5,4 @@
# 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.template.loader import get_template
-from django.http import HttpResponse
-from django.utils import timezone
-from django.db import transaction
-
-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, OPNFVConfig, ResourceOPNFVConfig, 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():
- """
- Verifies Booking Authorization.
-
- Class to verify that the user is allowed to book the requested resource
- The user must input a url to the INFO.yaml file to prove that they are the ptl of
- an approved project if they are booking a multi-node pod.
- This class parses the url and checks the logged in user against the info file.
- """
-
- 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):
- """
- Parse the project URL.
-
- Gets the INFO.yaml file from the project and returns the PTL info.
- """
- 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):
- """
- Assert the current Booking Policy.
-
- 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.get_required_resources()) < 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):
- """
- Poor man's enum for the status of a workflow step.
-
- The steps in a workflow are not completed (UNTOUCHED)
- or they have been completed correctly (VALID) or they were filled out
- incorrectly (INVALID)
- """
-
- 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):
- return HttpResponse(self.render_to_string(request))
-
- def render_to_string(self, request):
- template = get_template(self.template)
- return template.render(self.get_context(), request)
-
- def post(self, post_content, user):
- raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete post() method")
-
- 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(self, post_data, user):
- form = self.form(post_data, queryset=self.get_form_queryset())
- if form.is_valid():
- bundle = form.get_validated_bundle()
- if not bundle:
- self.alert_bundle_missing()
- return
- self.repo_put(self.select_repo_key, bundle)
- self.put_confirm_info(bundle)
- self.set_valid("Step Completed")
- else:
- self.alert_bundle_missing()
-
- 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()
- # Summary of submitted form data shown on the 'confirm' step of the workflow
- confirm_details = "\nPod:\n Name: '{name}'\n Description: '{desc}'\nLab: '{lab}'".format(
- name=self.repo_get(self.repo.CONFIRMATION)['resource']['name'],
- desc=self.repo_get(self.repo.CONFIRMATION)['resource']['description'],
- lab=self.repo_get(self.repo.CONFIRMATION)['template']['lab'])
- confirm_details += "\nResources:"
- for i, device in enumerate(self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS)['resources']):
- confirm_details += "\n " + str(device) + ": " + str(self.repo_get(self.repo.CONFIRMATION)['template']['resources'][i]['profile'])
- context['confirmation_info'] = confirm_details
- if self.valid == WorkflowStepStatus.VALID:
- context["confirm_succeeded"] = "true"
-
- return context
-
- def flush_to_db(self):
- errors = self.repo.make_models()
- if errors:
- return errors
-
- def post(self, post_data, user):
- form = ConfirmationForm(post_data)
- if form.is_valid():
- data = form.cleaned_data['confirm']
- if data == "True":
- errors = self.flush_to_db()
- if errors:
- self.set_invalid("ERROR OCCURRED: " + errors)
- else:
- self.set_valid("Confirmed")
-
- elif data == "False":
- self.repo.cancel()
- self.set_valid("Canceled")
- else:
- self.set_invalid("Bad Form Contents")
-
- else:
- self.set_invalid("Bad Form Contents")
-
-
-class Repository():
-
- EDIT = "editing"
- MODELS = "models"
- RESOURCE_SELECT = "resource_select"
- CONFIRMATION = "confirmation"
- SELECTED_RESOURCE_TEMPLATE = "selected resource template pk"
- SELECTED_OPNFV_CONFIG = "selected opnfv deployment config"
- RESOURCE_TEMPLATE_MODELS = "generic_resource_template_models"
- RESOURCE_TEMPLATE_INFO = "generic_resource_template_info"
- BOOKING = "booking"
- LAB = "lab"
- RCONFIG_LAST_HOSTLIST = "resource_configuration_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"
-
- # new keys for migration to using ResourceTemplates:
- RESOURCE_TEMPLATE_MODELS = "current working model of resource template"
-
- # 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_RESOURCE_TEMPLATE, 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 cancel(self):
- if self.RESOURCE_TEMPLATE_MODELS in self.el:
- models = self.el[self.RESOURCE_TEMPLATE_MODELS]
- if models['template'].temporary:
- models['template'].delete()
- # deleting current template should cascade delete all
- # necessary related models
-
- 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.RESOURCE_TEMPLATE_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_RESOURCE_TEMPLATE
- 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.RESOURCE_TEMPLATE_MODELS in self.el:
- models = self.el[self.RESOURCE_TEMPLATE_MODELS]
- models['template'].owner = owner
- models['template'].temporary = False
- models['template'].save()
- self.el[self.RESULT] = models['template']
- self.el[self.HAS_RESULT] = True
- return False
-
- else:
- return "GRB no models given. CODE:0x0001"
-
- 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_RESOURCE_TEMPLATE]
- 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.template = host_config.template
- host_config.profile = host_config.profile
- 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
-
- @transaction.atomic # TODO: Rewrite transactions with savepoints at user level for all workflows
- 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_RESOURCE_TEMPLATE in self.el:
- selected_grb = self.el[self.SELECTED_RESOURCE_TEMPLATE]
- else:
- return "BOOK, no selected resource. CODE:0x000e"
-
- 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:
- res_manager = ResourceManager.getInstance()
- resource_bundle = res_manager.instantiateTemplate(selected_grb)
- 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']
- )
- ResourceOPNFVConfig.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 = {}
+############################################################################## \ No newline at end of file