1 ##############################################################################
2 # Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
11 from django.template.loader import get_template
12 from django.http import HttpResponse
13 from django.utils import timezone
14 from django.db import transaction
19 from workflow.forms import ConfirmationForm
20 from api.models import JobFactory
21 from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
22 from resource_inventory.models import Image, OPNFVConfig, ResourceOPNFVConfig, NetworkRole
23 from resource_inventory.resource_manager import ResourceManager
24 from resource_inventory.pdf_templater import PDFTemplater
25 from notifier.manager import NotificationHandler
26 from booking.models import Booking
29 class BookingAuthManager():
31 Verifies Booking Authorization.
33 Class to verify that the user is allowed to book the requested resource
34 The user must input a url to the INFO.yaml file to prove that they are the ptl of
35 an approved project if they are booking a multi-node pod.
36 This class parses the url and checks the logged in user against the info file.
39 LFN_PROJECTS = ["opnfv"] # TODO
41 def parse_github_url(self, url):
44 parts = url.split("/")
45 if "http" in parts[0]: # the url include http(s)://
47 if parts[-1] != "INFO.yaml":
49 if parts[0] not in ["github.com", "raw.githubusercontent.com"]:
51 if parts[1] not in self.LFN_PROJECTS:
53 # now to download and parse file
54 if parts[3] == "blob":
56 url = "https://" + "/".join(parts)
57 info_file = requests.get(url, timeout=15).text
58 info_parsed = yaml.load(info_file)
59 ptl = info_parsed.get('project_lead')
61 project_leads.append(ptl)
62 sub_ptl = info_parsed.get("subproject_lead")
64 project_leads.append(sub_ptl)
71 def parse_gerrit_url(self, url):
74 halfs = url.split("?")
75 parts = halfs[0].split("/")
76 args = halfs[1].split(";")
77 if "http" in parts[0]: # the url include http(s)://
79 if "f=INFO.yaml" not in args:
81 if "gerrit.opnfv.org" not in parts[0]:
84 i = args.index("a=blob")
85 args[i] = "a=blob_plain"
89 halfs[1] = ";".join(args)
90 halfs[0] = "/".join(parts)
91 # now to download and parse file
92 url = "https://" + "?".join(halfs)
93 info_file = requests.get(url, timeout=15).text
94 info_parsed = yaml.load(info_file)
95 ptl = info_parsed.get('project_lead')
97 project_leads.append(ptl)
98 sub_ptl = info_parsed.get("subproject_lead")
100 project_leads.append(sub_ptl)
107 def parse_opnfv_git_url(self, url):
110 parts = url.split("/")
111 if "http" in parts[0]: # the url include http(s)://
113 if "INFO.yaml" not in parts[-1]:
115 if "git.opnfv.org" not in parts[0]:
117 if parts[-2] == "tree":
119 # now to download and parse file
120 url = "https://" + "/".join(parts)
121 info_file = requests.get(url, timeout=15).text
122 info_parsed = yaml.load(info_file)
123 ptl = info_parsed.get('project_lead')
125 project_leads.append(ptl)
126 sub_ptl = info_parsed.get("subproject_lead")
128 project_leads.append(sub_ptl)
135 def parse_url(self, info_url):
137 Parse the project URL.
139 Gets the INFO.yaml file from the project and returns the PTL info.
141 if "github" in info_url:
142 return self.parse_github_url(info_url)
144 if "gerrit.opnfv.org" in info_url:
145 return self.parse_gerrit_url(info_url)
147 if "git.opnfv.org" in info_url:
148 return self.parse_opnfv_git_url(info_url)
150 def booking_allowed(self, booking, repo):
152 Assert the current Booking Policy.
154 This is the method that will have to change whenever the booking policy changes in the Infra
155 group / LFN. This is a nice isolation of that administration crap
156 currently checks if the booking uses multiple servers. if it does, then the owner must be a PTL,
157 which is checked using the provided info file
159 if booking.owner.userprofile.booking_privledge:
160 return True # admin override for this user
161 if Booking.objects.filter(owner=booking.owner, end__gt=timezone.now()).count() >= 3:
163 if len(booking.resource.template.get_required_resources()) < 2:
164 return True # if they only have one server, we dont care
165 if repo.BOOKING_INFO_FILE not in repo.el:
166 return False # INFO file not provided
167 ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE))
169 if ptl['email'] == booking.owner.userprofile.email_addr:
174 class WorkflowStepStatus(object):
176 Poor man's enum for the status of a workflow step.
178 The steps in a workflow are not completed (UNTOUCHED)
179 or they have been completed correctly (VALID) or they were filled out
180 incorrectly (INVALID)
188 class WorkflowStep(object):
189 template = 'bad_request.html'
190 title = "Generic Step"
191 description = "You were led here by mistake"
192 short_title = "error"
194 # phasing out metastep:
196 valid = WorkflowStepStatus.UNTOUCHED
202 raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete implemented cleanup() method")
213 def set_invalid(self, message, code=WorkflowStepStatus.INVALID):
215 self.message = message
217 def set_valid(self, message, code=WorkflowStepStatus.VALID):
219 self.message = message
223 'title': self.short_title,
224 'enabled': self.enabled,
226 'message': self.message,
229 def __init__(self, id, repo=None):
233 def get_context(self):
235 context['step_number'] = self.repo_get('steps')
236 context['active_step'] = self.repo_get('active_step')
237 context['render_correct'] = "true"
238 context['step_title'] = self.title
239 context['description'] = self.description
242 def render(self, request):
243 return HttpResponse(self.render_to_string(request))
245 def render_to_string(self, request):
246 template = get_template(self.template)
247 return template.render(self.get_context(), request)
249 def post(self, post_content, user):
250 raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete post() method")
252 def validate(self, request):
255 def repo_get(self, key, default=None):
256 return self.repo.get(key, default, self.id)
258 def repo_put(self, key, value):
259 return self.repo.put(key, value, self.id)
264 subclasses have to define the following class attributes:
265 self.select_repo_key: where the selected "object" or "bundle" is to be placed in the repo
266 self.form: the form to be used
267 alert_bundle_missing(): what message to display if a user does not select/selects an invalid object
268 get_form_queryset(): generate a queryset to be used to filter available items for the field
269 get_page_context(): return simple context such as page header and other info
273 class AbstractSelectOrCreate(WorkflowStep):
274 template = 'dashboard/genericselect.html'
275 title = "Select a Bundle"
276 short_title = "select"
277 description = "Generic bundle selector step"
279 select_repo_key = None
280 form = None # subclasses are expected to use a form that is a subclass of SearchableSelectGenericForm
282 def alert_bundle_missing(self): # override in subclasses to change message if field isn't filled out
283 self.set_invalid("Please select a valid bundle")
285 def post(self, post_data, user):
286 form = self.form(post_data, queryset=self.get_form_queryset())
288 bundle = form.get_validated_bundle()
290 self.alert_bundle_missing()
292 self.repo_put(self.select_repo_key, bundle)
293 self.put_confirm_info(bundle)
294 self.set_valid("Step Completed")
296 self.alert_bundle_missing()
298 def get_context(self):
301 bundle = self.repo_get(self.select_repo_key, False)
303 default.append(bundle)
305 form = self.form(queryset=self.get_form_queryset(), initial=default)
307 context = {'form': form, **self.get_page_context()}
308 context.update(super().get_context())
312 def get_page_context():
314 'select_type': 'generic',
315 'select_type_title': 'Generic Bundle'
319 class Confirmation_Step(WorkflowStep):
320 template = 'workflow/confirm.html'
321 title = "Confirm Changes"
322 description = "Does this all look right?"
324 short_title = "confirm"
326 def get_context(self):
327 context = super(Confirmation_Step, self).get_context()
328 context['form'] = ConfirmationForm()
329 # Summary of submitted form data shown on the 'confirm' step of the workflow
330 confirm_details = "\nPod:\n Name: '{name}'\n Description: '{desc}'\nLab: '{lab}'".format(
331 name=self.repo_get(self.repo.CONFIRMATION)['resource']['name'],
332 desc=self.repo_get(self.repo.CONFIRMATION)['resource']['description'],
333 lab=self.repo_get(self.repo.CONFIRMATION)['template']['lab'])
334 confirm_details += "\nResources:"
335 for i, device in enumerate(self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS)['resources']):
336 confirm_details += "\n " + str(device) + ": " + str(self.repo_get(self.repo.CONFIRMATION)['template']['resources'][i]['profile'])
337 context['confirmation_info'] = confirm_details
338 if self.valid == WorkflowStepStatus.VALID:
339 context["confirm_succeeded"] = "true"
343 def flush_to_db(self):
344 errors = self.repo.make_models()
348 def post(self, post_data, user):
349 form = ConfirmationForm(post_data)
351 data = form.cleaned_data['confirm']
353 errors = self.flush_to_db()
355 self.set_invalid("ERROR OCCURRED: " + errors)
357 self.set_valid("Confirmed")
359 elif data == "False":
361 self.set_valid("Canceled")
363 self.set_invalid("Bad Form Contents")
366 self.set_invalid("Bad Form Contents")
373 RESOURCE_SELECT = "resource_select"
374 CONFIRMATION = "confirmation"
375 SELECTED_RESOURCE_TEMPLATE = "selected resource template pk"
376 SELECTED_OPNFV_CONFIG = "selected opnfv deployment config"
377 RESOURCE_TEMPLATE_MODELS = "generic_resource_template_models"
378 RESOURCE_TEMPLATE_INFO = "generic_resource_template_info"
381 RCONFIG_LAST_HOSTLIST = "resource_configuration_network_previous_hostlist"
382 BOOKING_FORMS = "booking_forms"
383 SWCONF_HOSTS = "swconf_hosts"
384 BOOKING_MODELS = "booking models"
385 CONFIG_MODELS = "configuration bundle models"
386 OPNFV_MODELS = "opnfv configuration models"
387 SESSION_USER = "session owner user account"
388 SESSION_MANAGER = "session manager for current session"
389 VALIDATED_MODEL_GRB = "valid grb config model instance in db"
390 VALIDATED_MODEL_CONFIG = "valid config model instance in db"
391 VALIDATED_MODEL_BOOKING = "valid booking model instance in db"
392 VLANS = "a list of vlans"
393 SNAPSHOT_MODELS = "the models for snapshotting"
394 SNAPSHOT_BOOKING_ID = "the booking id for snapshotting"
395 SNAPSHOT_NAME = "the name of the snapshot"
396 SNAPSHOT_DESC = "description of the snapshot"
397 BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking"
399 # new keys for migration to using ResourceTemplates:
400 RESOURCE_TEMPLATE_MODELS = "current working model of resource template"
402 # migratory elements of segmented workflow
403 # each of these is the end result of a different workflow.
404 HAS_RESULT = "whether or not workflow has a result"
405 RESULT_KEY = "key for target index that result will be put into in parent"
406 RESULT = "result object from workflow"
408 def get_child_defaults(self):
410 for key in [self.SELECTED_RESOURCE_TEMPLATE, self.SESSION_USER]:
411 return_tuples.append((key, self.el.get(key)))
414 def set_defaults(self, defaults):
415 for key, value in defaults:
418 def get(self, key, default, id):
420 self.add_get_history(key, id)
421 return self.el.get(key, default)
423 def put(self, key, val, id):
424 self.add_put_history(key, id)
427 def add_get_history(self, key, id):
428 self.add_history(key, id, self.get_history)
430 def add_put_history(self, key, id):
431 self.add_history(key, id, self.put_history)
433 def add_history(self, key, id, history):
434 if key not in history:
437 history[key].append(id)
440 if self.RESOURCE_TEMPLATE_MODELS in self.el:
441 models = self.el[self.RESOURCE_TEMPLATE_MODELS]
442 if models['template'].temporary:
443 models['template'].delete()
444 # deleting current template should cascade delete all
445 # necessary related models
447 def make_models(self):
448 if self.SNAPSHOT_MODELS in self.el:
449 errors = self.make_snapshot()
453 # if GRB WF, create it
454 if self.RESOURCE_TEMPLATE_MODELS in self.el:
455 errors = self.make_generic_resource_bundle()
459 self.el[self.HAS_RESULT] = True
460 self.el[self.RESULT_KEY] = self.SELECTED_RESOURCE_TEMPLATE
463 if self.OPNFV_MODELS in self.el:
464 errors = self.make_opnfv_config()
468 self.el[self.HAS_RESULT] = True
469 self.el[self.RESULT_KEY] = self.SELECTED_OPNFV_CONFIG
471 if self.BOOKING_MODELS in self.el:
472 errors = self.make_booking()
475 # create notification
476 booking = self.el[self.BOOKING_MODELS]['booking']
477 NotificationHandler.notify_new_booking(booking)
479 def make_snapshot(self):
480 owner = self.el[self.SESSION_USER]
481 models = self.el[self.SNAPSHOT_MODELS]
482 image = models.get('snapshot', Image())
483 booking_id = self.el.get(self.SNAPSHOT_BOOKING_ID)
485 return "SNAP, No booking ID provided"
486 booking = Booking.objects.get(pk=booking_id)
487 if booking.start > timezone.now() or booking.end < timezone.now():
488 return "Booking is not active"
489 name = self.el.get(self.SNAPSHOT_NAME)
491 return "SNAP, no name provided"
492 host = models.get('host')
494 return "SNAP, no host provided"
495 description = self.el.get(self.SNAPSHOT_DESC, "")
496 image.from_lab = booking.lab
498 image.description = description
502 image.host_type = host.profile
505 current_image = host.config.image
506 image.os = current_image.os
510 JobFactory.makeSnapshotTask(image, booking, host)
512 self.el[self.RESULT] = image
513 self.el[self.HAS_RESULT] = True
515 def make_generic_resource_bundle(self):
516 owner = self.el[self.SESSION_USER]
517 if self.RESOURCE_TEMPLATE_MODELS in self.el:
518 models = self.el[self.RESOURCE_TEMPLATE_MODELS]
519 models['template'].owner = owner
520 models['template'].temporary = False
521 models['template'].save()
522 self.el[self.RESULT] = models['template']
523 self.el[self.HAS_RESULT] = True
527 return "GRB no models given. CODE:0x0001"
529 def make_software_config_bundle(self):
530 models = self.el[self.CONFIG_MODELS]
531 if 'bundle' in models:
532 bundle = models['bundle']
533 bundle.bundle = self.el[self.SELECTED_RESOURCE_TEMPLATE]
536 except Exception as e:
537 return "SWC, saving bundle generated exception: " + str(e) + "CODE:0x0007"
540 return "SWC, no bundle in models. CODE:0x0006"
541 if 'host_configs' in models:
542 host_configs = models['host_configs']
543 for host_config in host_configs:
544 host_config.template = host_config.template
545 host_config.profile = host_config.profile
548 except Exception as e:
549 return "SWC, saving host configs generated exception: " + str(e) + "CODE:0x0009"
551 return "SWC, no host configs in models. CODE:0x0008"
552 if 'opnfv' in models:
553 opnfvconfig = models['opnfv']
554 opnfvconfig.bundle = opnfvconfig.bundle
555 if opnfvconfig.scenario not in opnfvconfig.installer.sup_scenarios.all():
556 return "SWC, scenario not supported by installer. CODE:0x000d"
559 except Exception as e:
560 return "SWC, saving opnfv config generated exception: " + str(e) + "CODE:0x000b"
564 self.el[self.RESULT] = bundle
567 @transaction.atomic # TODO: Rewrite transactions with savepoints at user level for all workflows
568 def make_booking(self):
569 models = self.el[self.BOOKING_MODELS]
570 owner = self.el[self.SESSION_USER]
572 if 'booking' in models:
573 booking = models['booking']
575 return "BOOK, no booking model exists. CODE:0x000f"
579 if self.SELECTED_RESOURCE_TEMPLATE in self.el:
580 selected_grb = self.el[self.SELECTED_RESOURCE_TEMPLATE]
582 return "BOOK, no selected resource. CODE:0x000e"
584 if not booking.start:
585 return "BOOK, booking has no start. CODE:0x0010"
587 return "BOOK, booking has no end. CODE:0x0011"
588 if booking.end <= booking.start:
589 return "BOOK, end before/same time as start. CODE:0x0012"
591 if 'collaborators' in models:
592 collaborators = models['collaborators']
594 return "BOOK, collaborators not defined. CODE:0x0013"
596 res_manager = ResourceManager.getInstance()
597 resource_bundle = res_manager.instantiateTemplate(selected_grb)
598 except ResourceAvailabilityException as e:
599 return "BOOK, requested resources are not available. Exception: " + str(e) + " CODE:0x0014"
600 except ModelValidationException as e:
601 return "Error encountered when saving bundle. " + str(e) + " CODE: 0x001b"
603 booking.resource = resource_bundle
604 booking.owner = owner
605 booking.lab = selected_grb.lab
607 is_allowed = BookingAuthManager().booking_allowed(booking, self)
609 return "BOOK, you are not allowed to book the requested resources"
613 except Exception as e:
614 return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0015"
616 for collaborator in collaborators:
617 booking.collaborators.add(collaborator)
620 booking.pdf = PDFTemplater.makePDF(booking)
622 except Exception as e:
623 return "BOOK, failed to create Pod Desriptor File: " + str(e)
626 JobFactory.makeCompleteJob(booking)
627 except Exception as e:
628 return "BOOK, serializing for api generated exception: " + str(e) + " CODE:0xFFFF"
632 except Exception as e:
633 return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016"
635 self.el[self.RESULT] = booking
636 self.el[self.HAS_RESULT] = True
638 def make_opnfv_config(self):
639 opnfv_models = self.el[self.OPNFV_MODELS]
640 config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
641 if not config_bundle:
642 return "No Configuration bundle selected"
643 info = opnfv_models.get("meta", {})
644 name = info.get("name", False)
645 desc = info.get("description", False)
646 if not (name and desc):
647 return "No name or description given"
648 installer = opnfv_models['installer_chosen']
650 return "No OPNFV Installer chosen"
651 scenario = opnfv_models['scenario_chosen']
653 return "No OPNFV Scenario chosen"
655 opnfv_config = OPNFVConfig.objects.create(
656 bundle=config_bundle,
663 network_roles = opnfv_models['network_roles']
664 for net_role in network_roles:
665 opnfv_config.networks.add(
666 NetworkRole.objects.create(
667 name=net_role['role'],
668 network=net_role['network']
672 host_roles = opnfv_models['host_roles']
673 for host_role in host_roles:
674 config = config_bundle.hostConfigurations.get(
675 host__resource__name=host_role['host_name']
677 ResourceOPNFVConfig.objects.create(
678 role=host_role['role'],
680 opnfv_config=opnfv_config
683 self.el[self.RESULT] = opnfv_config
684 self.el[self.HAS_RESULT] = True
688 self.el[self.CONFIRMATION] = {}
689 self.el["active_step"] = 0
690 self.el[self.HAS_RESULT] = False
691 self.el[self.RESULT] = None
692 self.get_history = {}
693 self.put_history = {}