Fixes creation of ssh access job
[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         d['context'] = json.loads(self.context)
354         return d
355
356     def get_delta(self):
357         if not self.delta:
358             self.delta = self.to_json()
359             self.save()
360         d = json.loads(self.delta)
361         d["lab_token"] = self.accessrelation.lab_token
362
363         return d
364
365     def to_json(self):
366         return json.dumps(self.to_dict())
367
368     def clear_delta(self):
369         d = {}
370         d["lab_token"] = self.accessrelation.lab_token
371         self.delta = json.dumps(d)
372
373     def set_access_type(self, access_type):
374         self.access_type = access_type
375         d = json.loads(self.delta)
376         d['access_type'] = access_type
377         self.delta = json.dumps(d)
378
379     def set_user(self, user):
380         self.user = user
381         d = json.loads(self.delta)
382         d['user'] = self.user.id
383         self.delta = json.dumps(d)
384
385     def set_revoke(self, revoke):
386         self.revoke = revoke
387         d = json.loads(self.delta)
388         d['revoke'] = revoke
389         self.delta = json.dumps(d)
390
391     def set_context(self, context):
392         self.context = json.dumps(context)
393         d = json.loads(self.delta)
394         d['context'] = context
395         self.delta = json.dumps(d)
396
397
398 class SoftwareConfig(TaskConfig):
399     """
400     handled opnfv installations, etc
401     """
402     opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
403
404     def to_dict(self):
405         d = {}
406         if self.opnfv:
407             d['opnfv'] = self.opnfv.to_dict()
408
409         d["lab_token"] = self.softwarerelation.lab_token
410         self.delta = json.dumps(d)
411
412         return d
413
414     def get_delta(self):
415         d = {}
416         d['opnfv'] = self.opnfv.get_delta()
417         d['lab_token'] = self.softwarerelation.lab_token
418
419         return d
420
421     def clear_delta(self):
422         self.opnfv.clear_delta()
423
424     def to_json(self):
425         return json.dumps(self.to_dict())
426
427
428 class HardwareConfig(TaskConfig):
429     """
430     handles imaging, user accounts, etc
431     """
432     image = models.CharField(max_length=100, default="defimage")
433     power = models.CharField(max_length=100, default="off")
434     hostname = models.CharField(max_length=100, default="hostname")
435     ipmi_create = models.BooleanField(default=False)
436     delta = models.TextField()
437
438     def to_dict(self):
439         d = {}
440         d['image'] = self.image
441         d['power'] = self.power
442         d['hostname'] = self.hostname
443         d['ipmi_create'] = str(self.ipmi_create)
444         d['id'] = self.hosthardwarerelation.host.labid
445         return d
446
447     def to_json(self):
448         return json.dumps(self.to_dict())
449
450     def get_delta(self):
451         if not self.delta:
452             self.delta = self.to_json()
453             self.save()
454         d = json.loads(self.delta)
455         d['lab_token'] = self.hosthardwarerelation.lab_token
456         return d
457
458     def clear_delta(self):
459         d = {}
460         d["id"] = self.hosthardwarerelation.host.labid
461         d["lab_token"] = self.hosthardwarerelation.lab_token
462         self.delta = json.dumps(d)
463
464     def set_image(self, image):
465         self.image = image
466         d = json.loads(self.delta)
467         d['image'] = self.image
468         self.delta = json.dumps(d)
469
470     def set_power(self, power):
471         self.power = power
472         d = json.loads(self.delta)
473         d['power'] = power
474         self.delta = json.dumps(d)
475
476     def set_hostname(self, hostname):
477         self.hostname = hostname
478         d = json.loads(self.delta)
479         d['hostname'] = hostname
480         self.delta = json.dumps(d)
481
482     def set_ipmi_create(self, ipmi_create):
483         self.ipmi_create = ipmi_create
484         d = json.loads(self.delta)
485         d['ipmi_create'] = ipmi_create
486         self.delta = json.dumps(d)
487
488
489 class NetworkConfig(TaskConfig):
490     """
491     handles network configuration
492     """
493     interfaces = models.ManyToManyField(Interface)
494     delta = models.TextField()
495
496     def to_dict(self):
497         d = {}
498         hid = self.hostnetworkrelation.host.labid
499         d[hid] = {}
500         for interface in self.interfaces.all():
501             d[hid][interface.mac_address] = []
502             for vlan in interface.config.all():
503                 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
504
505         return d
506
507     def to_json(self):
508         return json.dumps(self.to_dict())
509
510     def get_delta(self):
511         if not self.delta:
512             self.delta = self.to_json()
513             self.save()
514         d = json.loads(self.delta)
515         d['lab_token'] = self.hostnetworkrelation.lab_token
516         return d
517
518     def clear_delta(self):
519         self.delta = json.dumps(self.to_dict())
520         self.save()
521
522     def add_interface(self, interface):
523         self.interfaces.add(interface)
524         d = json.loads(self.delta)
525         hid = self.hostnetworkrelation.host.labid
526         if hid not in d:
527             d[hid] = {}
528         d[hid][interface.mac_address] = []
529         for vlan in interface.config.all():
530             d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
531         self.delta = json.dumps(d)
532
533
534 def get_task(task_id):
535     for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]:
536         try:
537             ret = taskclass.objects.get(task_id=task_id)
538             return ret
539         except taskclass.DoesNotExist:
540             pass
541     from django.core.exceptions import ObjectDoesNotExist
542     raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
543
544
545 def get_task_uuid():
546     return str(uuid.uuid4())
547
548
549 class TaskRelation(models.Model):
550     status = models.IntegerField(default=JobStatus.NEW)
551     job = models.ForeignKey(Job, on_delete=models.CASCADE)
552     config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
553     task_id = models.CharField(default=get_task_uuid, max_length=37)
554     lab_token = models.CharField(default="null", max_length=50)
555     message = models.TextField(default="")
556
557     def delete(self, *args, **kwargs):
558         self.config.delete()
559         return super(self.__class__, self).delete(*args, **kwargs)
560
561     def type_str(self):
562         return "Generic Task"
563
564     class Meta:
565         abstract = True
566
567
568 class AccessRelation(TaskRelation):
569     config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
570
571     def type_str(self):
572         return "Access Task"
573
574     def delete(self, *args, **kwargs):
575         self.config.delete()
576         return super(self.__class__, self).delete(*args, **kwargs)
577
578
579 class SoftwareRelation(TaskRelation):
580     config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
581
582     def type_str(self):
583         return "Software Configuration Task"
584
585     def delete(self, *args, **kwargs):
586         self.config.delete()
587         return super(self.__class__, self).delete(*args, **kwargs)
588
589
590 class HostHardwareRelation(TaskRelation):
591     host = models.ForeignKey(Host, on_delete=models.CASCADE)
592     config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
593
594     def type_str(self):
595         return "Hardware Configuration Task"
596
597     def get_delta(self):
598         return self.config.to_dict()
599
600     def delete(self, *args, **kwargs):
601         self.config.delete()
602         return super(self.__class__, self).delete(*args, **kwargs)
603
604
605 class HostNetworkRelation(TaskRelation):
606     host = models.ForeignKey(Host, on_delete=models.CASCADE)
607     config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
608
609     def type_str(self):
610         return "Network Configuration Task"
611
612     def delete(self, *args, **kwargs):
613         self.config.delete()
614         return super(self.__class__, self).delete(*args, **kwargs)
615
616
617 class JobFactory(object):
618
619     @classmethod
620     def makeCompleteJob(cls, booking):
621         hosts = Host.objects.filter(bundle=booking.resource)
622         job = None
623         try:
624             job = Job.objects.get(booking=booking)
625         except:
626             job = Job.objects.create(status=JobStatus.NEW, booking=booking)
627         cls.makeHardwareConfigs(
628             hosts=hosts,
629             job=job
630         )
631         cls.makeNetworkConfigs(
632             hosts=hosts,
633             job=job
634         )
635         cls.makeSoftware(
636             hosts=hosts,
637             job=job
638         )
639         all_users = list(booking.collaborators.all())
640         all_users.append(booking.owner)
641         cls.makeAccessConfig(
642             users=all_users,
643             access_type="vpn",
644             revoke=False,
645             job=job
646         )
647         for user in all_users:
648             try:
649                 cls.makeAccessConfig(
650                     users=[user],
651                     access_type="ssh",
652                     revoke=False,
653                     job=job,
654                     context={
655                         "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
656                         "hosts": [host.labid for host in hosts]
657                     }
658                 )
659             except Exception:
660                 continue
661
662     @classmethod
663     def makeHardwareConfigs(cls, hosts=[], job=Job()):
664         for host in hosts:
665             hardware_config = None
666             try:
667                 hardware_config = HardwareConfig.objects.get(relation__host=host)
668             except:
669                 hardware_config = HardwareConfig()
670
671             relation = HostHardwareRelation()
672             relation.host = host
673             relation.job = job
674             relation.config = hardware_config
675             relation.config.save()
676             relation.config = relation.config
677             relation.save()
678
679             hardware_config.clear_delta()
680             hardware_config.set_image(host.config.image.lab_id)
681             hardware_config.set_hostname(host.template.resource.name)
682             hardware_config.set_power("on")
683             hardware_config.set_ipmi_create(True)
684             hardware_config.save()
685
686     @classmethod
687     def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
688         for user in users:
689             relation = AccessRelation()
690             relation.job = job
691             config = AccessConfig()
692             config.access_type = access_type
693             config.user = user
694             config.save()
695             relation.config = config
696             relation.save()
697             config.clear_delta()
698             if context:
699                 config.set_context(context)
700             config.set_access_type(access_type)
701             config.set_revoke(revoke)
702             config.set_user(user)
703             config.save()
704
705     @classmethod
706     def makeNetworkConfigs(cls, hosts=[], job=Job()):
707         for host in hosts:
708             network_config = None
709             try:
710                 network_config = NetworkConfig.objects.get(relation__host=host)
711             except:
712                 network_config = NetworkConfig.objects.create()
713
714             relation = HostNetworkRelation()
715             relation.host = host
716             relation.job = job
717             network_config.save()
718             relation.config = network_config
719             relation.save()
720             network_config.clear_delta()
721
722             for interface in host.interfaces.all():
723                 network_config.add_interface(interface)
724             network_config.save()
725
726     @classmethod
727     def makeSoftware(cls, hosts=[], job=Job()):
728         def init_config(host):
729             opnfv_config = OpnfvApiConfig()
730             if host is not None:
731                 opnfv = host.config.bundle.opnfv_config.first()
732                 opnfv_config.installer = opnfv.installer.name
733                 opnfv_config.scenario = opnfv.scenario.name
734             opnfv_config.save()
735             return opnfv_config
736
737         try:
738             host = None
739             if len(hosts) > 0:
740                 host = hosts[0]
741             opnfv_config = init_config(host)
742
743             for host in hosts:
744                 opnfv_config.roles.add(host)
745             software_config = SoftwareConfig.objects.create(opnfv=opnfv_config)
746             software_config.save()
747             software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
748             software_relation.save()
749             return software_relation
750         except:
751             return None