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