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