Implement Segmented Workflows
[pharos-tools.git] / dashboard / src / workflow / models.py
1 ##############################################################################
2 # Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
3 #
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 ##############################################################################
9
10
11 from django.shortcuts import render
12 from django.contrib import messages
13 from django.http import HttpResponse
14
15 import yaml
16 import requests
17
18 from workflow.forms import ConfirmationForm
19 from api.models import JobFactory
20 from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
21 from resource_inventory.models import Image, GenericInterface
22 from resource_inventory.resource_manager import ResourceManager
23 from notifier.manager import NotificationHandler
24 from booking.models import Booking
25
26
27 class BookingAuthManager():
28     LFN_PROJECTS = ["opnfv"]  # TODO
29
30     def parse_url(self, info_url):
31         """
32         will return the PTL in the INFO file on success, or None
33         """
34         try:
35             parts = info_url.split("/")
36             if parts[0].find("http") > -1:  # the url include http(s)://
37                 parts = parts[2:]
38             if parts[-1] != "INFO.yaml":
39                 return None
40             if parts[0] not in ["github.com", "raw.githubusercontent.com"]:
41                 return None
42             if parts[1] not in self.LFN_PROJECTS:
43                 return None
44             # now to download and parse file
45             if parts[3] == "blob":
46                 parts[3] = "raw"
47             url = "https://" + "/".join(parts)
48             info_file = requests.get(url, timeout=15).text
49             info_parsed = yaml.load(info_file)
50             ptl = info_parsed.get('project_lead')
51             if not ptl:
52                 return None
53             return ptl
54
55         except Exception:
56             return None
57
58     def booking_allowed(self, booking, repo):
59         """
60         This is the method that will have to change whenever the booking policy changes in the Infra
61         group / LFN. This is a nice isolation of that administration crap
62         currently checks if the booking uses multiple servers. if it does, then the owner must be a PTL,
63         which is checked using the provided info file
64         """
65         if len(booking.resource.template.getHosts()) < 2:
66             return True  # if they only have one server, we dont care
67         if booking.owner.userprofile.booking_privledge:
68             return True  # admin override for this user
69         if repo.BOOKING_INFO_FILE not in repo.el:
70             return False  # INFO file not provided
71         ptl_info = self.parse_url(repo.BOOKING_INFO_FILE)
72         return ptl_info and ptl_info == booking.owner.userprofile.email_addr
73
74
75 class WorkflowStep(object):
76     template = 'bad_request.html'
77     title = "Generic Step"
78     description = "You were led here by mistake"
79     short_title = "error"
80     metastep = None
81
82     def __init__(self, id, repo=None):
83         self.repo = repo
84         self.id = id
85
86     def get_context(self):
87         context = {}
88         context['step_number'] = self.repo_get('steps')
89         context['active_step'] = self.repo_get('active_step')
90         context['render_correct'] = "true"
91         context['step_title'] = self.title
92         context['description'] = self.description
93         return context
94
95     def render(self, request):
96         self.context = self.get_context()
97         return render(request, self.template, self.context)
98
99     def post_render(self, request):
100         return self.render(request)
101
102     def test_render(self, request):
103         if request.method == "POST":
104             return self.post_render(request)
105         return self.render(request)
106
107     def validate(self, request):
108         pass
109
110     def repo_get(self, key, default=None):
111         return self.repo.get(key, default, self.id)
112
113     def repo_put(self, key, value):
114         return self.repo.put(key, value, self.id)
115
116
117 class Confirmation_Step(WorkflowStep):
118     template = 'workflow/confirm.html'
119     title = "Confirm Changes"
120     description = "Does this all look right?"
121
122     def get_vlan_warning(self):
123         grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False)
124         if not grb:
125             return 0
126         if self.repo.BOOKING_MODELS not in self.repo.el:
127             return 0
128         vlan_manager = grb.lab.vlan_manager
129         if vlan_manager is None:
130             return 0
131         hosts = grb.getHosts()
132         for host in hosts:
133             for interface in host.generic_interfaces.all():
134                 for vlan in interface.vlans.all():
135                     if vlan.public:
136                         if not vlan_manager.public_vlan_is_available(vlan.vlan_id):
137                             return 1
138                     else:
139                         if not vlan_manager.is_available(vlan.vlan_id):
140                             return 1  # There is a problem with these vlans
141         return 0
142
143     def get_context(self):
144         context = super(Confirmation_Step, self).get_context()
145         context['form'] = ConfirmationForm()
146         context['confirmation_info'] = yaml.dump(
147             self.repo_get(self.repo.CONFIRMATION),
148             default_flow_style=False
149         ).strip()
150         context['vlan_warning'] = self.get_vlan_warning()
151
152         return context
153
154     def flush_to_db(self):
155         errors = self.repo.make_models()
156         if errors:
157             return errors
158
159     def post_render(self, request):
160         form = ConfirmationForm(request.POST)
161         if form.is_valid():
162             data = form.cleaned_data['confirm']
163             context = self.get_context()
164             if data == "True":
165                 context["bypassed"] = "true"
166                 errors = self.flush_to_db()
167                 if errors:
168                     messages.add_message(request, messages.ERROR, "ERROR OCCURRED: " + errors)
169                 else:
170                     messages.add_message(request, messages.SUCCESS, "Confirmed")
171
172                 return HttpResponse('')
173             elif data == "False":
174                 context["bypassed"] = "true"
175                 messages.add_message(request, messages.SUCCESS, "Canceled")
176                 return render(request, self.template, context)
177             else:
178                 pass
179
180         else:
181             if "vlan_input" in request.POST:
182                 if request.POST.get("vlan_input") == "True":
183                     self.translate_vlans()
184                     return self.render(request)
185             pass
186
187     def translate_vlans(self):
188         grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False)
189         if not grb:
190             return 0
191         vlan_manager = grb.lab.vlan_manager
192         if vlan_manager is None:
193             return 0
194         hosts = grb.getHosts()
195         for host in hosts:
196             for interface in host.generic_interfaces.all():
197                 for vlan in interface.vlans.all():
198                     if not vlan.public:
199                         if not vlan_manager.is_available(vlan.vlan_id):
200                             vlan.vlan_id = vlan_manager.get_vlan()
201                             vlan.save()
202                     else:
203                         if not vlan_manager.public_vlan_is_available(vlan.vlan_id):
204                             pub_vlan = vlan_manager.get_public_vlan()
205                             vlan.vlan_id = pub_vlan.vlan
206                             vlan.save()
207
208
209 class Workflow():
210
211     steps = []
212     active_index = 0
213
214
215 class Repository():
216
217     EDIT = "editing"
218     MODELS = "models"
219     RESOURCE_SELECT = "resource_select"
220     CONFIRMATION = "confirmation"
221     SELECTED_GRESOURCE_BUNDLE = "selected generic bundle pk"
222     SELECTED_CONFIG_BUNDLE = "selected config bundle pk"
223     GRESOURCE_BUNDLE_MODELS = "generic_resource_bundle_models"
224     GRESOURCE_BUNDLE_INFO = "generic_resource_bundle_info"
225     BOOKING = "booking"
226     LAB = "lab"
227     GRB_LAST_HOSTLIST = "grb_network_previous_hostlist"
228     BOOKING_FORMS = "booking_forms"
229     SWCONF_HOSTS = "swconf_hosts"
230     BOOKING_MODELS = "booking models"
231     CONFIG_MODELS = "configuration bundle models"
232     SESSION_USER = "session owner user account"
233     VALIDATED_MODEL_GRB = "valid grb config model instance in db"
234     VALIDATED_MODEL_CONFIG = "valid config model instance in db"
235     VALIDATED_MODEL_BOOKING = "valid booking model instance in db"
236     VLANS = "a list of vlans"
237     SNAPSHOT_MODELS = "the models for snapshotting"
238     SNAPSHOT_BOOKING_ID = "the booking id for snapshotting"
239     SNAPSHOT_NAME = "the name of the snapshot"
240     SNAPSHOT_DESC = "description of the snapshot"
241     BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking"
242
243     #migratory elements of segmented workflow
244     #each of these is the end result of a different workflow.
245     HAS_RESULT = "whether or not workflow has a result"
246     RESULT_KEY = "key for target index that result will be put into in parent"
247     RESULT = "result object from workflow"
248
249     def get_child_defaults(self):
250         return_tuples = []
251         for key in [self.SELECTED_GRESOURCE_BUNDLE, self.SESSION_USER]:
252             return_tuples.append((key, self.el.get(key)))
253         return return_tuples
254
255     def set_defaults(self, defaults):
256         for key, value in defaults:
257             self.el[key] = value
258
259     def get(self, key, default, id):
260         self.add_get_history(key, id)
261         return self.el.get(key, default)
262
263     def put(self, key, val, id):
264         self.add_put_history(key, id)
265         self.el[key] = val
266
267     def add_get_history(self, key, id):
268         self.add_history(key, id, self.get_history)
269
270     def add_put_history(self, key, id):
271         self.add_history(key, id, self.put_history)
272
273     def add_history(self, key, id, history):
274         if key not in history:
275             history[key] = [id]
276         else:
277             history[key].append(id)
278
279     def make_models(self):
280         if self.SNAPSHOT_MODELS in self.el:
281             errors = self.make_snapshot()
282             if errors:
283                 return errors
284         # if GRB WF, create it
285         if self.GRESOURCE_BUNDLE_MODELS in self.el:
286             errors = self.make_generic_resource_bundle()
287             if errors:
288                 return errors
289             else:
290                 self.el[self.HAS_RESULT] = True
291                 self.el[self.RESULT_KEY] = self.SELECTED_GRESOURCE_BUNDLE
292                 return
293
294         if self.CONFIG_MODELS in self.el:
295             errors = self.make_software_config_bundle()
296             if errors:
297                 return errors
298             else:
299                 self.el[self.HAS_RESULT] = True
300                 self.el[self.RESULT_KEY] = self.SELECTED_CONFIG_BUNDLE
301                 return
302
303         if self.BOOKING_MODELS in self.el:
304             errors = self.make_booking()
305             if errors:
306                 return errors
307             # create notification
308             booking = self.el[self.BOOKING_MODELS]['booking']
309             NotificationHandler.notify_new_booking(booking)
310
311     def make_snapshot(self):
312         owner = self.el[self.SESSION_USER]
313         models = self.el[self.SNAPSHOT_MODELS]
314         image = models.get('snapshot', Image())
315         booking_id = self.el.get(self.SNAPSHOT_BOOKING_ID)
316         if not booking_id:
317             return "SNAP, No booking ID provided"
318         booking = Booking.objects.get(pk=booking_id)
319         name = self.el.get(self.SNAPSHOT_NAME)
320         if not name:
321             return "SNAP, no name provided"
322         host = models.get('host')
323         if not host:
324             return "SNAP, no host provided"
325         description = self.el.get(self.SNAPSHOT_DESC, "")
326         image.from_lab = booking.lab
327         image.name = name
328         image.description = description
329         image.public = False
330         image.lab_id = -1
331         image.owner = owner
332         image.host_type = host.profile
333         image.save()
334
335     def make_generic_resource_bundle(self):
336         owner = self.el[self.SESSION_USER]
337         if self.GRESOURCE_BUNDLE_MODELS in self.el:
338             models = self.el[self.GRESOURCE_BUNDLE_MODELS]
339             if 'hosts' in models:
340                 hosts = models['hosts']
341             else:
342                 return "GRB has no hosts. CODE:0x0002"
343             if 'bundle' in models:
344                 bundle = models['bundle']
345             else:
346                 return "GRB, no bundle in models. CODE:0x0003"
347
348             try:
349                 bundle.owner = owner
350                 bundle.save()
351             except Exception as e:
352                 return "GRB, saving bundle generated exception: " + str(e) + " CODE:0x0004"
353             try:
354                 for host in hosts:
355                     genericresource = host.resource
356                     genericresource.bundle = bundle
357                     genericresource.save()
358                     host.resource = genericresource
359                     host.save()
360             except Exception as e:
361                 return "GRB, saving hosts generated exception: " + str(e) + " CODE:0x0005"
362
363             if 'interfaces' in models:
364                 for interface_set in models['interfaces'].values():
365                     for interface in interface_set:
366                         try:
367                             interface.host = interface.host
368                             interface.save()
369                         except Exception as e:
370                             return "GRB, saving interface " + str(interface) + " failed. CODE:0x0019"
371             else:
372                 return "GRB, no interface set provided. CODE:0x001a"
373
374             if 'vlans' in models:
375                 for resource_name, mapping in models['vlans'].items():
376                     for profile_name, vlan_set in mapping.items():
377                         interface = GenericInterface.objects.get(
378                             profile__name=profile_name,
379                             host__resource__name=resource_name,
380                             host__resource__bundle=models['bundle']
381                         )
382                         for vlan in vlan_set:
383                             try:
384                                 vlan.save()
385                                 interface.vlans.add(vlan)
386                             except Exception as e:
387                                 return "GRB, saving vlan " + str(vlan) + " failed. Exception: " + str(e) + ". CODE:0x0017"
388             else:
389                 return "GRB, no vlan set provided. CODE:0x0018"
390
391         else:
392             return "GRB no models given. CODE:0x0001"
393
394         self.el[self.RESULT] = bundle
395         return False
396
397     def make_software_config_bundle(self):
398         models = self.el[self.CONFIG_MODELS]
399         if 'bundle' in models:
400             bundle = models['bundle']
401             bundle.bundle = bundle.bundle
402             try:
403                 bundle.save()
404             except Exception as e:
405                 return "SWC, saving bundle generated exception: " + str(e) + "CODE:0x0007"
406
407         else:
408             return "SWC, no bundle in models. CODE:0x0006"
409         if 'host_configs' in models:
410             host_configs = models['host_configs']
411             for host_config in host_configs:
412                 host_config.bundle = host_config.bundle
413                 host_config.host = host_config.host
414                 try:
415                     host_config.save()
416                 except Exception as e:
417                     return "SWC, saving host configs generated exception: " + str(e) + "CODE:0x0009"
418         else:
419             return "SWC, no host configs in models. CODE:0x0008"
420         if 'opnfv' in models:
421             opnfvconfig = models['opnfv']
422             opnfvconfig.bundle = opnfvconfig.bundle
423             if opnfvconfig.scenario not in opnfvconfig.installer.sup_scenarios.all():
424                 return "SWC, scenario not supported by installer. CODE:0x000d"
425             try:
426                 opnfvconfig.save()
427             except Exception as e:
428                 return "SWC, saving opnfv config generated exception: " + str(e) + "CODE:0x000b"
429         else:
430             pass
431
432         self.el[self.RESULT] = bundle
433         return False
434
435     def make_booking(self):
436         models = self.el[self.BOOKING_MODELS]
437         owner = self.el[self.SESSION_USER]
438
439         if self.SELECTED_GRESOURCE_BUNDLE in self.el:
440             selected_grb = self.el[self.SELECTED_GRESOURCE_BUNDLE]
441         else:
442             return "BOOK, no selected resource. CODE:0x000e"
443
444         if not self.reserve_vlans(selected_grb):
445             return "BOOK, vlans not available"
446
447         if 'booking' in models:
448             booking = models['booking']
449         else:
450             return "BOOK, no booking model exists. CODE:0x000f"
451
452         if not booking.start:
453             return "BOOK, booking has no start. CODE:0x0010"
454         if not booking.end:
455             return "BOOK, booking has no end. CODE:0x0011"
456         if booking.end <= booking.start:
457             return "BOOK, end before/same time as start. CODE:0x0012"
458
459         if 'collaborators' in models:
460             collaborators = models['collaborators']
461         else:
462             return "BOOK, collaborators not defined. CODE:0x0013"
463         try:
464             resource_bundle = ResourceManager.getInstance().convertResourceBundle(selected_grb, config=booking.config_bundle)
465         except ResourceAvailabilityException as e:
466             return "BOOK, requested resources are not available. Exception: " + str(e) + " CODE:0x0014"
467         except ModelValidationException as e:
468             return "Error encountered when saving bundle. " + str(e) + " CODE: 0x001b"
469
470         booking.resource = resource_bundle
471         booking.owner = owner
472         booking.config_bundle = booking.config_bundle
473         booking.lab = selected_grb.lab
474
475         is_allowed = BookingAuthManager().booking_allowed(booking, self)
476         if not is_allowed:
477             return "BOOK, you are not allowed to book the requested resources"
478
479         try:
480             booking.save()
481         except Exception as e:
482             return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0015"
483
484         for collaborator in collaborators:
485             booking.collaborators.add(collaborator)
486
487         try:
488             JobFactory.makeCompleteJob(booking)
489         except Exception as e:
490             return "BOOK, serializing for api generated exception: " + str(e) + " CODE:0xFFFF"
491
492         try:
493             booking.save()
494         except Exception as e:
495             return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016"
496
497     def reserve_vlans(self, grb):
498         """
499         True is success
500         """
501         vlans = []
502         public_vlan = None
503         vlan_manager = grb.lab.vlan_manager
504         if vlan_manager is None:
505             return True
506         for host in grb.getHosts():
507             for interface in host.generic_interfaces.all():
508                 for vlan in interface.vlans.all():
509                     if vlan.public:
510                         public_vlan = vlan
511                     else:
512                         vlans.append(vlan.vlan_id)
513
514         try:
515             vlan_manager.reserve_vlans(vlans)
516             vlan_manager.reserve_public_vlan(public_vlan.vlan_id)
517             return True
518         except Exception:
519             return False
520
521     def __init__(self):
522         self.el = {}
523         self.el[self.CONFIRMATION] = {}
524         self.el["active_step"] = 0
525         self.get_history = {}
526         self.put_history = {}