Merge "Remove Configure Pod"
[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.CLEAN)
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             for vlan in interface.config.all():
653                 # TODO: should this come from the interface?
654                 # e.g. will different interfaces for different resources need different configs?
655                 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
656
657         return d
658
659     def to_json(self):
660         return json.dumps(self.to_dict())
661
662     def get_delta(self):
663         if not self.delta:
664             self.delta = self.to_json()
665             self.save()
666         d = json.loads(self.delta)
667         d['lab_token'] = self.hostnetworkrelation.lab_token
668         return d
669
670     def clear_delta(self):
671         self.delta = json.dumps(self.to_dict())
672         self.save()
673
674     def add_interface(self, interface):
675         self.interfaces.add(interface)
676         d = json.loads(self.delta)
677         hid = self.hostnetworkrelation.resource_id
678         if hid not in d:
679             d[hid] = {}
680         d[hid][interface.mac_address] = []
681         for vlan in interface.config.all():
682             d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
683         self.delta = json.dumps(d)
684
685
686 class SnapshotConfig(TaskConfig):
687
688     resource_id = models.CharField(max_length=200, default="default_id")
689     image = models.IntegerField(null=True)
690     dashboard_id = models.IntegerField()
691     delta = models.TextField(default="{}")
692
693     def to_dict(self):
694         d = {}
695         if self.host:
696             d['host'] = self.host.labid
697         if self.image:
698             d['image'] = self.image
699         d['dashboard_id'] = self.dashboard_id
700         return d
701
702     def to_json(self):
703         return json.dumps(self.to_dict())
704
705     def get_delta(self):
706         if not self.delta:
707             self.delta = self.to_json()
708             self.save()
709
710         d = json.loads(self.delta)
711         return d
712
713     def clear_delta(self):
714         self.delta = json.dumps(self.to_dict())
715         self.save()
716
717     def set_host(self, host):
718         self.host = host
719         d = json.loads(self.delta)
720         d['host'] = host.labid
721         self.delta = json.dumps(d)
722
723     def set_image(self, image):
724         self.image = image
725         d = json.loads(self.delta)
726         d['image'] = self.image
727         self.delta = json.dumps(d)
728
729     def clear_image(self):
730         self.image = None
731         d = json.loads(self.delta)
732         d.pop("image", None)
733         self.delta = json.dumps(d)
734
735     def set_dashboard_id(self, dash):
736         self.dashboard_id = dash
737         d = json.loads(self.delta)
738         d['dashboard_id'] = self.dashboard_id
739         self.delta = json.dumps(d)
740
741     def save(self, *args, **kwargs):
742         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
743             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
744         super().save(*args, **kwargs)
745
746
747 def get_task(task_id):
748     for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]:
749         try:
750             ret = taskclass.objects.get(task_id=task_id)
751             return ret
752         except taskclass.DoesNotExist:
753             pass
754     from django.core.exceptions import ObjectDoesNotExist
755     raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
756
757
758 def get_task_uuid():
759     return str(uuid.uuid4())
760
761
762 class TaskRelation(models.Model):
763     """
764     Relates a Job to a TaskConfig.
765
766     superclass that relates a Job to tasks anc maintains information
767     like status and messages from the lab
768     """
769
770     status = models.IntegerField(default=JobStatus.NEW)
771     job = models.ForeignKey(Job, on_delete=models.CASCADE)
772     config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
773     task_id = models.CharField(default=get_task_uuid, max_length=37)
774     lab_token = models.CharField(default="null", max_length=50)
775     message = models.TextField(default="")
776
777     job_key = None
778
779     def delete(self, *args, **kwargs):
780         self.config.delete()
781         return super(self.__class__, self).delete(*args, **kwargs)
782
783     def type_str(self):
784         return "Generic Task"
785
786     class Meta:
787         abstract = True
788
789
790 class AccessRelation(TaskRelation):
791     config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
792     job_key = "access"
793
794     def type_str(self):
795         return "Access Task"
796
797     def delete(self, *args, **kwargs):
798         self.config.delete()
799         return super(self.__class__, self).delete(*args, **kwargs)
800
801
802 class SoftwareRelation(TaskRelation):
803     config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
804     job_key = "software"
805
806     def type_str(self):
807         return "Software Configuration Task"
808
809     def delete(self, *args, **kwargs):
810         self.config.delete()
811         return super(self.__class__, self).delete(*args, **kwargs)
812
813
814 class HostHardwareRelation(TaskRelation):
815     resource_id = models.CharField(max_length=200, default="default_id")
816     config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
817     job_key = "hardware"
818
819     def type_str(self):
820         return "Hardware Configuration Task"
821
822     def get_delta(self):
823         return self.config.to_dict()
824
825     def delete(self, *args, **kwargs):
826         self.config.delete()
827         return super(self.__class__, self).delete(*args, **kwargs)
828
829     def save(self, *args, **kwargs):
830         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
831             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
832         super().save(*args, **kwargs)
833
834     def get_resource(self):
835         return ResourceQuery.get(labid=self.resource_id)
836
837
838 class HostNetworkRelation(TaskRelation):
839     resource_id = models.CharField(max_length=200, default="default_id")
840     config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
841     job_key = "network"
842
843     def type_str(self):
844         return "Network Configuration Task"
845
846     def delete(self, *args, **kwargs):
847         self.config.delete()
848         return super(self.__class__, self).delete(*args, **kwargs)
849
850     def save(self, *args, **kwargs):
851         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
852             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
853         super().save(*args, **kwargs)
854
855     def get_resource(self):
856         return ResourceQuery.get(labid=self.resource_id)
857
858
859 class SnapshotRelation(TaskRelation):
860     snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
861     config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
862     job_key = "snapshot"
863
864     def type_str(self):
865         return "Snapshot Task"
866
867     def get_delta(self):
868         return self.config.to_dict()
869
870     def delete(self, *args, **kwargs):
871         self.config.delete()
872         return super(self.__class__, self).delete(*args, **kwargs)
873
874
875 class JobFactory(object):
876     """This class creates all the API models (jobs, tasks, etc) needed to fulfill a booking."""
877
878     @classmethod
879     def reimageHost(cls, new_image, booking, host):
880         """Modify an existing job to reimage the given host."""
881         job = Job.objects.get(booking=booking)
882         # make hardware task new
883         hardware_relation = HostHardwareRelation.objects.get(host=host, job=job)
884         hardware_relation.config.set_image(new_image.lab_id)
885         hardware_relation.config.save()
886         hardware_relation.status = JobStatus.NEW
887
888         # re-apply networking after host is reset
889         net_relation = HostNetworkRelation.objects.get(host=host, job=job)
890         net_relation.status = JobStatus.NEW
891
892         # re-apply ssh access after host is reset
893         for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
894             relation.status = JobStatus.NEW
895             relation.save()
896
897         hardware_relation.save()
898         net_relation.save()
899
900     @classmethod
901     def makeSnapshotTask(cls, image, booking, host):
902         relation = SnapshotRelation()
903         job = Job.objects.get(booking=booking)
904         config = SnapshotConfig.objects.create(dashboard_id=image.id)
905
906         relation.job = job
907         relation.config = config
908         relation.config.save()
909         relation.config = relation.config
910         relation.snapshot = image
911         relation.save()
912
913         config.clear_delta()
914         config.set_host(host)
915         config.save()
916
917     @classmethod
918     def makeCompleteJob(cls, booking):
919         """Create everything that is needed to fulfill the given booking."""
920         resources = booking.resource.get_resources()
921         job = None
922         try:
923             job = Job.objects.get(booking=booking)
924         except Exception:
925             job = Job.objects.create(status=JobStatus.NEW, booking=booking)
926         cls.makeHardwareConfigs(
927             resources=resources,
928             job=job
929         )
930         cls.makeNetworkConfigs(
931             resources=resources,
932             job=job
933         )
934         cls.makeSoftware(
935             booking=booking,
936             job=job
937         )
938         all_users = list(booking.collaborators.all())
939         all_users.append(booking.owner)
940         cls.makeAccessConfig(
941             users=all_users,
942             access_type="vpn",
943             revoke=False,
944             job=job
945         )
946         for user in all_users:
947             try:
948                 cls.makeAccessConfig(
949                     users=[user],
950                     access_type="ssh",
951                     revoke=False,
952                     job=job,
953                     context={
954                         "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
955                         "hosts": [r.labid for r in resources]
956                     }
957                 )
958             except Exception:
959                 continue
960
961     @classmethod
962     def makeHardwareConfigs(cls, resources=[], job=Job()):
963         """
964         Create and save HardwareConfig.
965
966         Helper function to create the tasks related to
967         configuring the hardware
968         """
969         for res in resources:
970             hardware_config = None
971             try:
972                 hardware_config = HardwareConfig.objects.get(relation__resource_id=res.labid)
973             except Exception:
974                 hardware_config = HardwareConfig()
975
976             relation = HostHardwareRelation()
977             relation.resource_id = res.labid
978             relation.job = job
979             relation.config = hardware_config
980             relation.config.save()
981             relation.config = relation.config
982             relation.save()
983
984             hardware_config.set("id", "image", "hostname", "power", "ipmi_create")
985             hardware_config.save()
986
987     @classmethod
988     def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
989         """
990         Create and save AccessConfig.
991
992         Helper function to create the tasks related to
993         configuring the VPN, SSH, etc access for users
994         """
995         for user in users:
996             relation = AccessRelation()
997             relation.job = job
998             config = AccessConfig()
999             config.access_type = access_type
1000             config.user = user
1001             config.save()
1002             relation.config = config
1003             relation.save()
1004             config.clear_delta()
1005             if context:
1006                 config.set_context(context)
1007             config.set_access_type(access_type)
1008             config.set_revoke(revoke)
1009             config.set_user(user)
1010             config.save()
1011
1012     @classmethod
1013     def makeNetworkConfigs(cls, resources=[], job=Job()):
1014         """
1015         Create and save NetworkConfig.
1016
1017         Helper function to create the tasks related to
1018         configuring the networking
1019         """
1020         for res in resources:
1021             network_config = None
1022             try:
1023                 network_config = NetworkConfig.objects.get(relation__host=res)
1024             except Exception:
1025                 network_config = NetworkConfig.objects.create()
1026
1027             relation = HostNetworkRelation()
1028             relation.resource_id = res.labid
1029             relation.job = job
1030             network_config.save()
1031             relation.config = network_config
1032             relation.save()
1033             network_config.clear_delta()
1034
1035             # TODO: use get_interfaces() on resource
1036             for interface in res.interfaces.all():
1037                 network_config.add_interface(interface)
1038             network_config.save()
1039
1040     @classmethod
1041     def make_bridge_config(cls, booking):
1042         if len(booking.resource.get_resources()) < 2:
1043             return None
1044         try:
1045             jumphost_config = ResourceOPNFVConfig.objects.filter(
1046                 role__name__iexact="jumphost"
1047             )
1048             jumphost = ResourceQuery.filter(
1049                 bundle=booking.resource,
1050                 config=jumphost_config.resource_config
1051             )[0]
1052         except Exception:
1053             return None
1054         br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
1055         for iface in jumphost.interfaces.all():
1056             br_config.interfaces.add(iface)
1057         return br_config
1058
1059     @classmethod
1060     def makeSoftware(cls, booking=None, job=Job()):
1061         """
1062         Create and save SoftwareConfig.
1063
1064         Helper function to create the tasks related to
1065         configuring the desired software, e.g. an OPNFV deployment
1066         """
1067         if not booking.opnfv_config:
1068             return None
1069
1070         opnfv_api_config = OpnfvApiConfig.objects.create(
1071             opnfv_config=booking.opnfv_config,
1072             installer=booking.opnfv_config.installer.name,
1073             scenario=booking.opnfv_config.scenario.name,
1074             bridge_config=cls.make_bridge_config(booking)
1075         )
1076
1077         opnfv_api_config.set_xdf(booking, False)
1078         opnfv_api_config.save()
1079
1080         for host in booking.resource.get_resources():
1081             opnfv_api_config.roles.add(host)
1082         software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
1083         software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
1084         return software_relation
1085
1086
1087 JOB_TASK_CLASSLIST = [
1088     HostHardwareRelation,
1089     AccessRelation,
1090     HostNetworkRelation,
1091     SoftwareRelation,
1092     SnapshotRelation
1093 ]
1094
1095
1096 class JobTaskQuery(AbstractModelQuery):
1097     model_list = JOB_TASK_CLASSLIST