Merge User Booking API Rev 1 (Try 3)
[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.contrib.postgres.fields import JSONField
16 from django.http import HttpResponseNotFound
17 from django.urls import reverse
18 from django.utils import timezone
19
20 import json
21 import uuid
22
23 from booking.models import Booking
24 from resource_inventory.models import (
25     Lab,
26     ResourceProfile,
27     Image,
28     Interface,
29     ResourceOPNFVConfig,
30     RemoteInfo,
31     OPNFVConfig,
32     ConfigState,
33     ResourceQuery
34 )
35 from resource_inventory.idf_templater import IDFTemplater
36 from resource_inventory.pdf_templater import PDFTemplater
37 from account.models import Downtime, UserProfile
38 from dashboard.utils import AbstractModelQuery
39
40
41 class JobStatus:
42     """
43     A poor man's enum for a job's status.
44
45     A job is NEW if it has not been started or recognized by the Lab
46     A job is CURRENT if it has been started by the lab but it is not yet completed
47     a job is DONE if all the tasks are complete and the booking is ready to use
48     """
49
50     NEW = 0
51     CURRENT = 100
52     DONE = 200
53     ERROR = 300
54
55
56 class LabManagerTracker:
57
58     @classmethod
59     def get(cls, lab_name, token):
60         """
61         Get a LabManager.
62
63         Takes in a lab name (from a url path)
64         returns a lab manager instance for that lab, if it exists
65         Also checks that the given API token is correct
66         """
67         try:
68             lab = Lab.objects.get(name=lab_name)
69         except Exception:
70             raise PermissionDenied("Lab not found")
71         if lab.api_token == token:
72             return LabManager(lab)
73         raise PermissionDenied("Lab not authorized")
74
75
76 class LabManager:
77     """
78     Handles all lab REST calls.
79
80     handles jobs, inventory, status, etc
81     may need to create helper classes
82     """
83
84     def __init__(self, lab):
85         self.lab = lab
86
87     def get_downtime(self):
88         return Downtime.objects.filter(start__lt=timezone.now(), end__gt=timezone.now(), lab=self.lab)
89
90     def get_downtime_json(self):
91         downtime = self.get_downtime().first()  # should only be one item in queryset
92         if downtime:
93             return {
94                 "is_down": True,
95                 "start": downtime.start,
96                 "end": downtime.end,
97                 "description": downtime.description
98             }
99         return {"is_down": False}
100
101     def create_downtime(self, form):
102         """
103         Create a downtime event.
104
105         Takes in a dictionary that describes the model.
106         {
107           "start": utc timestamp
108           "end": utc timestamp
109           "description": human text (optional)
110         }
111         For timestamp structure, https://docs.djangoproject.com/en/2.2/ref/forms/fields/#datetimefield
112         """
113         Downtime.objects.create(
114             start=form.cleaned_data['start'],
115             end=form.cleaned_data['end'],
116             description=form.cleaned_data['description'],
117             lab=self.lab
118         )
119         return self.get_downtime_json()
120
121     def update_host_remote_info(self, data, res_id):
122         resource = ResourceQuery.filter(labid=res_id, lab=self.lab)
123         if len(resource) != 1:
124             return HttpResponseNotFound("Could not find single host with id " + str(res_id))
125         resource = resource[0]
126         info = {}
127         try:
128             info['address'] = data['address']
129             info['mac_address'] = data['mac_address']
130             info['password'] = data['password']
131             info['user'] = data['user']
132             info['type'] = data['type']
133             info['versions'] = json.dumps(data['versions'])
134         except Exception as e:
135             return {"error": "invalid arguement: " + str(e)}
136         remote_info = resource.remote_management
137         if "default" in remote_info.mac_address:
138             remote_info = RemoteInfo()
139         remote_info.address = info['address']
140         remote_info.mac_address = info['mac_address']
141         remote_info.password = info['password']
142         remote_info.user = info['user']
143         remote_info.type = info['type']
144         remote_info.versions = info['versions']
145         remote_info.save()
146         resource.remote_management = remote_info
147         resource.save()
148         booking = Booking.objects.get(resource=resource.bundle)
149         self.update_xdf(booking)
150         return {"status": "success"}
151
152     def update_xdf(self, booking):
153         booking.pdf = PDFTemplater.makePDF(booking)
154         booking.idf = IDFTemplater().makeIDF(booking)
155         booking.save()
156
157     def get_pdf(self, booking_id):
158         booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
159         return booking.pdf
160
161     def get_idf(self, booking_id):
162         booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
163         return booking.idf
164
165     def get_profile(self):
166         prof = {}
167         prof['name'] = self.lab.name
168         prof['contact'] = {
169             "phone": self.lab.contact_phone,
170             "email": self.lab.contact_email
171         }
172         prof['host_count'] = [{
173             "type": profile.name,
174             "count": len(profile.get_resources(lab=self.lab))}
175             for profile in ResourceProfile.objects.filter(labs=self.lab)]
176         return prof
177
178     def format_user(self, userprofile):
179         return {
180             "id": userprofile.user.id,
181             "username": userprofile.user.username,
182             "email": userprofile.email_addr,
183             "first_name": userprofile.user.first_name,
184             "last_name": userprofile.user.last_name,
185             "company": userprofile.company
186         }
187
188     def get_users(self):
189         userlist = [self.format_user(profile) for profile in UserProfile.objects.select_related("user").all()]
190
191         return json.dumps({"users": userlist})
192
193     def get_user(self, user_id):
194         user = User.objects.get(pk=user_id)
195
196         profile = get_object_or_404(UserProfile, user=user)
197
198         return json.dumps(self.format_user(profile))
199
200     def get_inventory(self):
201         inventory = {}
202         resources = ResourceQuery.filter(lab=self.lab)
203         images = Image.objects.filter(from_lab=self.lab)
204         profiles = ResourceProfile.objects.filter(labs=self.lab)
205         inventory['resources'] = self.serialize_resources(resources)
206         inventory['images'] = self.serialize_images(images)
207         inventory['host_types'] = self.serialize_host_profiles(profiles)
208         return inventory
209
210     def get_host(self, hostname):
211         resource = ResourceQuery.filter(labid=hostname, lab=self.lab)
212         if len(resource) != 1:
213             return HttpResponseNotFound("Could not find single host with id " + str(hostname))
214         resource = resource[0]
215         return {
216             "booked": resource.booked,
217             "working": resource.working,
218             "type": resource.profile.name
219         }
220
221     def update_host(self, hostname, data):
222         resource = ResourceQuery.filter(labid=hostname, lab=self.lab)
223         if len(resource) != 1:
224             return HttpResponseNotFound("Could not find single host with id " + str(hostname))
225         resource = resource[0]
226         if "working" in data:
227             working = data['working'] == "true"
228             resource.working = working
229         resource.save()
230         return self.get_host(hostname)
231
232     def get_status(self):
233         return {"status": self.lab.status}
234
235     def set_status(self, payload):
236         {}
237
238     def get_current_jobs(self):
239         jobs = Job.objects.filter(booking__lab=self.lab)
240
241         return self.serialize_jobs(jobs, status=JobStatus.CURRENT)
242
243     def get_new_jobs(self):
244         jobs = Job.objects.filter(booking__lab=self.lab)
245
246         return self.serialize_jobs(jobs, status=JobStatus.NEW)
247
248     def get_done_jobs(self):
249         jobs = Job.objects.filter(booking__lab=self.lab)
250
251         return self.serialize_jobs(jobs, status=JobStatus.DONE)
252
253     def get_analytics_job(self):
254         """ Get analytics job with status new """
255         jobs = Job.objects.filter(
256             booking__lab=self.lab,
257             job_type='DATA'
258         )
259
260         return self.serialize_jobs(jobs, status=JobStatus.NEW)
261
262     def get_job(self, jobid):
263         return Job.objects.get(pk=jobid).to_dict()
264
265     def update_job(self, jobid, data):
266         {}
267
268     def serialize_jobs(self, jobs, status=JobStatus.NEW):
269         job_ser = []
270         for job in jobs:
271             jsonized_job = job.get_delta(status)
272             if len(jsonized_job['payload']) < 1:
273                 continue
274             job_ser.append(jsonized_job)
275
276         return job_ser
277
278     def serialize_resources(self, resources):
279         # TODO: rewrite for Resource model
280         host_ser = []
281         for res in resources:
282             r = {
283                 'interfaces': [],
284                 'hostname': res.name,
285                 'host_type': res.profile.name
286             }
287             for iface in res.get_interfaces():
288                 r['interfaces'].append({
289                     'mac': iface.mac_address,
290                     'busaddr': iface.bus_address,
291                     'name': iface.name,
292                     'switchport': {"switch_name": iface.switch_name, "port_name": iface.port_name}
293                 })
294         return host_ser
295
296     def serialize_images(self, images):
297         images_ser = []
298         for image in images:
299             images_ser.append(
300                 {
301                     "name": image.name,
302                     "lab_id": image.lab_id,
303                     "dashboard_id": image.id
304                 }
305             )
306         return images_ser
307
308     def serialize_resource_profiles(self, profiles):
309         profile_ser = []
310         for profile in profiles:
311             p = {}
312             p['cpu'] = {
313                 "cores": profile.cpuprofile.first().cores,
314                 "arch": profile.cpuprofile.first().architecture,
315                 "cpus": profile.cpuprofile.first().cpus,
316             }
317             p['disks'] = []
318             for disk in profile.storageprofile.all():
319                 d = {
320                     "size": disk.size,
321                     "type": disk.media_type,
322                     "name": disk.name
323                 }
324                 p['disks'].append(d)
325             p['description'] = profile.description
326             p['interfaces'] = []
327             for iface in profile.interfaceprofile.all():
328                 p['interfaces'].append(
329                     {
330                         "speed": iface.speed,
331                         "name": iface.name
332                     }
333                 )
334
335             p['ram'] = {"amount": profile.ramprofile.first().amount}
336             p['name'] = profile.name
337             profile_ser.append(p)
338         return profile_ser
339
340
341 class APILog(models.Model):
342     user = models.ForeignKey(User, on_delete=models.PROTECT)
343     call_time = models.DateTimeField(auto_now=True)
344     method = models.CharField(null=True, max_length=6)
345     endpoint = models.CharField(null=True, max_length=300)
346     ip_addr = models.GenericIPAddressField(protocol="both", null=True, unpack_ipv4=False)
347     body = JSONField(null=True)
348
349     def __str__(self):
350         return "Call to {} at {} by {}".format(
351             self.endpoint,
352             self.call_time,
353             self.user.username
354         )
355
356
357 class AutomationAPIManager:
358     @staticmethod
359     def serialize_booking(booking):
360         sbook = {}
361         sbook['id'] = booking.pk
362         sbook['owner'] = booking.owner.username
363         sbook['collaborators'] = [user.username for user in booking.collaborators.all()]
364         sbook['start'] = booking.start
365         sbook['end'] = booking.end
366         sbook['lab'] = AutomationAPIManager.serialize_lab(booking.lab)
367         sbook['purpose'] = booking.purpose
368         sbook['resourceBundle'] = AutomationAPIManager.serialize_bundle(booking.resource)
369         return sbook
370
371     @staticmethod
372     def serialize_lab(lab):
373         slab = {}
374         slab['id'] = lab.pk
375         slab['name'] = lab.name
376         return slab
377
378     @staticmethod
379     def serialize_bundle(bundle):
380         sbundle = {}
381         sbundle['id'] = bundle.pk
382         sbundle['resources'] = [
383             AutomationAPIManager.serialize_server(server)
384             for server in bundle.get_resources()]
385         return sbundle
386
387     @staticmethod
388     def serialize_server(server):
389         sserver = {}
390         sserver['id'] = server.pk
391         sserver['name'] = server.name
392         return sserver
393
394     @staticmethod
395     def serialize_resource_profile(profile):
396         sprofile = {}
397         sprofile['id'] = profile.pk
398         sprofile['name'] = profile.name
399         return sprofile
400
401     @staticmethod
402     def serialize_template(rec_temp_and_count):
403         template = rec_temp_and_count[0]
404         count = rec_temp_and_count[1]
405
406         stemplate = {}
407         stemplate['id'] = template.pk
408         stemplate['name'] = template.name
409         stemplate['count_available'] = count
410         stemplate['resourceProfiles'] = [
411             AutomationAPIManager.serialize_resource_profile(config.profile)
412             for config in template.getConfigs()
413         ]
414         return stemplate
415
416     @staticmethod
417     def serialize_image(image):
418         simage = {}
419         simage['id'] = image.pk
420         simage['name'] = image.name
421         return simage
422
423     @staticmethod
424     def serialize_userprofile(up):
425         sup = {}
426         sup['id'] = up.pk
427         sup['username'] = up.user.username
428         return sup
429
430
431 class Job(models.Model):
432     """
433     A Job to be performed by the Lab.
434
435     The API uses Jobs and Tasks to communicate actions that need to be taken to the Lab
436     that is hosting a booking. A booking from a user has an associated Job which tells
437     the lab how to configure the hardware, networking, etc to fulfill the booking
438     for the user.
439     This is the class that is serialized and put into the api
440     """
441
442     JOB_TYPES = (
443         ('BOOK', 'Booking'),
444         ('DATA', 'Analytics')
445     )
446
447     booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
448     status = models.IntegerField(default=JobStatus.NEW)
449     complete = models.BooleanField(default=False)
450     job_type = models.CharField(
451         max_length=4,
452         choices=JOB_TYPES,
453         default='BOOK'
454     )
455
456     def to_dict(self):
457         d = {}
458         for relation in self.get_tasklist():
459             if relation.job_key not in d:
460                 d[relation.job_key] = {}
461             d[relation.job_key][relation.task_id] = relation.config.to_dict()
462
463         return {"id": self.id, "payload": d}
464
465     def get_tasklist(self, status="all"):
466         if status != "all":
467             return JobTaskQuery.filter(job=self, status=status)
468         return JobTaskQuery.filter(job=self)
469
470     def is_fulfilled(self):
471         """
472         If a job has been completed by the lab.
473
474         This method should return true if all of the job's tasks are done,
475         and false otherwise
476         """
477         my_tasks = self.get_tasklist()
478         for task in my_tasks:
479             if task.status != JobStatus.DONE:
480                 return False
481         return True
482
483     def get_delta(self, status):
484         d = {}
485         for relation in self.get_tasklist(status=status):
486             if relation.job_key not in d:
487                 d[relation.job_key] = {}
488             d[relation.job_key][relation.task_id] = relation.config.get_delta()
489
490         return {"id": self.id, "payload": d}
491
492     def to_json(self):
493         return json.dumps(self.to_dict())
494
495
496 class TaskConfig(models.Model):
497     state = models.IntegerField(default=ConfigState.NEW)
498
499     keys = set()  # TODO: This needs to be an instance variable, not a class variable
500     delta_keys_list = models.CharField(max_length=200, default="[]")
501
502     @property
503     def delta_keys(self):
504         return list(set(json.loads(self.delta_keys_list)))
505
506     @delta_keys.setter
507     def delta_keys(self, keylist):
508         self.delta_keys_list = json.dumps(keylist)
509
510     def to_dict(self):
511         raise NotImplementedError
512
513     def get_delta(self):
514         raise NotImplementedError
515
516     def format_delta(self, config, token):
517         delta = {k: config[k] for k in self.delta_keys}
518         delta['lab_token'] = token
519         return delta
520
521     def to_json(self):
522         return json.dumps(self.to_dict())
523
524     def clear_delta(self):
525         self.delta_keys = []
526
527     def set(self, *args):
528         dkeys = self.delta_keys
529         for arg in args:
530             if arg in self.keys:
531                 dkeys.append(arg)
532         self.delta_keys = dkeys
533
534
535 class BridgeConfig(models.Model):
536     """Displays mapping between jumphost interfaces and bridges."""
537
538     interfaces = models.ManyToManyField(Interface)
539     opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
540
541     def to_dict(self):
542         d = {}
543         hid = ResourceQuery.get(interface__pk=self.interfaces.first().pk).labid
544         d[hid] = {}
545         for interface in self.interfaces.all():
546             d[hid][interface.mac_address] = []
547             for vlan in interface.config.all():
548                 network_role = self.opnfv_model.networks().filter(network=vlan.network)
549                 bridge = IDFTemplater.bridge_names[network_role.name]
550                 br_config = {
551                     "vlan_id": vlan.vlan_id,
552                     "tagged": vlan.tagged,
553                     "bridge": bridge
554                 }
555                 d[hid][interface.mac_address].append(br_config)
556         return d
557
558     def to_json(self):
559         return json.dumps(self.to_dict())
560
561
562 class ActiveUsersConfig(models.Model):
563     """
564     Task for getting active VPN users
565
566     StackStorm needs no information to run this job
567     so this task is very bare, but neccessary to fit
568     job creation convention.
569     """
570
571     def clear_delta(self):
572         self.delta = '{}'
573
574     def get_delta(self):
575         return json.loads(self.to_json())
576
577     def to_json(self):
578         return json.dumps(self.to_dict())
579
580     def to_dict(self):
581         return {}
582
583
584 class OpnfvApiConfig(models.Model):
585
586     installer = models.CharField(max_length=200)
587     scenario = models.CharField(max_length=300)
588     roles = models.ManyToManyField(ResourceOPNFVConfig)
589     # pdf and idf are url endpoints, not the actual file
590     pdf = models.CharField(max_length=100)
591     idf = models.CharField(max_length=100)
592     bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True)
593     delta = models.TextField()
594     opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL)
595
596     def to_dict(self):
597         d = {}
598         if not self.opnfv_config:
599             return d
600         if self.installer:
601             d['installer'] = self.installer
602         if self.scenario:
603             d['scenario'] = self.scenario
604         if self.pdf:
605             d['pdf'] = self.pdf
606         if self.idf:
607             d['idf'] = self.idf
608         if self.bridge_config:
609             d['bridged_interfaces'] = self.bridge_config.to_dict()
610
611         hosts = self.roles.all()
612         if hosts.exists():
613             d['roles'] = []
614             for host in hosts:
615                 d['roles'].append({
616                     host.labid: self.opnfv_config.host_opnfv_config.get(
617                         host_config__pk=host.config.pk
618                     ).role.name
619                 })
620
621         return d
622
623     def to_json(self):
624         return json.dumps(self.to_dict())
625
626     def set_installer(self, installer):
627         self.installer = installer
628         d = json.loads(self.delta)
629         d['installer'] = installer
630         self.delta = json.dumps(d)
631
632     def set_scenario(self, scenario):
633         self.scenario = scenario
634         d = json.loads(self.delta)
635         d['scenario'] = scenario
636         self.delta = json.dumps(d)
637
638     def set_xdf(self, booking, update_delta=True):
639         kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id}
640         self.pdf = reverse('get-pdf', kwargs=kwargs)
641         self.idf = reverse('get-idf', kwargs=kwargs)
642         if update_delta:
643             d = json.loads(self.delta)
644             d['pdf'] = self.pdf
645             d['idf'] = self.idf
646             self.delta = json.dumps(d)
647
648     def add_role(self, host):
649         self.roles.add(host)
650         d = json.loads(self.delta)
651         if 'role' not in d:
652             d['role'] = []
653         d['roles'].append({host.labid: host.config.opnfvRole.name})
654         self.delta = json.dumps(d)
655
656     def clear_delta(self):
657         self.delta = '{}'
658
659     def get_delta(self):
660         return json.loads(self.to_json())
661
662
663 class AccessConfig(TaskConfig):
664     access_type = models.CharField(max_length=50)
665     user = models.ForeignKey(User, on_delete=models.CASCADE)
666     revoke = models.BooleanField(default=False)
667     context = models.TextField(default="")
668     delta = models.TextField(default="{}")
669
670     def to_dict(self):
671         d = {}
672         d['access_type'] = self.access_type
673         d['user'] = self.user.id
674         d['revoke'] = self.revoke
675         try:
676             d['context'] = json.loads(self.context)
677         except Exception:
678             pass
679         return d
680
681     def get_delta(self):
682         d = json.loads(self.to_json())
683         d["lab_token"] = self.accessrelation.lab_token
684
685         return d
686
687     def to_json(self):
688         return json.dumps(self.to_dict())
689
690     def clear_delta(self):
691         d = {}
692         d["lab_token"] = self.accessrelation.lab_token
693         self.delta = json.dumps(d)
694
695     def set_access_type(self, access_type):
696         self.access_type = access_type
697         d = json.loads(self.delta)
698         d['access_type'] = access_type
699         self.delta = json.dumps(d)
700
701     def set_user(self, user):
702         self.user = user
703         d = json.loads(self.delta)
704         d['user'] = self.user.id
705         self.delta = json.dumps(d)
706
707     def set_revoke(self, revoke):
708         self.revoke = revoke
709         d = json.loads(self.delta)
710         d['revoke'] = revoke
711         self.delta = json.dumps(d)
712
713     def set_context(self, context):
714         self.context = json.dumps(context)
715         d = json.loads(self.delta)
716         d['context'] = context
717         self.delta = json.dumps(d)
718
719
720 class SoftwareConfig(TaskConfig):
721     """Handles software installations, such as OPNFV or ONAP."""
722
723     opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
724
725     def to_dict(self):
726         d = {}
727         if self.opnfv:
728             d['opnfv'] = self.opnfv.to_dict()
729
730         d["lab_token"] = self.softwarerelation.lab_token
731         self.delta = json.dumps(d)
732
733         return d
734
735     def get_delta(self):
736         d = {}
737         d['opnfv'] = self.opnfv.get_delta()
738         d['lab_token'] = self.softwarerelation.lab_token
739
740         return d
741
742     def clear_delta(self):
743         self.opnfv.clear_delta()
744
745     def to_json(self):
746         return json.dumps(self.to_dict())
747
748
749 class HardwareConfig(TaskConfig):
750     """Describes the desired configuration of the hardware."""
751
752     image = models.CharField(max_length=100, default="defimage")
753     power = models.CharField(max_length=100, default="off")
754     hostname = models.CharField(max_length=100, default="hostname")
755     ipmi_create = models.BooleanField(default=False)
756     delta = models.TextField()
757
758     keys = set(["id", "image", "power", "hostname", "ipmi_create"])
759
760     def to_dict(self):
761         return self.get_delta()
762
763     def get_delta(self):
764         return self.format_delta(
765             self.hosthardwarerelation.get_resource().get_configuration(self.state),
766             self.hosthardwarerelation.lab_token)
767
768
769 class NetworkConfig(TaskConfig):
770     """Handles network configuration."""
771
772     interfaces = models.ManyToManyField(Interface)
773     delta = models.TextField()
774
775     def to_dict(self):
776         d = {}
777         hid = self.hostnetworkrelation.resource_id
778         d[hid] = {}
779         for interface in self.interfaces.all():
780             d[hid][interface.mac_address] = []
781             if self.state != ConfigState.CLEAN:
782                 for vlan in interface.config.all():
783                     # TODO: should this come from the interface?
784                     # e.g. will different interfaces for different resources need different configs?
785                     d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
786
787         return d
788
789     def to_json(self):
790         return json.dumps(self.to_dict())
791
792     def get_delta(self):
793         d = json.loads(self.to_json())
794         d['lab_token'] = self.hostnetworkrelation.lab_token
795         return d
796
797     def clear_delta(self):
798         self.delta = json.dumps(self.to_dict())
799         self.save()
800
801     def add_interface(self, interface):
802         self.interfaces.add(interface)
803         d = json.loads(self.delta)
804         hid = self.hostnetworkrelation.resource_id
805         if hid not in d:
806             d[hid] = {}
807         d[hid][interface.mac_address] = []
808         for vlan in interface.config.all():
809             d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
810         self.delta = json.dumps(d)
811
812
813 class SnapshotConfig(TaskConfig):
814
815     resource_id = models.CharField(max_length=200, default="default_id")
816     image = models.IntegerField(null=True)
817     dashboard_id = models.IntegerField()
818     delta = models.TextField(default="{}")
819
820     def to_dict(self):
821         d = {}
822         if self.host:
823             d['host'] = self.host.labid
824         if self.image:
825             d['image'] = self.image
826         d['dashboard_id'] = self.dashboard_id
827         return d
828
829     def to_json(self):
830         return json.dumps(self.to_dict())
831
832     def get_delta(self):
833         d = json.loads(self.to_json())
834         return d
835
836     def clear_delta(self):
837         self.delta = json.dumps(self.to_dict())
838         self.save()
839
840     def set_host(self, host):
841         self.host = host
842         d = json.loads(self.delta)
843         d['host'] = host.labid
844         self.delta = json.dumps(d)
845
846     def set_image(self, image):
847         self.image = image
848         d = json.loads(self.delta)
849         d['image'] = self.image
850         self.delta = json.dumps(d)
851
852     def clear_image(self):
853         self.image = None
854         d = json.loads(self.delta)
855         d.pop("image", None)
856         self.delta = json.dumps(d)
857
858     def set_dashboard_id(self, dash):
859         self.dashboard_id = dash
860         d = json.loads(self.delta)
861         d['dashboard_id'] = self.dashboard_id
862         self.delta = json.dumps(d)
863
864     def save(self, *args, **kwargs):
865         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
866             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
867         super().save(*args, **kwargs)
868
869
870 def get_task(task_id):
871     for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]:
872         try:
873             ret = taskclass.objects.get(task_id=task_id)
874             return ret
875         except taskclass.DoesNotExist:
876             pass
877     from django.core.exceptions import ObjectDoesNotExist
878     raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
879
880
881 def get_task_uuid():
882     return str(uuid.uuid4())
883
884
885 class TaskRelation(models.Model):
886     """
887     Relates a Job to a TaskConfig.
888
889     superclass that relates a Job to tasks anc maintains information
890     like status and messages from the lab
891     """
892
893     status = models.IntegerField(default=JobStatus.NEW)
894     job = models.ForeignKey(Job, on_delete=models.CASCADE)
895     config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
896     task_id = models.CharField(default=get_task_uuid, max_length=37)
897     lab_token = models.CharField(default="null", max_length=50)
898     message = models.TextField(default="")
899
900     job_key = None
901
902     def delete(self, *args, **kwargs):
903         self.config.delete()
904         return super(self.__class__, self).delete(*args, **kwargs)
905
906     def type_str(self):
907         return "Generic Task"
908
909     class Meta:
910         abstract = True
911
912
913 class AccessRelation(TaskRelation):
914     config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
915     job_key = "access"
916
917     def type_str(self):
918         return "Access Task"
919
920     def delete(self, *args, **kwargs):
921         self.config.delete()
922         return super(self.__class__, self).delete(*args, **kwargs)
923
924
925 class SoftwareRelation(TaskRelation):
926     config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
927     job_key = "software"
928
929     def type_str(self):
930         return "Software Configuration Task"
931
932     def delete(self, *args, **kwargs):
933         self.config.delete()
934         return super(self.__class__, self).delete(*args, **kwargs)
935
936
937 class HostHardwareRelation(TaskRelation):
938     resource_id = models.CharField(max_length=200, default="default_id")
939     config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
940     job_key = "hardware"
941
942     def type_str(self):
943         return "Hardware Configuration Task"
944
945     def get_delta(self):
946         return self.config.to_dict()
947
948     def delete(self, *args, **kwargs):
949         self.config.delete()
950         return super(self.__class__, self).delete(*args, **kwargs)
951
952     def save(self, *args, **kwargs):
953         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
954             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
955         super().save(*args, **kwargs)
956
957     def get_resource(self):
958         return ResourceQuery.get(labid=self.resource_id)
959
960
961 class HostNetworkRelation(TaskRelation):
962     resource_id = models.CharField(max_length=200, default="default_id")
963     config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
964     job_key = "network"
965
966     def type_str(self):
967         return "Network Configuration Task"
968
969     def delete(self, *args, **kwargs):
970         self.config.delete()
971         return super(self.__class__, self).delete(*args, **kwargs)
972
973     def save(self, *args, **kwargs):
974         if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
975             raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
976         super().save(*args, **kwargs)
977
978     def get_resource(self):
979         return ResourceQuery.get(labid=self.resource_id)
980
981
982 class SnapshotRelation(TaskRelation):
983     snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
984     config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
985     job_key = "snapshot"
986
987     def type_str(self):
988         return "Snapshot Task"
989
990     def get_delta(self):
991         return self.config.to_dict()
992
993     def delete(self, *args, **kwargs):
994         self.config.delete()
995         return super(self.__class__, self).delete(*args, **kwargs)
996
997
998 class ActiveUsersRelation(TaskRelation):
999     config = models.OneToOneField(ActiveUsersConfig, on_delete=models.CASCADE)
1000     job_key = "active users task"
1001
1002     def type_str(self):
1003         return "Active Users Task"
1004
1005
1006 class JobFactory(object):
1007     """This class creates all the API models (jobs, tasks, etc) needed to fulfill a booking."""
1008
1009     @classmethod
1010     def reimageHost(cls, new_image, booking, host):
1011         """Modify an existing job to reimage the given host."""
1012         job = Job.objects.get(booking=booking)
1013         # make hardware task new
1014         hardware_relation = HostHardwareRelation.objects.get(resource_id=host, job=job)
1015         hardware_relation.config.image = new_image.lab_id
1016         hardware_relation.config.save()
1017         hardware_relation.status = JobStatus.NEW
1018
1019         # re-apply networking after host is reset
1020         net_relation = HostNetworkRelation.objects.get(resource_id=host, job=job)
1021         net_relation.status = JobStatus.NEW
1022
1023         # re-apply ssh access after host is reset
1024         for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
1025             relation.status = JobStatus.NEW
1026             relation.save()
1027
1028         hardware_relation.save()
1029         net_relation.save()
1030
1031     @classmethod
1032     def makeSnapshotTask(cls, image, booking, host):
1033         relation = SnapshotRelation()
1034         job = Job.objects.get(booking=booking)
1035         config = SnapshotConfig.objects.create(dashboard_id=image.id)
1036
1037         relation.job = job
1038         relation.config = config
1039         relation.config.save()
1040         relation.config = relation.config
1041         relation.snapshot = image
1042         relation.save()
1043
1044         config.clear_delta()
1045         config.set_host(host)
1046         config.save()
1047
1048     @classmethod
1049     def makeActiveUsersTask(cls):
1050         """ Append active users task to analytics job """
1051         config = ActiveUsersConfig()
1052         relation = ActiveUsersRelation()
1053         job = Job.objects.get(job_type='DATA')
1054
1055         job.status = JobStatus.NEW
1056
1057         relation.job = job
1058         relation.config = config
1059         relation.config.save()
1060         relation.config = relation.config
1061         relation.save()
1062         config.save()
1063
1064     @classmethod
1065     def makeAnalyticsJob(cls, booking):
1066         """
1067         Create the analytics job
1068
1069         This will only run once since there will only be one analytics job.
1070         All analytics tasks get appended to analytics job.
1071         """
1072
1073         if len(Job.objects.filter(job_type='DATA')) > 0:
1074             raise Exception("Cannot have more than one analytics job")
1075
1076         if booking.resource:
1077             raise Exception("Booking is not marker for analytics job, has resoure")
1078
1079         job = Job()
1080         job.booking = booking
1081         job.job_type = 'DATA'
1082         job.save()
1083
1084         cls.makeActiveUsersTask()
1085
1086     @classmethod
1087     def makeCompleteJob(cls, booking):
1088         """Create everything that is needed to fulfill the given booking."""
1089         resources = booking.resource.get_resources()
1090         job = None
1091         try:
1092             job = Job.objects.get(booking=booking)
1093         except Exception:
1094             job = Job.objects.create(status=JobStatus.NEW, booking=booking)
1095         cls.makeHardwareConfigs(
1096             resources=resources,
1097             job=job
1098         )
1099         cls.makeNetworkConfigs(
1100             resources=resources,
1101             job=job
1102         )
1103         cls.makeSoftware(
1104             booking=booking,
1105             job=job
1106         )
1107         all_users = list(booking.collaborators.all())
1108         all_users.append(booking.owner)
1109         cls.makeAccessConfig(
1110             users=all_users,
1111             access_type="vpn",
1112             revoke=False,
1113             job=job
1114         )
1115         for user in all_users:
1116             try:
1117                 cls.makeAccessConfig(
1118                     users=[user],
1119                     access_type="ssh",
1120                     revoke=False,
1121                     job=job,
1122                     context={
1123                         "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
1124                         "hosts": [r.labid for r in resources]
1125                     }
1126                 )
1127             except Exception:
1128                 continue
1129
1130     @classmethod
1131     def makeHardwareConfigs(cls, resources=[], job=Job()):
1132         """
1133         Create and save HardwareConfig.
1134
1135         Helper function to create the tasks related to
1136         configuring the hardware
1137         """
1138         for res in resources:
1139             hardware_config = None
1140             try:
1141                 hardware_config = HardwareConfig.objects.get(relation__resource_id=res.labid)
1142             except Exception:
1143                 hardware_config = HardwareConfig()
1144
1145             relation = HostHardwareRelation()
1146             relation.resource_id = res.labid
1147             relation.job = job
1148             relation.config = hardware_config
1149             relation.config.save()
1150             relation.config = relation.config
1151             relation.save()
1152
1153             hardware_config.set("id", "image", "hostname", "power", "ipmi_create")
1154             hardware_config.save()
1155
1156     @classmethod
1157     def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
1158         """
1159         Create and save AccessConfig.
1160
1161         Helper function to create the tasks related to
1162         configuring the VPN, SSH, etc access for users
1163         """
1164         for user in users:
1165             relation = AccessRelation()
1166             relation.job = job
1167             config = AccessConfig()
1168             config.access_type = access_type
1169             config.user = user
1170             config.save()
1171             relation.config = config
1172             relation.save()
1173             config.clear_delta()
1174             if context:
1175                 config.set_context(context)
1176             config.set_access_type(access_type)
1177             config.set_revoke(revoke)
1178             config.set_user(user)
1179             config.save()
1180
1181     @classmethod
1182     def makeNetworkConfigs(cls, resources=[], job=Job()):
1183         """
1184         Create and save NetworkConfig.
1185
1186         Helper function to create the tasks related to
1187         configuring the networking
1188         """
1189         for res in resources:
1190             network_config = None
1191             try:
1192                 network_config = NetworkConfig.objects.get(relation__host=res)
1193             except Exception:
1194                 network_config = NetworkConfig.objects.create()
1195
1196             relation = HostNetworkRelation()
1197             relation.resource_id = res.labid
1198             relation.job = job
1199             network_config.save()
1200             relation.config = network_config
1201             relation.save()
1202             network_config.clear_delta()
1203
1204             # TODO: use get_interfaces() on resource
1205             for interface in res.interfaces.all():
1206                 network_config.add_interface(interface)
1207             network_config.save()
1208
1209     @classmethod
1210     def make_bridge_config(cls, booking):
1211         if len(booking.resource.get_resources()) < 2:
1212             return None
1213         try:
1214             jumphost_config = ResourceOPNFVConfig.objects.filter(
1215                 role__name__iexact="jumphost"
1216             )
1217             jumphost = ResourceQuery.filter(
1218                 bundle=booking.resource,
1219                 config=jumphost_config.resource_config
1220             )[0]
1221         except Exception:
1222             return None
1223         br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
1224         for iface in jumphost.interfaces.all():
1225             br_config.interfaces.add(iface)
1226         return br_config
1227
1228     @classmethod
1229     def makeSoftware(cls, booking=None, job=Job()):
1230         """
1231         Create and save SoftwareConfig.
1232
1233         Helper function to create the tasks related to
1234         configuring the desired software, e.g. an OPNFV deployment
1235         """
1236         if not booking.opnfv_config:
1237             return None
1238
1239         opnfv_api_config = OpnfvApiConfig.objects.create(
1240             opnfv_config=booking.opnfv_config,
1241             installer=booking.opnfv_config.installer.name,
1242             scenario=booking.opnfv_config.scenario.name,
1243             bridge_config=cls.make_bridge_config(booking)
1244         )
1245
1246         opnfv_api_config.set_xdf(booking, False)
1247         opnfv_api_config.save()
1248
1249         for host in booking.resource.get_resources():
1250             opnfv_api_config.roles.add(host)
1251         software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
1252         software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
1253         return software_relation
1254
1255
1256 JOB_TASK_CLASSLIST = [
1257     HostHardwareRelation,
1258     AccessRelation,
1259     HostNetworkRelation,
1260     SoftwareRelation,
1261     SnapshotRelation,
1262     ActiveUsersRelation
1263 ]
1264
1265
1266 class JobTaskQuery(AbstractModelQuery):
1267     model_list = JOB_TASK_CLASSLIST