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