42375598ce9bdf719f17e512a6d01388d536bad8
[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 is_fulfilled(self):
221         """
222         This method should return true if all of the job's tasks are done,
223         and false otherwise
224         """
225         my_tasks = self.get_tasklist()
226         for task in my_tasks:
227             if task.status != JobStatus.DONE:
228                 return False
229         return True
230
231
232     def get_delta(self, status):
233         d = {}
234         j = {}
235         j['id'] = self.id
236         for relation in AccessRelation.objects.filter(job=self).filter(status=status):
237             if 'access' not in d:
238                 d['access'] = {}
239             d['access'][relation.task_id] = relation.config.get_delta()
240         for relation in SoftwareRelation.objects.filter(job=self).filter(status=status):
241             if 'software' not in d:
242                 d['software'] = {}
243             d['software'][relation.task_id] = relation.config.get_delta()
244         for relation in HostHardwareRelation.objects.filter(job=self).filter(status=status):
245             if 'hardware' not in d:
246                 d['hardware'] = {}
247             d['hardware'][relation.task_id] = relation.config.get_delta()
248         for relation in HostNetworkRelation.objects.filter(job=self).filter(status=status):
249             if 'network' not in d:
250                 d['network'] = {}
251             d['network'][relation.task_id] = relation.config.get_delta()
252
253         j['payload'] = d
254         return j
255
256     def to_json(self):
257         return json.dumps(self.to_dict())
258
259
260 class TaskConfig(models.Model):
261     def to_dict(self):
262         pass
263
264     def get_delta(self):
265         pass
266
267     def to_json(self):
268         return json.dumps(self.to_dict())
269
270     def clear_delta(self):
271         self.delta = '{}'
272
273 class OpnfvApiConfig(models.Model):
274
275     installer = models.CharField(max_length=100)
276     scenario = models.CharField(max_length=100)
277     roles = models.ManyToManyField(Host)
278     delta = models.TextField()
279
280     def to_dict(self):
281         d = {}
282         if self.installer:
283             d['installer'] = self.installer
284         if self.scenario:
285             d['scenario'] = self.scenario
286
287         hosts = self.roles.all()
288         if hosts.exists():
289             d['roles'] = []
290         for host in self.roles.all():
291             d['roles'].append({host.labid: host.config.opnfvRole.name})
292
293         return d
294
295     def to_json(self):
296         return json.dumps(self.to_dict())
297
298     def set_installer(self, installer):
299         self.installer = installer
300         d = json.loads(self.delta)
301         d['installer'] = installer
302         self.delta = json.dumps(d)
303
304     def set_scenario(self, scenario):
305         self.scenario = scenario
306         d = json.loads(self.delta)
307         d['scenario'] = scenario
308         self.delta = json.dumps(d)
309
310     def add_role(self, host):
311         self.roles.add(host)
312         d = json.loads(self.delta)
313         if 'role' not in d:
314             d['role'] = []
315         d['roles'].append({host.labid: host.config.opnfvRole.name})
316         self.delta = json.dumps(d)
317
318     def clear_delta(self):
319         self.delta = '{}'
320
321     def get_delta(self):
322         if not self.delta:
323             self.delta = self.to_json()
324             self.save()
325         return json.loads(self.delta)
326
327 class AccessConfig(TaskConfig):
328     access_type = models.CharField(max_length=50)
329     user = models.ForeignKey(User, on_delete=models.CASCADE)
330     revoke = models.BooleanField(default=False)
331     context = models.TextField(default="")
332     delta = models.TextField()
333
334     def to_dict(self):
335         d = {}
336         d['access_type'] = self.access_type
337         d['user'] = self.user.id
338         d['revoke'] = self.revoke
339         d['context'] = json.loads(self.context)
340         return d
341
342     def get_delta(self):
343         if not self.delta:
344             self.delta = self.to_json()
345             self.save()
346         d = json.loads(self.delta)
347         d["lab_token"] = self.accessrelation.lab_token
348
349         return d
350
351     def to_json(self):
352         return json.dumps(self.to_dict())
353
354     def clear_delta(self):
355         d = {}
356         d["lab_token"] = self.accessrelation.lab_token
357         self.delta = json.dumps(d)
358
359     def set_access_type(self, access_type):
360         self.access_type = access_type
361         d = json.loads(self.delta)
362         d['access_type'] = access_type
363         self.delta = json.dumps(d)
364
365     def set_user(self, user):
366         self.user = user
367         d = json.loads(self.delta)
368         d['user'] = self.user.id
369         self.delta = json.dumps(d)
370
371     def set_revoke(self, revoke):
372         self.revoke = revoke
373         d = json.loads(self.delta)
374         d['revoke'] = revoke
375         self.delta = json.dumps(d)
376
377     def set_context(self, context):
378         self.context = json.dumps(context)
379         d = json.loads(self.delta)
380         d['context'] = context
381         self.delta = json.dumps(d)
382
383 class SoftwareConfig(TaskConfig):
384     """
385     handled opnfv installations, etc
386     """
387     opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
388
389     def to_dict(self):
390         d = {}
391         if self.opnfv:
392             d['opnfv'] = self.opnfv.to_dict()
393
394         d["lab_token"] = self.softwarerelation.lab_token
395         self.delta = json.dumps(d)
396
397         return d
398
399     def get_delta(self):
400         d = {}
401         d['opnfv'] = self.opnfv.get_delta()
402         d['lab_token'] = self.softwarerelation.lab_token
403
404         return d
405
406     def clear_delta(self):
407         self.opnfv.clear_delta()
408
409     def to_json(self):
410         return json.dumps(self.to_dict())
411
412 class HardwareConfig(TaskConfig):
413     """
414     handles imaging, user accounts, etc
415     """
416     image = models.CharField(max_length=100, default="defimage")
417     power = models.CharField(max_length=100, default="off")
418     hostname = models.CharField(max_length=100, default="hostname")
419     ipmi_create = models.BooleanField(default=False)
420     delta = models.TextField()
421
422     def to_dict(self):
423         d = {}
424         d['image'] = self.image
425         d['power'] = self.power
426         d['hostname'] = self.hostname
427         d['ipmi_create'] = str(self.ipmi_create)
428         d['id'] = self.hosthardwarerelation.host.labid
429         return d
430
431     def to_json(self):
432         return json.dumps(self.to_dict())
433
434     def get_delta(self):
435         if not self.delta:
436             self.delta = self.to_json()
437             self.save()
438         d = json.loads(self.delta)
439         d['lab_token'] = self.hosthardwarerelation.lab_token
440         return d
441
442     def clear_delta(self):
443         d = {}
444         d["id"] = self.hosthardwarerelation.host.labid
445         d["lab_token"] = self.hosthardwarerelation.lab_token
446         self.delta = json.dumps(d)
447
448     def set_image(self, image):
449         self.image = image
450         d = json.loads(self.delta)
451         d['image'] = self.image
452         self.delta = json.dumps(d)
453
454     def set_power(self, power):
455         self.power = power
456         d = json.loads(self.delta)
457         d['power'] = power
458         self.delta = json.dumps(d)
459
460     def set_hostname(self, hostname):
461         self.hostname = hostname
462         d = json.loads(self.delta)
463         d['hostname'] = hostname
464         self.delta = json.dumps(d)
465
466     def set_ipmi_create(self, ipmi_create):
467         self.ipmi_create = ipmi_create
468         d = json.loads(self.delta)
469         d['ipmi_create'] = ipmi_create
470         self.delta = json.dumps(d)
471
472
473 class NetworkConfig(TaskConfig):
474     """
475     handles network configuration
476     """
477     interfaces = models.ManyToManyField(Interface)
478     delta = models.TextField()
479
480     def to_dict(self):
481         d = {}
482         hid = self.hostnetworkrelation.host.labid
483         d[hid] = {}
484         for interface in self.interfaces.all():
485             d[hid][interface.mac_address] = []
486             for vlan in interface.config.all():
487                 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
488
489         return d
490
491     def to_json(self):
492         return json.dumps(self.to_dict())
493
494     def get_delta(self):
495         if not self.delta:
496             self.delta = self.to_json()
497             self.save()
498         d = json.loads(self.delta)
499         d['lab_token'] = self.hostnetworkrelation.lab_token
500         return d
501
502     def clear_delta(self):
503         self.delta = json.dumps(self.to_dict())
504         self.save()
505
506     def add_interface(self, interface):
507         self.interfaces.add(interface)
508         d = json.loads(self.delta)
509         hid = self.hostnetworkrelation.host.labid
510         if hid not in d:
511             d[hid] = {}
512         d[hid][interface.mac_address] = []
513         for vlan in interface.config.all():
514             d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
515         self.delta = json.dumps(d)
516
517
518 def get_task(task_id):
519     for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]:
520         try:
521             ret = taskclass.objects.get(task_id=task_id)
522             return ret
523         except taskclass.DoesNotExist:
524             pass
525     from django.core.exceptions import ObjectDoesNotExist
526     raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
527
528
529 def get_task_uuid():
530     return str(uuid.uuid4())
531
532
533 class TaskRelation(models.Model):
534     status = models.IntegerField(default=JobStatus.NEW)
535     job = models.ForeignKey(Job, on_delete=models.CASCADE)
536     config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
537     task_id = models.CharField(default=get_task_uuid, max_length=37)
538     lab_token = models.CharField(default="null", max_length=50)
539     message = models.TextField(default="")
540
541     def delete(self, *args, **kwargs):
542         self.config.delete()
543         return super(self.__class__, self).delete(*args, **kwargs)
544
545     def type_str(self):
546         return "Generic Task"
547
548     class Meta:
549         abstract = True
550
551
552 class AccessRelation(TaskRelation):
553     config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
554
555     def type_str(self):
556         return "Access Task"
557
558     def delete(self, *args, **kwargs):
559         self.config.delete()
560         return super(self.__class__, self).delete(*args, **kwargs)
561
562
563 class SoftwareRelation(TaskRelation):
564     config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
565
566     def type_str(self):
567         return "Software Configuration Task"
568
569     def delete(self, *args, **kwargs):
570         self.config.delete()
571         return super(self.__class__, self).delete(*args, **kwargs)
572
573
574 class HostHardwareRelation(TaskRelation):
575     host = models.ForeignKey(Host, on_delete=models.CASCADE)
576     config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
577
578     def type_str(self):
579         return "Hardware Configuration Task"
580
581     def get_delta(self):
582         return self.config.to_dict()
583
584     def delete(self, *args, **kwargs):
585         self.config.delete()
586         return super(self.__class__, self).delete(*args, **kwargs)
587
588
589 class HostNetworkRelation(TaskRelation):
590     host = models.ForeignKey(Host, on_delete=models.CASCADE)
591     config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
592
593     def type_str(self):
594         return "Network Configuration Task"
595
596     def delete(self, *args, **kwargs):
597         self.config.delete()
598         return super(self.__class__, self).delete(*args, **kwargs)
599
600
601 class JobFactory(object):
602
603     @classmethod
604     def makeCompleteJob(cls, booking):
605         hosts = Host.objects.filter(bundle=booking.resource)
606         job = None
607         try:
608             job = Job.objects.get(booking=booking)
609         except:
610             job = Job.objects.create(status=JobStatus.NEW, booking=booking)
611         cls.makeHardwareConfigs(
612                 hosts=hosts,
613                 job=job
614                 )
615         cls.makeNetworkConfigs(
616                 hosts=hosts,
617                 job=job
618                 )
619         cls.makeSoftware(
620                 hosts=hosts,
621                 job=job
622                 )
623         all_users = list(booking.collaborators.all())
624         all_users.append(booking.owner)
625         cls.makeAccessConfig(
626                 users=all_users,
627                 access_type="vpn",
628                 revoke=False,
629                 job=job
630                 )
631         for user in all_users:
632             try:
633                 cls.makeAccessConfig(
634                         users=[user],
635                         access_type="ssh",
636                         revoke=False,
637                         job=job,
638                         context={
639                             "key": user.userprofile.ssh_public_key.read(),
640                             "hosts": [host.labid for host in hosts]
641                             }
642                         )
643             except Exception:
644                 continue
645
646     @classmethod
647     def makeHardwareConfigs(cls, hosts=[], job=Job()):
648         for host in hosts:
649             hardware_config = None
650             try:
651                 hardware_config = HardwareConfig.objects.get(relation__host=host)
652             except:
653                 hardware_config = HardwareConfig()
654
655             relation = HostHardwareRelation()
656             relation.host = host
657             relation.job = job
658             relation.config = hardware_config
659             relation.config.save()
660             relation.config = relation.config
661             relation.save()
662
663             hardware_config.clear_delta()
664             hardware_config.set_image(host.config.image.lab_id)
665             hardware_config.set_hostname(host.template.resource.name)
666             hardware_config.set_power("on")
667             hardware_config.set_ipmi_create(True)
668             hardware_config.save()
669
670     @classmethod
671     def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
672         for user in users:
673             relation = AccessRelation()
674             relation.job = job
675             config = AccessConfig()
676             config.access_type = access_type
677             config.user = user
678             if context:
679                 config.set_context(context)
680             config.save()
681             relation.config = config
682             relation.save()
683             config.clear_delta()
684             config.set_access_type(access_type)
685             config.set_revoke(revoke)
686             config.set_user(user)
687             config.save()
688
689     @classmethod
690     def makeNetworkConfigs(cls, hosts=[], job=Job()):
691         for host in hosts:
692             network_config = None
693             try:
694                 network_config = NetworkConfig.objects.get(relation__host=host)
695             except:
696                 network_config = NetworkConfig.objects.create()
697
698             relation = HostNetworkRelation()
699             relation.host = host
700             relation.job = job
701             network_config.save()
702             relation.config = network_config
703             relation.save()
704             network_config.clear_delta()
705
706             for interface in host.interfaces.all():
707                 network_config.add_interface(interface)
708             network_config.save()
709
710     @classmethod
711     def makeSoftware(cls, hosts=[], job=Job()):
712         def init_config(host):
713             opnfv_config = OpnfvApiConfig()
714             if host is not None:
715                 opnfv = host.config.bundle.opnfv_config.first()
716                 opnfv_config.installer = opnfv.installer.name
717                 opnfv_config.scenario = opnfv.scenario.name
718             opnfv_config.save()
719             return opnfv_config
720
721         try:
722             host = None
723             if len(hosts) > 0:
724                 host = hosts[0]
725             opnfv_config = init_config(host)
726
727             for host in hosts:
728                 opnfv_config.roles.add(host)
729             software_config = SoftwareConfig.objects.create(opnfv=opnfv_config)
730             software_config.save()
731             software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
732             software_relation.save()
733             return software_relation
734         except:
735             return None