Merge "Test resource templates now use the same lab as the image generated alongside...
[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 = self.interfaces.first().host.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.host.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.host.labid
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.host.labid
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
813 class HostNetworkRelation(TaskRelation):
814     resource_id = models.CharField(max_length=200, default="default_id")
815     config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
816     job_key = "network"
817
818     def type_str(self):
819         return "Network Configuration Task"
820
821     def delete(self, *args, **kwargs):
822         self.config.delete()
823         return super(self.__class__, self).delete(*args, **kwargs)
824
825     def save(self, *args, **kwargs):
826         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
827             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
828         super().save(*args, **kwargs)
829
830
831 class SnapshotRelation(TaskRelation):
832     snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
833     config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
834     job_key = "snapshot"
835
836     def type_str(self):
837         return "Snapshot Task"
838
839     def get_delta(self):
840         return self.config.to_dict()
841
842     def delete(self, *args, **kwargs):
843         self.config.delete()
844         return super(self.__class__, self).delete(*args, **kwargs)
845
846
847 class JobFactory(object):
848     """This class creates all the API models (jobs, tasks, etc) needed to fulfill a booking."""
849
850     @classmethod
851     def reimageHost(cls, new_image, booking, host):
852         """Modify an existing job to reimage the given host."""
853         job = Job.objects.get(booking=booking)
854         # make hardware task new
855         hardware_relation = HostHardwareRelation.objects.get(host=host, job=job)
856         hardware_relation.config.set_image(new_image.lab_id)
857         hardware_relation.config.save()
858         hardware_relation.status = JobStatus.NEW
859
860         # re-apply networking after host is reset
861         net_relation = HostNetworkRelation.objects.get(host=host, job=job)
862         net_relation.status = JobStatus.NEW
863
864         # re-apply ssh access after host is reset
865         for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
866             relation.status = JobStatus.NEW
867             relation.save()
868
869         hardware_relation.save()
870         net_relation.save()
871
872     @classmethod
873     def makeSnapshotTask(cls, image, booking, host):
874         relation = SnapshotRelation()
875         job = Job.objects.get(booking=booking)
876         config = SnapshotConfig.objects.create(dashboard_id=image.id)
877
878         relation.job = job
879         relation.config = config
880         relation.config.save()
881         relation.config = relation.config
882         relation.snapshot = image
883         relation.save()
884
885         config.clear_delta()
886         config.set_host(host)
887         config.save()
888
889     @classmethod
890     def makeCompleteJob(cls, booking):
891         """Create everything that is needed to fulfill the given booking."""
892         resources = booking.resource.get_resources()
893         job = None
894         try:
895             job = Job.objects.get(booking=booking)
896         except Exception:
897             job = Job.objects.create(status=JobStatus.NEW, booking=booking)
898         cls.makeHardwareConfigs(
899             resources=resources,
900             job=job
901         )
902         cls.makeNetworkConfigs(
903             resources=resources,
904             job=job
905         )
906         cls.makeSoftware(
907             booking=booking,
908             job=job
909         )
910         all_users = list(booking.collaborators.all())
911         all_users.append(booking.owner)
912         cls.makeAccessConfig(
913             users=all_users,
914             access_type="vpn",
915             revoke=False,
916             job=job
917         )
918         for user in all_users:
919             try:
920                 cls.makeAccessConfig(
921                     users=[user],
922                     access_type="ssh",
923                     revoke=False,
924                     job=job,
925                     context={
926                         "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
927                         "hosts": [r.labid for r in resources]
928                     }
929                 )
930             except Exception:
931                 continue
932
933     @classmethod
934     def makeHardwareConfigs(cls, resources=[], job=Job()):
935         """
936         Create and save HardwareConfig.
937
938         Helper function to create the tasks related to
939         configuring the hardware
940         """
941         for res in resources:
942             hardware_config = None
943             try:
944                 hardware_config = HardwareConfig.objects.get(relation__host=res)
945             except Exception:
946                 hardware_config = HardwareConfig()
947
948             relation = HostHardwareRelation()
949             relation.resource_id = res.labid
950             relation.job = job
951             relation.config = hardware_config
952             relation.config.save()
953             relation.config = relation.config
954             relation.save()
955
956             hardware_config.set("image", "hostname", "power", "ipmi_create")
957             hardware_config.save()
958
959     @classmethod
960     def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
961         """
962         Create and save AccessConfig.
963
964         Helper function to create the tasks related to
965         configuring the VPN, SSH, etc access for users
966         """
967         for user in users:
968             relation = AccessRelation()
969             relation.job = job
970             config = AccessConfig()
971             config.access_type = access_type
972             config.user = user
973             config.save()
974             relation.config = config
975             relation.save()
976             config.clear_delta()
977             if context:
978                 config.set_context(context)
979             config.set_access_type(access_type)
980             config.set_revoke(revoke)
981             config.set_user(user)
982             config.save()
983
984     @classmethod
985     def makeNetworkConfigs(cls, resources=[], job=Job()):
986         """
987         Create and save NetworkConfig.
988
989         Helper function to create the tasks related to
990         configuring the networking
991         """
992         for res in resources:
993             network_config = None
994             try:
995                 network_config = NetworkConfig.objects.get(relation__host=res)
996             except Exception:
997                 network_config = NetworkConfig.objects.create()
998
999             relation = HostNetworkRelation()
1000             relation.resource_id = res.labid
1001             relation.job = job
1002             network_config.save()
1003             relation.config = network_config
1004             relation.save()
1005             network_config.clear_delta()
1006
1007             # TODO: use get_interfaces() on resource
1008             for interface in res.interfaces.all():
1009                 network_config.add_interface(interface)
1010             network_config.save()
1011
1012     @classmethod
1013     def make_bridge_config(cls, booking):
1014         if booking.resource.hosts.count() < 2:
1015             return None
1016         try:
1017             jumphost_config = ResourceOPNFVConfig.objects.filter(
1018                 role__name__iexact="jumphost"
1019             )
1020             jumphost = ResourceQuery.filter(
1021                 bundle=booking.resource,
1022                 config=jumphost_config.resource_config
1023             )[0]
1024         except Exception:
1025             return None
1026         br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
1027         for iface in jumphost.interfaces.all():
1028             br_config.interfaces.add(iface)
1029         return br_config
1030
1031     @classmethod
1032     def makeSoftware(cls, booking=None, job=Job()):
1033         """
1034         Create and save SoftwareConfig.
1035
1036         Helper function to create the tasks related to
1037         configuring the desired software, e.g. an OPNFV deployment
1038         """
1039         if not booking.opnfv_config:
1040             return None
1041
1042         opnfv_api_config = OpnfvApiConfig.objects.create(
1043             opnfv_config=booking.opnfv_config,
1044             installer=booking.opnfv_config.installer.name,
1045             scenario=booking.opnfv_config.scenario.name,
1046             bridge_config=cls.make_bridge_config(booking)
1047         )
1048
1049         opnfv_api_config.set_xdf(booking, False)
1050         opnfv_api_config.save()
1051
1052         for host in booking.resource.hosts.all():
1053             opnfv_api_config.roles.add(host)
1054         software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
1055         software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
1056         return software_relation
1057
1058
1059 JOB_TASK_CLASSLIST = [
1060     HostHardwareRelation,
1061     AccessRelation,
1062     HostNetworkRelation,
1063     SoftwareRelation,
1064     SnapshotRelation
1065 ]
1066
1067
1068 class JobTaskQuery(AbstractModelQuery):
1069     model_list = JOB_TASK_CLASSLIST