############################################################################## # 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.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.getResources()) < 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() context['confirmation_info'] = yaml.dump( self.repo_get(self.repo.CONFIRMATION), default_flow_style=False ).strip() 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 = {}