Merge "Fix error in Booking cleanup task"
[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.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         self.delta = json.dumps(self.to_dict())
492         self.save()
493
494     def add_interface(self, interface):
495         self.interfaces.add(interface)
496         d = json.loads(self.delta)
497         hid = self.hostnetworkrelation.host.labid
498         if hid not in d:
499             d[hid] = {}
500         d[hid][interface.mac_address] = []
501         for vlan in interface.config.all():
502             d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
503         self.delta = json.dumps(d)
504
505
506 def get_task(task_id):
507     for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]:
508         try:
509             ret = taskclass.objects.get(task_id=task_id)
510             return ret
511         except taskclass.DoesNotExist:
512             pass
513     from django.core.exceptions import ObjectDoesNotExist
514     raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
515
516
517 def get_task_uuid():
518     return str(uuid.uuid4())
519
520
521 class TaskRelation(models.Model):
522     status = models.IntegerField(default=JobStatus.NEW)
523     job = models.ForeignKey(Job, on_delete=models.CASCADE)
524     config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
525     task_id = models.CharField(default=get_task_uuid, max_length=37)
526     lab_token = models.CharField(default="null", max_length=50)
527     message = models.TextField(default="")
528
529     def delete(self, *args, **kwargs):
530         self.config.delete()
531         return super(self.__class__, self).delete(*args, **kwargs)
532
533     def type_str(self):
534         return "Generic Task"
535
536     class Meta:
537         abstract = True
538
539
540 class AccessRelation(TaskRelation):
541     config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
542
543     def type_str(self):
544         return "Access Task"
545
546     def delete(self, *args, **kwargs):
547         self.config.delete()
548         return super(self.__class__, self).delete(*args, **kwargs)
549
550
551 class SoftwareRelation(TaskRelation):
552     config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
553
554     def type_str(self):
555         return "Software Configuration Task"
556
557     def delete(self, *args, **kwargs):
558         self.config.delete()
559         return super(self.__class__, self).delete(*args, **kwargs)
560
561
562 class HostHardwareRelation(TaskRelation):
563     host = models.ForeignKey(Host, on_delete=models.CASCADE)
564     config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
565
566     def type_str(self):
567         return "Hardware Configuration Task"
568
569     def get_delta(self):
570         return self.config.to_dict()
571
572     def delete(self, *args, **kwargs):
573         self.config.delete()
574         return super(self.__class__, self).delete(*args, **kwargs)
575
576
577 class HostNetworkRelation(TaskRelation):
578     host = models.ForeignKey(Host, on_delete=models.CASCADE)
579     config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
580
581     def type_str(self):
582         return "Network Configuration Task"
583
584     def delete(self, *args, **kwargs):
585         self.config.delete()
586         return super(self.__class__, self).delete(*args, **kwargs)
587
588
589 class JobFactory(object):
590
591     @classmethod
592     def makeCompleteJob(cls, booking):
593         hosts = Host.objects.filter(bundle=booking.resource)
594         job = None
595         try:
596             job = Job.objects.get(booking=booking)
597         except:
598             job = Job.objects.create(status=JobStatus.NEW, booking=booking)
599         cls.makeHardwareConfigs(
600                 hosts=hosts,
601                 job=job
602                 )
603         cls.makeNetworkConfigs(
604                 hosts=hosts,
605                 job=job
606                 )
607         cls.makeSoftware(
608                 hosts=hosts,
609                 job=job
610                 )
611         cls.makeAccessConfig(
612                 users=booking.collaborators.all(),
613                 access_type="vpn",
614                 revoke=False,
615                 job=job
616                 )
617         cls.makeAccessConfig(
618                 users=[booking.owner],
619                 access_type="vpn",
620                 revoke=False,
621                 job=job
622                 )
623
624     @classmethod
625     def makeHardwareConfigs(cls, hosts=[], job=Job()):
626         for host in hosts:
627             hardware_config = None
628             try:
629                 hardware_config = HardwareConfig.objects.get(relation__host=host)
630             except:
631                 hardware_config = HardwareConfig()
632
633             relation = HostHardwareRelation()
634             relation.host = host
635             relation.job = job
636             relation.config = hardware_config
637             relation.config.save()
638             relation.config = relation.config
639             relation.save()
640
641             hardware_config.clear_delta()
642             hardware_config.set_image(host.config.image.lab_id)
643             hardware_config.set_hostname(host.template.resource.name)
644             hardware_config.set_power("on")
645             hardware_config.set_ipmi_create(True)
646             hardware_config.save()
647
648     @classmethod
649     def makeAccessConfig(cls, users, access_type, revoke=False, job=Job()):
650         for user in users:
651             relation = AccessRelation()
652             relation.job = job
653             config = AccessConfig()
654             config.access_type = access_type
655             config.user = user
656             config.save()
657             relation.config = config
658             relation.save()
659             config.clear_delta()
660             config.set_access_type(access_type)
661             config.set_revoke(revoke)
662             config.set_user(user)
663             config.save()
664
665     @classmethod
666     def makeNetworkConfigs(cls, hosts=[], job=Job()):
667         for host in hosts:
668             network_config = None
669             try:
670                 network_config = NetworkConfig.objects.get(relation__host=host)
671             except:
672                 network_config = NetworkConfig.objects.create()
673
674             relation = HostNetworkRelation()
675             relation.host = host
676             relation.job = job
677             network_config.save()
678             relation.config = network_config
679             relation.save()
680             network_config.clear_delta()
681
682             for interface in host.interfaces.all():
683                 network_config.add_interface(interface)
684             network_config.save()
685
686     @classmethod
687     def makeSoftware(cls, hosts=[], job=Job()):
688         def init_config(host):
689             opnfv_config = OpnfvApiConfig()
690             if host is not None:
691                 opnfv = host.config.bundle.opnfv_config.first()
692                 opnfv_config.installer = opnfv.installer.name
693                 opnfv_config.scenario = opnfv.scenario.name
694             opnfv_config.save()
695             return opnfv_config
696
697         try:
698             host = None
699             if len(hosts) > 0:
700                 host = hosts[0]
701             opnfv_config = init_config(host)
702
703             for host in hosts:
704                 opnfv_config.roles.add(host)
705             software_config = SoftwareConfig.objects.create(opnfv=opnfv_config)
706             software_config.save()
707             software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
708             software_relation.save()
709             return software_relation
710         except:
711             return None
712
713     def makeAccess(cls, user, access_type, revoke):
714         pass