Merge "Landing page now links to LaaS 2.0 wiki entry"
[pharos-tools.git] / dashboard / 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
15 import json
16 import uuid
17
18 from booking.models import Booking
19 from resource_inventory.models import (
20     Lab,
21     HostProfile,
22     Host,
23     Image,
24     Interface
25 )
26
27
28 class JobStatus(object):
29     NEW = 0
30     CURRENT = 100
31     DONE = 200
32     ERROR = 300
33
34
35 class LabManagerTracker(object):
36
37     @classmethod
38     def get(cls, lab_name, token):
39         """
40         Takes in a lab name (from a url path)
41         returns a lab manager instance for that lab, if it exists
42         """
43         try:
44             lab = Lab.objects.get(name=lab_name)
45         except:
46             raise PermissionDenied("Lab not found")
47         if lab.api_token == token:
48             return LabManager(lab)
49         raise PermissionDenied("Lab not authorized")
50
51
52 class LabManager(object):
53     """
54     This is the class that will ultimately handle all REST calls to
55     lab endpoints.
56     handles jobs, inventory, status, etc
57     may need to create helper classes
58     """
59
60     def __init__(self, lab):
61         self.lab = lab
62
63     def get_profile(self):
64         prof = {}
65         prof['name'] = self.lab.name
66         prof['contact'] = {
67             "phone": self.lab.contact_phone,
68             "email": self.lab.contact_email
69         }
70         prof['host_count'] = []
71         for host in HostProfile.objects.filter(labs=self.lab):
72             count = Host.objects.filter(profile=host, lab=self.lab).count()
73             prof['host_count'].append(
74                 {
75                     "type": host.name,
76                     "count": count
77                 }
78             )
79         return prof
80
81     def get_inventory(self):
82         inventory = {}
83         hosts = Host.objects.filter(lab=self.lab)
84         images = Image.objects.filter(from_lab=self.lab)
85         profiles = HostProfile.objects.filter(labs=self.lab)
86         inventory['hosts'] = self.serialize_hosts(hosts)
87         inventory['images'] = self.serialize_images(images)
88         inventory['host_types'] = self.serialize_host_profiles(profiles)
89         return inventory
90
91     def get_status(self):
92         return {"status": self.lab.status}
93
94     def set_status(self, payload):
95         {}
96
97     def get_current_jobs(self):
98         jobs = Job.objects.filter(booking__lab=self.lab)
99
100         return self.serialize_jobs(jobs, status=JobStatus.CURRENT)
101
102     def get_new_jobs(self):
103         jobs = Job.objects.filter(booking__lab=self.lab)
104
105         return self.serialize_jobs(jobs, status=JobStatus.NEW)
106
107     def get_done_jobs(self):
108         jobs = Job.objects.filter(booking__lab=self.lab)
109
110         return self.serialize_jobs(jobs, status=JobStatus.DONE)
111
112     def get_job(self, jobid):
113         return Job.objects.get(pk=jobid).to_dict()
114
115     def update_job(self, jobid, data):
116         {}
117
118     def serialize_jobs(self, jobs, status=JobStatus.NEW):
119         job_ser = []
120         for job in jobs:
121             jsonized_job = job.get_delta(status)
122             if len(jsonized_job['payload']) < 1:
123                 continue
124             job_ser.append(jsonized_job)
125
126         return job_ser
127
128     def serialize_hosts(self, hosts):
129         host_ser = []
130         for host in hosts:
131             h = {}
132             h['interfaces'] = []
133             h['hostname'] = host.name
134             h['host_type'] = host.profile.name
135             for iface in host.interfaces.all():
136                 eth = {}
137                 eth['mac'] = iface.mac_address
138                 eth['busaddr'] = iface.bus_address
139                 eth['name'] = iface.name
140                 eth['switchport'] = {"switch_name": iface.switch_name, "port_name": iface.port_name}
141                 h['interfaces'].append(eth)
142         return host_ser
143
144     def serialize_images(self, images):
145         images_ser = []
146         for image in images:
147             images_ser.append(
148                 {
149                     "name": image.name,
150                     "lab_id": image.lab_id,
151                     "dashboard_id": image.id
152                 }
153             )
154         return images_ser
155
156     def serialize_host_profiles(self, profiles):
157         profile_ser = []
158         for profile in profiles:
159             p = {}
160             p['cpu'] = {
161                 "cores": profile.cpuprofile.first().cores,
162                 "arch": profile.cpuprofile.first().architecture,
163                 "cpus": profile.cpuprofile.first().cpus,
164             }
165             p['disks'] = []
166             for disk in profile.storageprofile.all():
167                 d = {
168                     "size": disk.size,
169                     "type": disk.media_type,
170                     "name": disk.name
171                 }
172                 p['disks'].append(d)
173             p['description'] = profile.description
174             p['interfaces'] = []
175             for iface in profile.interfaceprofile.all():
176                 p['interfaces'].append(
177                     {
178                         "speed": iface.speed,
179                         "name": iface.name
180                     }
181                 )
182
183             p['ram'] = {"amount": profile.ramprofile.first().amount}
184             p['name'] = profile.name
185             profile_ser.append(p)
186         return profile_ser
187
188
189 class Job(models.Model):
190     """
191     This is the class that is serialized and put into the api
192     """
193     booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
194     status = models.IntegerField(default=JobStatus.NEW)
195     complete = models.BooleanField(default=False)
196
197     def to_dict(self):
198         d = {}
199         j = {}
200         j['id'] = self.id
201         for relation in AccessRelation.objects.filter(job=self):
202             if 'access' not in d:
203                 d['access'] = {}
204             d['access'][relation.task_id] = relation.config.to_dict()
205         for relation in SoftwareRelation.objects.filter(job=self):
206             if 'software' not in d:
207                 d['software'] = {}
208             d['software'][relation.task_id] = relation.config.to_dict()
209         for relation in HostHardwareRelation.objects.filter(job=self):
210             if 'hardware' not in d:
211                 d['hardware'] = {}
212             d['hardware'][relation.task_id] = relation.config.to_dict()
213         for relation in HostNetworkRelation.objects.filter(job=self):
214             if 'network' not in d:
215                 d['network'] = {}
216             d['network'][relation.task_id] = relation.config.to_dict()
217
218         j['payload'] = d
219
220         return j
221
222     def get_tasklist(self, status="all"):
223         tasklist = []
224         clist = [HostHardwareRelation, AccessRelation, HostNetworkRelation, SoftwareRelation]
225         if status == "all":
226             for cls in clist:
227                 tasklist += list(cls.objects.filter(job=self))
228         else:
229             for cls in clist:
230                 tasklist += list(cls.objects.filter(job=self).filter(status=status))
231         return tasklist
232
233     def is_fulfilled(self):
234         """
235         This method should return true if all of the job's tasks are done,
236         and false otherwise
237         """
238         my_tasks = self.get_tasklist()
239         for task in my_tasks:
240             if task.status != JobStatus.DONE:
241                 return False
242         return True
243
244     def get_delta(self, status):
245         d = {}
246         j = {}
247         j['id'] = self.id
248         for relation in AccessRelation.objects.filter(job=self).filter(status=status):
249             if 'access' not in d:
250                 d['access'] = {}
251             d['access'][relation.task_id] = relation.config.get_delta()
252         for relation in SoftwareRelation.objects.filter(job=self).filter(status=status):
253             if 'software' not in d:
254                 d['software'] = {}
255             d['software'][relation.task_id] = relation.config.get_delta()
256         for relation in HostHardwareRelation.objects.filter(job=self).filter(status=status):
257             if 'hardware' not in d:
258                 d['hardware'] = {}
259             d['hardware'][relation.task_id] = relation.config.get_delta()
260         for relation in HostNetworkRelation.objects.filter(job=self).filter(status=status):
261             if 'network' not in d:
262                 d['network'] = {}
263             d['network'][relation.task_id] = relation.config.get_delta()
264
265         j['payload'] = d
266         return j
267
268     def to_json(self):
269         return json.dumps(self.to_dict())
270
271
272 class TaskConfig(models.Model):
273     def to_dict(self):
274         pass
275
276     def get_delta(self):
277         pass
278
279     def to_json(self):
280         return json.dumps(self.to_dict())
281
282     def clear_delta(self):
283         self.delta = '{}'
284
285
286 class OpnfvApiConfig(models.Model):
287
288     installer = models.CharField(max_length=100)
289     scenario = models.CharField(max_length=100)
290     roles = models.ManyToManyField(Host)
291     delta = models.TextField()
292
293     def to_dict(self):
294         d = {}
295         if self.installer:
296             d['installer'] = self.installer
297         if self.scenario:
298             d['scenario'] = self.scenario
299
300         hosts = self.roles.all()
301         if hosts.exists():
302             d['roles'] = []
303         for host in self.roles.all():
304             d['roles'].append({host.labid: host.config.opnfvRole.name})
305
306         return d
307
308     def to_json(self):
309         return json.dumps(self.to_dict())
310
311     def set_installer(self, installer):
312         self.installer = installer
313         d = json.loads(self.delta)
314         d['installer'] = installer
315         self.delta = json.dumps(d)
316
317     def set_scenario(self, scenario):
318         self.scenario = scenario
319         d = json.loads(self.delta)
320         d['scenario'] = scenario
321         self.delta = json.dumps(d)
322
323     def add_role(self, host):
324         self.roles.add(host)
325         d = json.loads(self.delta)
326         if 'role' not in d:
327             d['role'] = []
328         d['roles'].append({host.labid: host.config.opnfvRole.name})
329         self.delta = json.dumps(d)
330
331     def clear_delta(self):
332         self.delta = '{}'
333
334     def get_delta(self):
335         if not self.delta:
336             self.delta = self.to_json()
337             self.save()
338         return json.loads(self.delta)
339
340
341 class AccessConfig(TaskConfig):
342     access_type = models.CharField(max_length=50)
343     user = models.ForeignKey(User, on_delete=models.CASCADE)
344     revoke = models.BooleanField(default=False)
345     context = models.TextField(default="")
346     delta = models.TextField(default="{}")
347
348     def to_dict(self):
349         d = {}
350         d['access_type'] = self.access_type
351         d['user'] = self.user.id
352         d['revoke'] = self.revoke
353         try:
354             d['context'] = json.loads(self.context)
355         except:
356             pass
357         return d
358
359     def get_delta(self):
360         if not self.delta:
361             self.delta = self.to_json()
362             self.save()
363         d = json.loads(self.delta)
364         d["lab_token"] = self.accessrelation.lab_token
365
366         return d
367
368     def to_json(self):
369         return json.dumps(self.to_dict())
370
371     def clear_delta(self):
372         d = {}
373         d["lab_token"] = self.accessrelation.lab_token
374         self.delta = json.dumps(d)
375
376     def set_access_type(self, access_type):
377         self.access_type = access_type
378         d = json.loads(self.delta)
379         d['access_type'] = access_type
380         self.delta = json.dumps(d)
381
382     def set_user(self, user):
383         self.user = user
384         d = json.loads(self.delta)
385         d['user'] = self.user.id
386         self.delta = json.dumps(d)
387
388     def set_revoke(self, revoke):
389         self.revoke = revoke
390         d = json.loads(self.delta)
391         d['revoke'] = revoke
392         self.delta = json.dumps(d)
393
394     def set_context(self, context):
395         self.context = json.dumps(context)
396         d = json.loads(self.delta)
397         d['context'] = context
398         self.delta = json.dumps(d)
399
400
401 class SoftwareConfig(TaskConfig):
402     """
403     handled opnfv installations, etc
404     """
405     opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
406
407     def to_dict(self):
408         d = {}
409         if self.opnfv:
410             d['opnfv'] = self.opnfv.to_dict()
411
412         d["lab_token"] = self.softwarerelation.lab_token
413         self.delta = json.dumps(d)
414
415         return d
416
417     def get_delta(self):
418         d = {}
419         d['opnfv'] = self.opnfv.get_delta()
420         d['lab_token'] = self.softwarerelation.lab_token
421
422         return d
423
424     def clear_delta(self):
425         self.opnfv.clear_delta()
426
427     def to_json(self):
428         return json.dumps(self.to_dict())
429
430
431 class HardwareConfig(TaskConfig):
432     """
433     handles imaging, user accounts, etc
434     """
435     image = models.CharField(max_length=100, default="defimage")
436     power = models.CharField(max_length=100, default="off")
437     hostname = models.CharField(max_length=100, default="hostname")
438     ipmi_create = models.BooleanField(default=False)
439     delta = models.TextField()
440
441     def to_dict(self):
442         d = {}
443         d['image'] = self.image
444         d['power'] = self.power
445         d['hostname'] = self.hostname
446         d['ipmi_create'] = str(self.ipmi_create)
447         d['id'] = self.hosthardwarerelation.host.labid
448         return d
449
450     def to_json(self):
451         return json.dumps(self.to_dict())
452
453     def get_delta(self):
454         if not self.delta:
455             self.delta = self.to_json()
456             self.save()
457         d = json.loads(self.delta)
458         d['lab_token'] = self.hosthardwarerelation.lab_token
459         return d
460
461     def clear_delta(self):
462         d = {}
463         d["id"] = self.hosthardwarerelation.host.labid
464         d["lab_token"] = self.hosthardwarerelation.lab_token
465         self.delta = json.dumps(d)
466
467     def set_image(self, image):
468         self.image = image
469         d = json.loads(self.delta)
470         d['image'] = self.image
471         self.delta = json.dumps(d)
472
473     def set_power(self, power):
474         self.power = power
475         d = json.loads(self.delta)
476         d['power'] = power
477         self.delta = json.dumps(d)
478
479     def set_hostname(self, hostname):
480         self.hostname = hostname
481         d = json.loads(self.delta)
482         d['hostname'] = hostname
483         self.delta = json.dumps(d)
484
485     def set_ipmi_create(self, ipmi_create):
486         self.ipmi_create = ipmi_create
487         d = json.loads(self.delta)
488         d['ipmi_create'] = ipmi_create
489         self.delta = json.dumps(d)
490
491
492 class NetworkConfig(TaskConfig):
493     """
494     handles network configuration
495     """
496     interfaces = models.ManyToManyField(Interface)
497     delta = models.TextField()
498
499     def to_dict(self):
500         d = {}
501         hid = self.hostnetworkrelation.host.labid
502         d[hid] = {}
503         for interface in self.interfaces.all():
504             d[hid][interface.mac_address] = []
505             for vlan in interface.config.all():
506                 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
507
508         return d
509
510     def to_json(self):
511         return json.dumps(self.to_dict())
512
513     def get_delta(self):
514         if not self.delta:
515             self.delta = self.to_json()
516             self.save()
517         d = json.loads(self.delta)
518         d['lab_token'] = self.hostnetworkrelation.lab_token
519         return d
520
521     def clear_delta(self):
522         self.delta = json.dumps(self.to_dict())
523         self.save()
524
525     def add_interface(self, interface):
526         self.interfaces.add(interface)
527         d = json.loads(self.delta)
528         hid = self.hostnetworkrelation.host.labid
529         if hid not in d:
530             d[hid] = {}
531         d[hid][interface.mac_address] = []
532         for vlan in interface.config.all():
533             d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
534         self.delta = json.dumps(d)
535
536
537 def get_task(task_id):
538     for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]:
539         try:
540             ret = taskclass.objects.get(task_id=task_id)
541             return ret
542         except taskclass.DoesNotExist:
543             pass
544     from django.core.exceptions import ObjectDoesNotExist
545     raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
546
547
548 def get_task_uuid():
549     return str(uuid.uuid4())
550
551
552 class TaskRelation(models.Model):
553     status = models.IntegerField(default=JobStatus.NEW)
554     job = models.ForeignKey(Job, on_delete=models.CASCADE)
555     config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
556     task_id = models.CharField(default=get_task_uuid, max_length=37)
557     lab_token = models.CharField(default="null", max_length=50)
558     message = models.TextField(default="")
559
560     def delete(self, *args, **kwargs):
561         self.config.delete()
562         return super(self.__class__, self).delete(*args, **kwargs)
563
564     def type_str(self):
565         return "Generic Task"
566
567     class Meta:
568         abstract = True
569
570
571 class AccessRelation(TaskRelation):
572     config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
573
574     def type_str(self):
575         return "Access Task"
576
577     def delete(self, *args, **kwargs):
578         self.config.delete()
579         return super(self.__class__, self).delete(*args, **kwargs)
580
581
582 class SoftwareRelation(TaskRelation):
583     config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
584
585     def type_str(self):
586         return "Software Configuration Task"
587
588     def delete(self, *args, **kwargs):
589         self.config.delete()
590         return super(self.__class__, self).delete(*args, **kwargs)
591
592
593 class HostHardwareRelation(TaskRelation):
594     host = models.ForeignKey(Host, on_delete=models.CASCADE)
595     config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
596
597     def type_str(self):
598         return "Hardware Configuration Task"
599
600     def get_delta(self):
601         return self.config.to_dict()
602
603     def delete(self, *args, **kwargs):
604         self.config.delete()
605         return super(self.__class__, self).delete(*args, **kwargs)
606
607
608 class HostNetworkRelation(TaskRelation):
609     host = models.ForeignKey(Host, on_delete=models.CASCADE)
610     config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
611
612     def type_str(self):
613         return "Network Configuration Task"
614
615     def delete(self, *args, **kwargs):
616         self.config.delete()
617         return super(self.__class__, self).delete(*args, **kwargs)
618
619
620 class JobFactory(object):
621
622     @classmethod
623     def makeCompleteJob(cls, booking):
624         hosts = Host.objects.filter(bundle=booking.resource)
625         job = None
626         try:
627             job = Job.objects.get(booking=booking)
628         except:
629             job = Job.objects.create(status=JobStatus.NEW, booking=booking)
630         cls.makeHardwareConfigs(
631             hosts=hosts,
632             job=job
633         )
634         cls.makeNetworkConfigs(
635             hosts=hosts,
636             job=job
637         )
638         cls.makeSoftware(
639             hosts=hosts,
640             job=job
641         )
642         all_users = list(booking.collaborators.all())
643         all_users.append(booking.owner)
644         cls.makeAccessConfig(
645             users=all_users,
646             access_type="vpn",
647             revoke=False,
648             job=job
649         )
650         for user in all_users:
651             try:
652                 cls.makeAccessConfig(
653                     users=[user],
654                     access_type="ssh",
655                     revoke=False,
656                     job=job,
657                     context={
658                         "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
659                         "hosts": [host.labid for host in hosts]
660                     }
661                 )
662             except Exception:
663                 continue
664
665     @classmethod
666     def makeHardwareConfigs(cls, hosts=[], job=Job()):
667         for host in hosts:
668             hardware_config = None
669             try:
670                 hardware_config = HardwareConfig.objects.get(relation__host=host)
671             except:
672                 hardware_config = HardwareConfig()
673
674             relation = HostHardwareRelation()
675             relation.host = host
676             relation.job = job
677             relation.config = hardware_config
678             relation.config.save()
679             relation.config = relation.config
680             relation.save()
681
682             hardware_config.clear_delta()
683             hardware_config.set_image(host.config.image.lab_id)
684             hardware_config.set_hostname(host.template.resource.name)
685             hardware_config.set_power("on")
686             hardware_config.set_ipmi_create(True)
687             hardware_config.save()
688
689     @classmethod
690     def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
691         for user in users:
692             relation = AccessRelation()
693             relation.job = job
694             config = AccessConfig()
695             config.access_type = access_type
696             config.user = user
697             config.save()
698             relation.config = config
699             relation.save()
700             config.clear_delta()
701             if context:
702                 config.set_context(context)
703             config.set_access_type(access_type)
704             config.set_revoke(revoke)
705             config.set_user(user)
706             config.save()
707
708     @classmethod
709     def makeNetworkConfigs(cls, hosts=[], job=Job()):
710         for host in hosts:
711             network_config = None
712             try:
713                 network_config = NetworkConfig.objects.get(relation__host=host)
714             except:
715                 network_config = NetworkConfig.objects.create()
716
717             relation = HostNetworkRelation()
718             relation.host = host
719             relation.job = job
720             network_config.save()
721             relation.config = network_config
722             relation.save()
723             network_config.clear_delta()
724
725             for interface in host.interfaces.all():
726                 network_config.add_interface(interface)
727             network_config.save()
728
729     @classmethod
730     def makeSoftware(cls, hosts=[], job=Job()):
731         def init_config(host):
732             opnfv_config = OpnfvApiConfig()
733             if host is not None:
734                 opnfv = host.config.bundle.opnfv_config.first()
735                 opnfv_config.installer = opnfv.installer.name
736                 opnfv_config.scenario = opnfv.scenario.name
737             opnfv_config.save()
738             return opnfv_config
739
740         try:
741             host = None
742             if len(hosts) > 0:
743                 host = hosts[0]
744             opnfv_config = init_config(host)
745
746             for host in hosts:
747                 opnfv_config.roles.add(host)
748             software_config = SoftwareConfig.objects.create(opnfv=opnfv_config)
749             software_config.save()
750             software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
751             software_relation.save()
752             return software_relation
753         except:
754             return None