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