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 context['confirmation_info'] = yaml.dump(
330 self.repo_get(self.repo.CONFIRMATION),
331 default_flow_style=False
334 if self.valid == WorkflowStepStatus.VALID:
335 context["confirm_succeeded"] = "true"
339 def flush_to_db(self):
340 errors = self.repo.make_models()
344 def post(self, post_data, user):
345 form = ConfirmationForm(post_data)
347 data = form.cleaned_data['confirm']
349 errors = self.flush_to_db()
351 self.set_invalid("ERROR OCCURRED: " + errors)
353 self.set_valid("Confirmed")
355 elif data == "False":
357 self.set_valid("Canceled")
359 self.set_invalid("Bad Form Contents")
362 self.set_invalid("Bad Form Contents")
369 RESOURCE_SELECT = "resource_select"
370 CONFIRMATION = "confirmation"
371 SELECTED_RESOURCE_TEMPLATE = "selected resource template pk"
372 SELECTED_OPNFV_CONFIG = "selected opnfv deployment config"
373 RESOURCE_TEMPLATE_MODELS = "generic_resource_template_models"
374 RESOURCE_TEMPLATE_INFO = "generic_resource_template_info"
377 RCONFIG_LAST_HOSTLIST = "resource_configuration_network_previous_hostlist"
378 BOOKING_FORMS = "booking_forms"
379 SWCONF_HOSTS = "swconf_hosts"
380 BOOKING_MODELS = "booking models"
381 CONFIG_MODELS = "configuration bundle models"
382 OPNFV_MODELS = "opnfv configuration models"
383 SESSION_USER = "session owner user account"
384 SESSION_MANAGER = "session manager for current session"
385 VALIDATED_MODEL_GRB = "valid grb config model instance in db"
386 VALIDATED_MODEL_CONFIG = "valid config model instance in db"
387 VALIDATED_MODEL_BOOKING = "valid booking model instance in db"
388 VLANS = "a list of vlans"
389 SNAPSHOT_MODELS = "the models for snapshotting"
390 SNAPSHOT_BOOKING_ID = "the booking id for snapshotting"
391 SNAPSHOT_NAME = "the name of the snapshot"
392 SNAPSHOT_DESC = "description of the snapshot"
393 BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking"
395 # new keys for migration to using ResourceTemplates:
396 RESOURCE_TEMPLATE_MODELS = "current working model of resource template"
398 # migratory elements of segmented workflow
399 # each of these is the end result of a different workflow.
400 HAS_RESULT = "whether or not workflow has a result"
401 RESULT_KEY = "key for target index that result will be put into in parent"
402 RESULT = "result object from workflow"
404 def get_child_defaults(self):
406 for key in [self.SELECTED_RESOURCE_TEMPLATE, self.SESSION_USER]:
407 return_tuples.append((key, self.el.get(key)))
410 def set_defaults(self, defaults):
411 for key, value in defaults:
414 def get(self, key, default, id):
416 self.add_get_history(key, id)
417 return self.el.get(key, default)
419 def put(self, key, val, id):
420 self.add_put_history(key, id)
423 def add_get_history(self, key, id):
424 self.add_history(key, id, self.get_history)
426 def add_put_history(self, key, id):
427 self.add_history(key, id, self.put_history)
429 def add_history(self, key, id, history):
430 if key not in history:
433 history[key].append(id)
436 if self.RESOURCE_TEMPLATE_MODELS in self.el:
437 models = self.el[self.RESOURCE_TEMPLATE_MODELS]
438 if models['template'].temporary:
439 models['template'].delete()
440 # deleting current template should cascade delete all
441 # necessary related models
443 def make_models(self):
444 if self.SNAPSHOT_MODELS in self.el:
445 errors = self.make_snapshot()
449 # if GRB WF, create it
450 if self.RESOURCE_TEMPLATE_MODELS in self.el:
451 errors = self.make_generic_resource_bundle()
455 self.el[self.HAS_RESULT] = True
456 self.el[self.RESULT_KEY] = self.SELECTED_RESOURCE_TEMPLATE
459 if self.OPNFV_MODELS in self.el:
460 errors = self.make_opnfv_config()
464 self.el[self.HAS_RESULT] = True
465 self.el[self.RESULT_KEY] = self.SELECTED_OPNFV_CONFIG
467 if self.BOOKING_MODELS in self.el:
468 errors = self.make_booking()
471 # create notification
472 booking = self.el[self.BOOKING_MODELS]['booking']
473 NotificationHandler.notify_new_booking(booking)
475 def make_snapshot(self):
476 owner = self.el[self.SESSION_USER]
477 models = self.el[self.SNAPSHOT_MODELS]
478 image = models.get('snapshot', Image())
479 booking_id = self.el.get(self.SNAPSHOT_BOOKING_ID)
481 return "SNAP, No booking ID provided"
482 booking = Booking.objects.get(pk=booking_id)
483 if booking.start > timezone.now() or booking.end < timezone.now():
484 return "Booking is not active"
485 name = self.el.get(self.SNAPSHOT_NAME)
487 return "SNAP, no name provided"
488 host = models.get('host')
490 return "SNAP, no host provided"
491 description = self.el.get(self.SNAPSHOT_DESC, "")
492 image.from_lab = booking.lab
494 image.description = description
498 image.host_type = host.profile
501 current_image = host.config.image
502 image.os = current_image.os
506 JobFactory.makeSnapshotTask(image, booking, host)
508 self.el[self.RESULT] = image
509 self.el[self.HAS_RESULT] = True
511 def make_generic_resource_bundle(self):
512 owner = self.el[self.SESSION_USER]
513 if self.RESOURCE_TEMPLATE_MODELS in self.el:
514 models = self.el[self.RESOURCE_TEMPLATE_MODELS]
515 models['template'].owner = owner
516 models['template'].temporary = False
517 models['template'].save()
518 self.el[self.RESULT] = models['template']
519 self.el[self.HAS_RESULT] = True
523 return "GRB no models given. CODE:0x0001"
525 def make_software_config_bundle(self):
526 models = self.el[self.CONFIG_MODELS]
527 if 'bundle' in models:
528 bundle = models['bundle']
529 bundle.bundle = self.el[self.SELECTED_RESOURCE_TEMPLATE]
532 except Exception as e:
533 return "SWC, saving bundle generated exception: " + str(e) + "CODE:0x0007"
536 return "SWC, no bundle in models. CODE:0x0006"
537 if 'host_configs' in models:
538 host_configs = models['host_configs']
539 for host_config in host_configs:
540 host_config.template = host_config.template
541 host_config.profile = host_config.profile
544 except Exception as e:
545 return "SWC, saving host configs generated exception: " + str(e) + "CODE:0x0009"
547 return "SWC, no host configs in models. CODE:0x0008"
548 if 'opnfv' in models:
549 opnfvconfig = models['opnfv']
550 opnfvconfig.bundle = opnfvconfig.bundle
551 if opnfvconfig.scenario not in opnfvconfig.installer.sup_scenarios.all():
552 return "SWC, scenario not supported by installer. CODE:0x000d"
555 except Exception as e:
556 return "SWC, saving opnfv config generated exception: " + str(e) + "CODE:0x000b"
560 self.el[self.RESULT] = bundle
563 @transaction.atomic # TODO: Rewrite transactions with savepoints at user level for all workflows
564 def make_booking(self):
565 models = self.el[self.BOOKING_MODELS]
566 owner = self.el[self.SESSION_USER]
568 if 'booking' in models:
569 booking = models['booking']
571 return "BOOK, no booking model exists. CODE:0x000f"
575 if self.SELECTED_RESOURCE_TEMPLATE in self.el:
576 selected_grb = self.el[self.SELECTED_RESOURCE_TEMPLATE]
578 return "BOOK, no selected resource. CODE:0x000e"
580 if not booking.start:
581 return "BOOK, booking has no start. CODE:0x0010"
583 return "BOOK, booking has no end. CODE:0x0011"
584 if booking.end <= booking.start:
585 return "BOOK, end before/same time as start. CODE:0x0012"
587 if 'collaborators' in models:
588 collaborators = models['collaborators']
590 return "BOOK, collaborators not defined. CODE:0x0013"
592 res_manager = ResourceManager.getInstance()
593 resource_bundle = res_manager.instantiateTemplate(selected_grb)
594 except ResourceAvailabilityException as e:
595 return "BOOK, requested resources are not available. Exception: " + str(e) + " CODE:0x0014"
596 except ModelValidationException as e:
597 return "Error encountered when saving bundle. " + str(e) + " CODE: 0x001b"
599 booking.resource = resource_bundle
600 booking.owner = owner
601 booking.lab = selected_grb.lab
603 is_allowed = BookingAuthManager().booking_allowed(booking, self)
605 return "BOOK, you are not allowed to book the requested resources"
609 except Exception as e:
610 return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0015"
612 for collaborator in collaborators:
613 booking.collaborators.add(collaborator)
616 booking.pdf = PDFTemplater.makePDF(booking)
618 except Exception as e:
619 return "BOOK, failed to create Pod Desriptor File: " + str(e)
622 JobFactory.makeCompleteJob(booking)
623 except Exception as e:
624 return "BOOK, serializing for api generated exception: " + str(e) + " CODE:0xFFFF"
628 except Exception as e:
629 return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016"
631 self.el[self.RESULT] = booking
632 self.el[self.HAS_RESULT] = True
634 def make_opnfv_config(self):
635 opnfv_models = self.el[self.OPNFV_MODELS]
636 config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
637 if not config_bundle:
638 return "No Configuration bundle selected"
639 info = opnfv_models.get("meta", {})
640 name = info.get("name", False)
641 desc = info.get("description", False)
642 if not (name and desc):
643 return "No name or description given"
644 installer = opnfv_models['installer_chosen']
646 return "No OPNFV Installer chosen"
647 scenario = opnfv_models['scenario_chosen']
649 return "No OPNFV Scenario chosen"
651 opnfv_config = OPNFVConfig.objects.create(
652 bundle=config_bundle,
659 network_roles = opnfv_models['network_roles']
660 for net_role in network_roles:
661 opnfv_config.networks.add(
662 NetworkRole.objects.create(
663 name=net_role['role'],
664 network=net_role['network']
668 host_roles = opnfv_models['host_roles']
669 for host_role in host_roles:
670 config = config_bundle.hostConfigurations.get(
671 host__resource__name=host_role['host_name']
673 ResourceOPNFVConfig.objects.create(
674 role=host_role['role'],
676 opnfv_config=opnfv_config
679 self.el[self.RESULT] = opnfv_config
680 self.el[self.HAS_RESULT] = True
684 self.el[self.CONFIRMATION] = {}
685 self.el["active_step"] = 0
686 self.el[self.HAS_RESULT] = False
687 self.el[self.RESULT] = None
688 self.get_history = {}
689 self.put_history = {}