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