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