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