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