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