Quick Deploy Fixes.
[laas.git] / src / api / 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.core.exceptions import PermissionDenied, ValidationError
14 from django.shortcuts import get_object_or_404
15 from django.http import HttpResponseNotFound
16 from django.urls import reverse
17 from django.utils import timezone
18
19 import json
20 import uuid
21
22 from booking.models import Booking
23 from resource_inventory.models import (
24     Lab,
25     ResourceProfile,
26     Image,
27     Interface,
28     ResourceOPNFVConfig,
29     RemoteInfo,
30     OPNFVConfig,
31     ConfigState,
32     ResourceQuery
33 )
34 from resource_inventory.idf_templater import IDFTemplater
35 from resource_inventory.pdf_templater import PDFTemplater
36 from account.models import Downtime
37 from dashboard.utils import AbstractModelQuery
38
39
40 class JobStatus(object):
41     """
42     A poor man's enum for a job's status.
43
44     A job is NEW if it has not been started or recognized by the Lab
45     A job is CURRENT if it has been started by the lab but it is not yet completed
46     a job is DONE if all the tasks are complete and the booking is ready to use
47     """
48
49     NEW = 0
50     CURRENT = 100
51     DONE = 200
52     ERROR = 300
53
54
55 class LabManagerTracker(object):
56
57     @classmethod
58     def get(cls, lab_name, token):
59         """
60         Get a LabManager.
61
62         Takes in a lab name (from a url path)
63         returns a lab manager instance for that lab, if it exists
64         Also checks that the given API token is correct
65         """
66         try:
67             lab = Lab.objects.get(name=lab_name)
68         except Exception:
69             raise PermissionDenied("Lab not found")
70         if lab.api_token == token:
71             return LabManager(lab)
72         raise PermissionDenied("Lab not authorized")
73
74
75 class LabManager(object):
76     """
77     Handles all lab REST calls.
78
79     handles jobs, inventory, status, etc
80     may need to create helper classes
81     """
82
83     def __init__(self, lab):
84         self.lab = lab
85
86     def get_downtime(self):
87         return Downtime.objects.filter(start__lt=timezone.now(), end__gt=timezone.now(), lab=self.lab)
88
89     def get_downtime_json(self):
90         downtime = self.get_downtime().first()  # should only be one item in queryset
91         if downtime:
92             return {
93                 "is_down": True,
94                 "start": downtime.start,
95                 "end": downtime.end,
96                 "description": downtime.description
97             }
98         return {"is_down": False}
99
100     def create_downtime(self, form):
101         """
102         Create a downtime event.
103
104         Takes in a dictionary that describes the model.
105         {
106           "start": utc timestamp
107           "end": utc timestamp
108           "description": human text (optional)
109         }
110         For timestamp structure, https://docs.djangoproject.com/en/2.2/ref/forms/fields/#datetimefield
111         """
112         Downtime.objects.create(
113             start=form.cleaned_data['start'],
114             end=form.cleaned_data['end'],
115             description=form.cleaned_data['description'],
116             lab=self.lab
117         )
118         return self.get_downtime_json()
119
120     def update_host_remote_info(self, data, res_id):
121         resource = ResourceQuery.filter(labid=res_id, lab=self.lab)
122         if len(resource) != 1:
123             return HttpResponseNotFound("Could not find single host with id " + str(res_id))
124         resource = resource[0]
125         info = {}
126         try:
127             info['address'] = data['address']
128             info['mac_address'] = data['mac_address']
129             info['password'] = data['password']
130             info['user'] = data['user']
131             info['type'] = data['type']
132             info['versions'] = json.dumps(data['versions'])
133         except Exception as e:
134             return {"error": "invalid arguement: " + str(e)}
135         remote_info = resource.remote_management
136         if "default" in remote_info.mac_address:
137             remote_info = RemoteInfo()
138         remote_info.address = info['address']
139         remote_info.mac_address = info['mac_address']
140         remote_info.password = info['password']
141         remote_info.user = info['user']
142         remote_info.type = info['type']
143         remote_info.versions = info['versions']
144         remote_info.save()
145         resource.remote_management = remote_info
146         resource.save()
147         booking = Booking.objects.get(resource=resource.bundle)
148         self.update_xdf(booking)
149         return {"status": "success"}
150
151     def update_xdf(self, booking):
152         booking.pdf = PDFTemplater.makePDF(booking)
153         booking.idf = IDFTemplater().makeIDF(booking)
154         booking.save()
155
156     def get_pdf(self, booking_id):
157         booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
158         return booking.pdf
159
160     def get_idf(self, booking_id):
161         booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
162         return booking.idf
163
164     def get_profile(self):
165         prof = {}
166         prof['name'] = self.lab.name
167         prof['contact'] = {
168             "phone": self.lab.contact_phone,
169             "email": self.lab.contact_email
170         }
171         prof['host_count'] = [{
172             "type": profile.name,
173             "count": len(profile.get_resources(lab=self.lab))}
174             for profile in ResourceProfile.objects.filter(labs=self.lab)]
175         return prof
176
177     def get_inventory(self):
178         inventory = {}
179         resources = ResourceQuery.filter(lab=self.lab)
180         images = Image.objects.filter(from_lab=self.lab)
181         profiles = ResourceProfile.objects.filter(labs=self.lab)
182         inventory['resources'] = self.serialize_resources(resources)
183         inventory['images'] = self.serialize_images(images)
184         inventory['host_types'] = self.serialize_host_profiles(profiles)
185         return inventory
186
187     def get_host(self, hostname):
188         resource = ResourceQuery.filter(labid=hostname, lab=self.lab)
189         if len(resource) != 1:
190             return HttpResponseNotFound("Could not find single host with id " + str(hostname))
191         resource = resource[0]
192         return {
193             "booked": resource.booked,
194             "working": resource.working,
195             "type": resource.profile.name
196         }
197
198     def update_host(self, hostname, data):
199         resource = ResourceQuery.filter(labid=hostname, lab=self.lab)
200         if len(resource) != 1:
201             return HttpResponseNotFound("Could not find single host with id " + str(hostname))
202         resource = resource[0]
203         if "working" in data:
204             working = data['working'] == "true"
205             resource.working = working
206         resource.save()
207         return self.get_host(hostname)
208
209     def get_status(self):
210         return {"status": self.lab.status}
211
212     def set_status(self, payload):
213         {}
214
215     def get_current_jobs(self):
216         jobs = Job.objects.filter(booking__lab=self.lab)
217
218         return self.serialize_jobs(jobs, status=JobStatus.CURRENT)
219
220     def get_new_jobs(self):
221         jobs = Job.objects.filter(booking__lab=self.lab)
222
223         return self.serialize_jobs(jobs, status=JobStatus.NEW)
224
225     def get_done_jobs(self):
226         jobs = Job.objects.filter(booking__lab=self.lab)
227
228         return self.serialize_jobs(jobs, status=JobStatus.DONE)
229
230     def get_job(self, jobid):
231         return Job.objects.get(pk=jobid).to_dict()
232
233     def update_job(self, jobid, data):
234         {}
235
236     def serialize_jobs(self, jobs, status=JobStatus.NEW):
237         job_ser = []
238         for job in jobs:
239             jsonized_job = job.get_delta(status)
240             if len(jsonized_job['payload']) < 1:
241                 continue
242             job_ser.append(jsonized_job)
243
244         return job_ser
245
246     def serialize_resources(self, resources):
247         # TODO: rewrite for Resource model
248         host_ser = []
249         for res in resources:
250             r = {
251                 'interfaces': [],
252                 'hostname': res.name,
253                 'host_type': res.profile.name
254             }
255             for iface in res.get_interfaces():
256                 r['interfaces'].append({
257                     'mac': iface.mac_address,
258                     'busaddr': iface.bus_address,
259                     'name': iface.name,
260                     'switchport': {"switch_name": iface.switch_name, "port_name": iface.port_name}
261                 })
262         return host_ser
263
264     def serialize_images(self, images):
265         images_ser = []
266         for image in images:
267             images_ser.append(
268                 {
269                     "name": image.name,
270                     "lab_id": image.lab_id,
271                     "dashboard_id": image.id
272                 }
273             )
274         return images_ser
275
276     def serialize_resource_profiles(self, profiles):
277         profile_ser = []
278         for profile in profiles:
279             p = {}
280             p['cpu'] = {
281                 "cores": profile.cpuprofile.first().cores,
282                 "arch": profile.cpuprofile.first().architecture,
283                 "cpus": profile.cpuprofile.first().cpus,
284             }
285             p['disks'] = []
286             for disk in profile.storageprofile.all():
287                 d = {
288                     "size": disk.size,
289                     "type": disk.media_type,
290                     "name": disk.name
291                 }
292                 p['disks'].append(d)
293             p['description'] = profile.description
294             p['interfaces'] = []
295             for iface in profile.interfaceprofile.all():
296                 p['interfaces'].append(
297                     {
298                         "speed": iface.speed,
299                         "name": iface.name
300                     }
301                 )
302
303             p['ram'] = {"amount": profile.ramprofile.first().amount}
304             p['name'] = profile.name
305             profile_ser.append(p)
306         return profile_ser
307
308
309 class Job(models.Model):
310     """
311     A Job to be performed by the Lab.
312
313     The API uses Jobs and Tasks to communicate actions that need to be taken to the Lab
314     that is hosting a booking. A booking from a user has an associated Job which tells
315     the lab how to configure the hardware, networking, etc to fulfill the booking
316     for the user.
317     This is the class that is serialized and put into the api
318     """
319
320     booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
321     status = models.IntegerField(default=JobStatus.NEW)
322     complete = models.BooleanField(default=False)
323
324     def to_dict(self):
325         d = {}
326         for relation in self.get_tasklist():
327             if relation.job_key not in d:
328                 d[relation.job_key] = {}
329             d[relation.job_key][relation.task_id] = relation.config.to_dict()
330
331         return {"id": self.id, "payload": d}
332
333     def get_tasklist(self, status="all"):
334         if status != "all":
335             return JobTaskQuery.filter(job=self, status=status)
336         return JobTaskQuery.filter(job=self)
337
338     def is_fulfilled(self):
339         """
340         If a job has been completed by the lab.
341
342         This method should return true if all of the job's tasks are done,
343         and false otherwise
344         """
345         my_tasks = self.get_tasklist()
346         for task in my_tasks:
347             if task.status != JobStatus.DONE:
348                 return False
349         return True
350
351     def get_delta(self, status):
352         d = {}
353         for relation in self.get_tasklist(status=status):
354             if relation.job_key not in d:
355                 d[relation.job_key] = {}
356             d[relation.job_key][relation.task_id] = relation.config.get_delta()
357
358         return {"id": self.id, "payload": d}
359
360     def to_json(self):
361         return json.dumps(self.to_dict())
362
363
364 class TaskConfig(models.Model):
365     state = models.IntegerField(default=ConfigState.CLEAN)
366
367     keys = set()  # TODO: This needs to be an instance variable, not a class variable
368     delta_keys_list = models.CharField(max_length=200, default="[]")
369
370     @property
371     def delta_keys(self):
372         return list(set(json.loads(self.delta_keys_list)))
373
374     @delta_keys.setter
375     def delta_keys(self, keylist):
376         self.delta_keys_list = json.dumps(keylist)
377
378     def to_dict(self):
379         raise NotImplementedError
380
381     def get_delta(self):
382         raise NotImplementedError
383
384     def format_delta(self, config, token):
385         delta = {k: config[k] for k in self.delta_keys}
386         delta['lab_token'] = token
387         return delta
388
389     def to_json(self):
390         return json.dumps(self.to_dict())
391
392     def clear_delta(self):
393         self.delta_keys = []
394
395     def set(self, *args):
396         dkeys = self.delta_keys
397         for arg in args:
398             if arg in self.keys:
399                 dkeys.append(arg)
400         self.delta_keys = dkeys
401
402
403 class BridgeConfig(models.Model):
404     """Displays mapping between jumphost interfaces and bridges."""
405
406     interfaces = models.ManyToManyField(Interface)
407     opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
408
409     def to_dict(self):
410         d = {}
411         hid = ResourceQuery.get(interface__pk=self.interfaces.first().pk).labid
412         d[hid] = {}
413         for interface in self.interfaces.all():
414             d[hid][interface.mac_address] = []
415             for vlan in interface.config.all():
416                 network_role = self.opnfv_model.networks().filter(network=vlan.network)
417                 bridge = IDFTemplater.bridge_names[network_role.name]
418                 br_config = {
419                     "vlan_id": vlan.vlan_id,
420                     "tagged": vlan.tagged,
421                     "bridge": bridge
422                 }
423                 d[hid][interface.mac_address].append(br_config)
424         return d
425
426     def to_json(self):
427         return json.dumps(self.to_dict())
428
429
430 class OpnfvApiConfig(models.Model):
431
432     installer = models.CharField(max_length=200)
433     scenario = models.CharField(max_length=300)
434     roles = models.ManyToManyField(ResourceOPNFVConfig)
435     # pdf and idf are url endpoints, not the actual file
436     pdf = models.CharField(max_length=100)
437     idf = models.CharField(max_length=100)
438     bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True)
439     delta = models.TextField()
440     opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL)
441
442     def to_dict(self):
443         d = {}
444         if not self.opnfv_config:
445             return d
446         if self.installer:
447             d['installer'] = self.installer
448         if self.scenario:
449             d['scenario'] = self.scenario
450         if self.pdf:
451             d['pdf'] = self.pdf
452         if self.idf:
453             d['idf'] = self.idf
454         if self.bridge_config:
455             d['bridged_interfaces'] = self.bridge_config.to_dict()
456
457         hosts = self.roles.all()
458         if hosts.exists():
459             d['roles'] = []
460             for host in hosts:
461                 d['roles'].append({
462                     host.labid: self.opnfv_config.host_opnfv_config.get(
463                         host_config__pk=host.config.pk
464                     ).role.name
465                 })
466
467         return d
468
469     def to_json(self):
470         return json.dumps(self.to_dict())
471
472     def set_installer(self, installer):
473         self.installer = installer
474         d = json.loads(self.delta)
475         d['installer'] = installer
476         self.delta = json.dumps(d)
477
478     def set_scenario(self, scenario):
479         self.scenario = scenario
480         d = json.loads(self.delta)
481         d['scenario'] = scenario
482         self.delta = json.dumps(d)
483
484     def set_xdf(self, booking, update_delta=True):
485         kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id}
486         self.pdf = reverse('get-pdf', kwargs=kwargs)
487         self.idf = reverse('get-idf', kwargs=kwargs)
488         if update_delta:
489             d = json.loads(self.delta)
490             d['pdf'] = self.pdf
491             d['idf'] = self.idf
492             self.delta = json.dumps(d)
493
494     def add_role(self, host):
495         self.roles.add(host)
496         d = json.loads(self.delta)
497         if 'role' not in d:
498             d['role'] = []
499         d['roles'].append({host.labid: host.config.opnfvRole.name})
500         self.delta = json.dumps(d)
501
502     def clear_delta(self):
503         self.delta = '{}'
504
505     def get_delta(self):
506         if not self.delta:
507             self.delta = self.to_json()
508             self.save()
509         return json.loads(self.delta)
510
511
512 class AccessConfig(TaskConfig):
513     access_type = models.CharField(max_length=50)
514     user = models.ForeignKey(User, on_delete=models.CASCADE)
515     revoke = models.BooleanField(default=False)
516     context = models.TextField(default="")
517     delta = models.TextField(default="{}")
518
519     def to_dict(self):
520         d = {}
521         d['access_type'] = self.access_type
522         d['user'] = self.user.id
523         d['revoke'] = self.revoke
524         try:
525             d['context'] = json.loads(self.context)
526         except Exception:
527             pass
528         return d
529
530     def get_delta(self):
531         if not self.delta:
532             self.delta = self.to_json()
533             self.save()
534         d = json.loads(self.delta)
535         d["lab_token"] = self.accessrelation.lab_token
536
537         return d
538
539     def to_json(self):
540         return json.dumps(self.to_dict())
541
542     def clear_delta(self):
543         d = {}
544         d["lab_token"] = self.accessrelation.lab_token
545         self.delta = json.dumps(d)
546
547     def set_access_type(self, access_type):
548         self.access_type = access_type
549         d = json.loads(self.delta)
550         d['access_type'] = access_type
551         self.delta = json.dumps(d)
552
553     def set_user(self, user):
554         self.user = user
555         d = json.loads(self.delta)
556         d['user'] = self.user.id
557         self.delta = json.dumps(d)
558
559     def set_revoke(self, revoke):
560         self.revoke = revoke
561         d = json.loads(self.delta)
562         d['revoke'] = revoke
563         self.delta = json.dumps(d)
564
565     def set_context(self, context):
566         self.context = json.dumps(context)
567         d = json.loads(self.delta)
568         d['context'] = context
569         self.delta = json.dumps(d)
570
571
572 class SoftwareConfig(TaskConfig):
573     """Handles software installations, such as OPNFV or ONAP."""
574
575     opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
576
577     def to_dict(self):
578         d = {}
579         if self.opnfv:
580             d['opnfv'] = self.opnfv.to_dict()
581
582         d["lab_token"] = self.softwarerelation.lab_token
583         self.delta = json.dumps(d)
584
585         return d
586
587     def get_delta(self):
588         d = {}
589         d['opnfv'] = self.opnfv.get_delta()
590         d['lab_token'] = self.softwarerelation.lab_token
591
592         return d
593
594     def clear_delta(self):
595         self.opnfv.clear_delta()
596
597     def to_json(self):
598         return json.dumps(self.to_dict())
599
600
601 class HardwareConfig(TaskConfig):
602     """Describes the desired configuration of the hardware."""
603
604     image = models.CharField(max_length=100, default="defimage")
605     power = models.CharField(max_length=100, default="off")
606     hostname = models.CharField(max_length=100, default="hostname")
607     ipmi_create = models.BooleanField(default=False)
608     delta = models.TextField()
609
610     keys = set(["id", "image", "power", "hostname", "ipmi_create"])
611
612     def get_delta(self):
613         return self.format_delta(
614             self.hosthardwarerelation.get_resource().get_configuration(self.state),
615             self.hosthardwarerelation.lab_token)
616
617
618 class NetworkConfig(TaskConfig):
619     """Handles network configuration."""
620
621     interfaces = models.ManyToManyField(Interface)
622     delta = models.TextField()
623
624     def to_dict(self):
625         d = {}
626         hid = self.hostnetworkrelation.resource_id
627         d[hid] = {}
628         for interface in self.interfaces.all():
629             d[hid][interface.mac_address] = []
630             for vlan in interface.config.all():
631                 # TODO: should this come from the interface?
632                 # e.g. will different interfaces for different resources need different configs?
633                 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
634
635         return d
636
637     def to_json(self):
638         return json.dumps(self.to_dict())
639
640     def get_delta(self):
641         if not self.delta:
642             self.delta = self.to_json()
643             self.save()
644         d = json.loads(self.delta)
645         d['lab_token'] = self.hostnetworkrelation.lab_token
646         return d
647
648     def clear_delta(self):
649         self.delta = json.dumps(self.to_dict())
650         self.save()
651
652     def add_interface(self, interface):
653         self.interfaces.add(interface)
654         d = json.loads(self.delta)
655         hid = self.hostnetworkrelation.resource_id
656         if hid not in d:
657             d[hid] = {}
658         d[hid][interface.mac_address] = []
659         for vlan in interface.config.all():
660             d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
661         self.delta = json.dumps(d)
662
663
664 class SnapshotConfig(TaskConfig):
665
666     resource_id = models.CharField(max_length=200, default="default_id")
667     image = models.IntegerField(null=True)
668     dashboard_id = models.IntegerField()
669     delta = models.TextField(default="{}")
670
671     def to_dict(self):
672         d = {}
673         if self.host:
674             d['host'] = self.host.labid
675         if self.image:
676             d['image'] = self.image
677         d['dashboard_id'] = self.dashboard_id
678         return d
679
680     def to_json(self):
681         return json.dumps(self.to_dict())
682
683     def get_delta(self):
684         if not self.delta:
685             self.delta = self.to_json()
686             self.save()
687
688         d = json.loads(self.delta)
689         return d
690
691     def clear_delta(self):
692         self.delta = json.dumps(self.to_dict())
693         self.save()
694
695     def set_host(self, host):
696         self.host = host
697         d = json.loads(self.delta)
698         d['host'] = host.labid
699         self.delta = json.dumps(d)
700
701     def set_image(self, image):
702         self.image = image
703         d = json.loads(self.delta)
704         d['image'] = self.image
705         self.delta = json.dumps(d)
706
707     def clear_image(self):
708         self.image = None
709         d = json.loads(self.delta)
710         d.pop("image", None)
711         self.delta = json.dumps(d)
712
713     def set_dashboard_id(self, dash):
714         self.dashboard_id = dash
715         d = json.loads(self.delta)
716         d['dashboard_id'] = self.dashboard_id
717         self.delta = json.dumps(d)
718
719     def save(self, *args, **kwargs):
720         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
721             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
722         super().save(*args, **kwargs)
723
724
725 def get_task(task_id):
726     for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]:
727         try:
728             ret = taskclass.objects.get(task_id=task_id)
729             return ret
730         except taskclass.DoesNotExist:
731             pass
732     from django.core.exceptions import ObjectDoesNotExist
733     raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
734
735
736 def get_task_uuid():
737     return str(uuid.uuid4())
738
739
740 class TaskRelation(models.Model):
741     """
742     Relates a Job to a TaskConfig.
743
744     superclass that relates a Job to tasks anc maintains information
745     like status and messages from the lab
746     """
747
748     status = models.IntegerField(default=JobStatus.NEW)
749     job = models.ForeignKey(Job, on_delete=models.CASCADE)
750     config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
751     task_id = models.CharField(default=get_task_uuid, max_length=37)
752     lab_token = models.CharField(default="null", max_length=50)
753     message = models.TextField(default="")
754
755     job_key = None
756
757     def delete(self, *args, **kwargs):
758         self.config.delete()
759         return super(self.__class__, self).delete(*args, **kwargs)
760
761     def type_str(self):
762         return "Generic Task"
763
764     class Meta:
765         abstract = True
766
767
768 class AccessRelation(TaskRelation):
769     config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
770     job_key = "access"
771
772     def type_str(self):
773         return "Access Task"
774
775     def delete(self, *args, **kwargs):
776         self.config.delete()
777         return super(self.__class__, self).delete(*args, **kwargs)
778
779
780 class SoftwareRelation(TaskRelation):
781     config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
782     job_key = "software"
783
784     def type_str(self):
785         return "Software Configuration Task"
786
787     def delete(self, *args, **kwargs):
788         self.config.delete()
789         return super(self.__class__, self).delete(*args, **kwargs)
790
791
792 class HostHardwareRelation(TaskRelation):
793     resource_id = models.CharField(max_length=200, default="default_id")
794     config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
795     job_key = "hardware"
796
797     def type_str(self):
798         return "Hardware Configuration Task"
799
800     def get_delta(self):
801         return self.config.to_dict()
802
803     def delete(self, *args, **kwargs):
804         self.config.delete()
805         return super(self.__class__, self).delete(*args, **kwargs)
806
807     def save(self, *args, **kwargs):
808         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
809             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
810         super().save(*args, **kwargs)
811
812     def get_resource(self):
813         return ResourceQuery.get(labid=self.resource_id)
814
815
816 class HostNetworkRelation(TaskRelation):
817     resource_id = models.CharField(max_length=200, default="default_id")
818     config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
819     job_key = "network"
820
821     def type_str(self):
822         return "Network Configuration Task"
823
824     def delete(self, *args, **kwargs):
825         self.config.delete()
826         return super(self.__class__, self).delete(*args, **kwargs)
827
828     def save(self, *args, **kwargs):
829         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
830             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
831         super().save(*args, **kwargs)
832
833     def get_resource(self):
834         return ResourceQuery.get(labid=self.resource_id)
835
836
837 class SnapshotRelation(TaskRelation):
838     snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
839     config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
840     job_key = "snapshot"
841
842     def type_str(self):
843         return "Snapshot Task"
844
845     def get_delta(self):
846         return self.config.to_dict()
847
848     def delete(self, *args, **kwargs):
849         self.config.delete()
850         return super(self.__class__, self).delete(*args, **kwargs)
851
852
853 class JobFactory(object):
854     """This class creates all the API models (jobs, tasks, etc) needed to fulfill a booking."""
855
856     @classmethod
857     def reimageHost(cls, new_image, booking, host):
858         """Modify an existing job to reimage the given host."""
859         job = Job.objects.get(booking=booking)
860         # make hardware task new
861         hardware_relation = HostHardwareRelation.objects.get(host=host, job=job)
862         hardware_relation.config.set_image(new_image.lab_id)
863         hardware_relation.config.save()
864         hardware_relation.status = JobStatus.NEW
865
866         # re-apply networking after host is reset
867         net_relation = HostNetworkRelation.objects.get(host=host, job=job)
868         net_relation.status = JobStatus.NEW
869
870         # re-apply ssh access after host is reset
871         for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
872             relation.status = JobStatus.NEW
873             relation.save()
874
875         hardware_relation.save()
876         net_relation.save()
877
878     @classmethod
879     def makeSnapshotTask(cls, image, booking, host):
880         relation = SnapshotRelation()
881         job = Job.objects.get(booking=booking)
882         config = SnapshotConfig.objects.create(dashboard_id=image.id)
883
884         relation.job = job
885         relation.config = config
886         relation.config.save()
887         relation.config = relation.config
888         relation.snapshot = image
889         relation.save()
890
891         config.clear_delta()
892         config.set_host(host)
893         config.save()
894
895     @classmethod
896     def makeCompleteJob(cls, booking):
897         """Create everything that is needed to fulfill the given booking."""
898         resources = booking.resource.get_resources()
899         job = None
900         try:
901             job = Job.objects.get(booking=booking)
902         except Exception:
903             job = Job.objects.create(status=JobStatus.NEW, booking=booking)
904         cls.makeHardwareConfigs(
905             resources=resources,
906             job=job
907         )
908         cls.makeNetworkConfigs(
909             resources=resources,
910             job=job
911         )
912         cls.makeSoftware(
913             booking=booking,
914             job=job
915         )
916         all_users = list(booking.collaborators.all())
917         all_users.append(booking.owner)
918         cls.makeAccessConfig(
919             users=all_users,
920             access_type="vpn",
921             revoke=False,
922             job=job
923         )
924         for user in all_users:
925             try:
926                 cls.makeAccessConfig(
927                     users=[user],
928                     access_type="ssh",
929                     revoke=False,
930                     job=job,
931                     context={
932                         "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
933                         "hosts": [r.labid for r in resources]
934                     }
935                 )
936             except Exception:
937                 continue
938
939     @classmethod
940     def makeHardwareConfigs(cls, resources=[], job=Job()):
941         """
942         Create and save HardwareConfig.
943
944         Helper function to create the tasks related to
945         configuring the hardware
946         """
947         for res in resources:
948             hardware_config = None
949             try:
950                 hardware_config = HardwareConfig.objects.get(relation__host=res)
951             except Exception:
952                 hardware_config = HardwareConfig()
953
954             relation = HostHardwareRelation()
955             relation.resource_id = res.labid
956             relation.job = job
957             relation.config = hardware_config
958             relation.config.save()
959             relation.config = relation.config
960             relation.save()
961
962             hardware_config.set("image", "hostname", "power", "ipmi_create")
963             hardware_config.save()
964
965     @classmethod
966     def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
967         """
968         Create and save AccessConfig.
969
970         Helper function to create the tasks related to
971         configuring the VPN, SSH, etc access for users
972         """
973         for user in users:
974             relation = AccessRelation()
975             relation.job = job
976             config = AccessConfig()
977             config.access_type = access_type
978             config.user = user
979             config.save()
980             relation.config = config
981             relation.save()
982             config.clear_delta()
983             if context:
984                 config.set_context(context)
985             config.set_access_type(access_type)
986             config.set_revoke(revoke)
987             config.set_user(user)
988             config.save()
989
990     @classmethod
991     def makeNetworkConfigs(cls, resources=[], job=Job()):
992         """
993         Create and save NetworkConfig.
994
995         Helper function to create the tasks related to
996         configuring the networking
997         """
998         for res in resources:
999             network_config = None
1000             try:
1001                 network_config = NetworkConfig.objects.get(relation__host=res)
1002             except Exception:
1003                 network_config = NetworkConfig.objects.create()
1004
1005             relation = HostNetworkRelation()
1006             relation.resource_id = res.labid
1007             relation.job = job
1008             network_config.save()
1009             relation.config = network_config
1010             relation.save()
1011             network_config.clear_delta()
1012
1013             # TODO: use get_interfaces() on resource
1014             for interface in res.interfaces.all():
1015                 network_config.add_interface(interface)
1016             network_config.save()
1017
1018     @classmethod
1019     def make_bridge_config(cls, booking):
1020         if len(booking.resource.get_resources()) < 2:
1021             return None
1022         try:
1023             jumphost_config = ResourceOPNFVConfig.objects.filter(
1024                 role__name__iexact="jumphost"
1025             )
1026             jumphost = ResourceQuery.filter(
1027                 bundle=booking.resource,
1028                 config=jumphost_config.resource_config
1029             )[0]
1030         except Exception:
1031             return None
1032         br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
1033         for iface in jumphost.interfaces.all():
1034             br_config.interfaces.add(iface)
1035         return br_config
1036
1037     @classmethod
1038     def makeSoftware(cls, booking=None, job=Job()):
1039         """
1040         Create and save SoftwareConfig.
1041
1042         Helper function to create the tasks related to
1043         configuring the desired software, e.g. an OPNFV deployment
1044         """
1045         if not booking.opnfv_config:
1046             return None
1047
1048         opnfv_api_config = OpnfvApiConfig.objects.create(
1049             opnfv_config=booking.opnfv_config,
1050             installer=booking.opnfv_config.installer.name,
1051             scenario=booking.opnfv_config.scenario.name,
1052             bridge_config=cls.make_bridge_config(booking)
1053         )
1054
1055         opnfv_api_config.set_xdf(booking, False)
1056         opnfv_api_config.save()
1057
1058         for host in booking.resource.get_resources():
1059             opnfv_api_config.roles.add(host)
1060         software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
1061         software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
1062         return software_relation
1063
1064
1065 JOB_TASK_CLASSLIST = [
1066     HostHardwareRelation,
1067     AccessRelation,
1068     HostNetworkRelation,
1069     SoftwareRelation,
1070     SnapshotRelation
1071 ]
1072
1073
1074 class JobTaskQuery(AbstractModelQuery):
1075     model_list = JOB_TASK_CLASSLIST