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