1 ##############################################################################
2 # Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
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 ##############################################################################
11 from django.contrib.auth.models import User
12 from django.db import models
13 from django.core.exceptions import PermissionDenied
14 from django.shortcuts import get_object_or_404
15 from django.urls import reverse
16 from django.utils import timezone
21 from booking.models import Booking
22 from resource_inventory.models import (
32 from resource_inventory.idf_templater import IDFTemplater
33 from resource_inventory.pdf_templater import PDFTemplater
34 from account.models import Downtime
37 class JobStatus(object):
44 class LabManagerTracker(object):
47 def get(cls, lab_name, token):
49 Takes in a lab name (from a url path)
50 returns a lab manager instance for that lab, if it exists
53 lab = Lab.objects.get(name=lab_name)
55 raise PermissionDenied("Lab not found")
56 if lab.api_token == token:
57 return LabManager(lab)
58 raise PermissionDenied("Lab not authorized")
61 class LabManager(object):
63 This is the class that will ultimately handle all REST calls to
65 handles jobs, inventory, status, etc
66 may need to create helper classes
69 def __init__(self, lab):
72 def get_downtime(self):
73 return Downtime.objects.filter(start__lt=timezone.now(), end__gt=timezone.now(), lab=self.lab)
75 def get_downtime_json(self):
76 downtime = self.get_downtime().first() # should only be one item in queryset
80 "start": downtime.start,
82 "description": downtime.description
84 return {"is_down": False}
86 def create_downtime(self, form):
88 takes in a dictionary that describes the model.
90 "start": utc timestamp
92 "description": human text (optional)
94 For timestamp structure, https://docs.djangoproject.com/en/2.2/ref/forms/fields/#datetimefield
96 Downtime.objects.create(
97 start=form.cleaned_data['start'],
98 end=form.cleaned_data['end'],
99 description=form.cleaned_data['description'],
102 return self.get_downtime_json()
104 def update_host_remote_info(self, data, host_id):
105 host = get_object_or_404(Host, labid=host_id, lab=self.lab)
108 info['address'] = data['address']
109 info['mac_address'] = data['mac_address']
110 info['password'] = data['password']
111 info['user'] = data['user']
112 info['type'] = data['type']
113 info['versions'] = json.dumps(data['versions'])
114 except Exception as e:
115 return {"error": "invalid arguement: " + str(e)}
116 remote_info = host.remote_management
117 if "default" in remote_info.mac_address:
118 remote_info = RemoteInfo()
119 remote_info.address = info['address']
120 remote_info.mac_address = info['mac_address']
121 remote_info.password = info['password']
122 remote_info.user = info['user']
123 remote_info.type = info['type']
124 remote_info.versions = info['versions']
126 host.remote_management = remote_info
128 booking = Booking.objects.get(resource=host.bundle)
129 self.update_xdf(booking)
130 return {"status": "success"}
132 def update_xdf(self, booking):
133 booking.pdf = PDFTemplater.makePDF(booking)
134 booking.idf = IDFTemplater().makeIDF(booking)
137 def get_pdf(self, booking_id):
138 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
141 def get_idf(self, booking_id):
142 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
145 def get_profile(self):
147 prof['name'] = self.lab.name
149 "phone": self.lab.contact_phone,
150 "email": self.lab.contact_email
152 prof['host_count'] = []
153 for host in HostProfile.objects.filter(labs=self.lab):
154 count = Host.objects.filter(profile=host, lab=self.lab).count()
155 prof['host_count'].append(
163 def get_inventory(self):
165 hosts = Host.objects.filter(lab=self.lab)
166 images = Image.objects.filter(from_lab=self.lab)
167 profiles = HostProfile.objects.filter(labs=self.lab)
168 inventory['hosts'] = self.serialize_hosts(hosts)
169 inventory['images'] = self.serialize_images(images)
170 inventory['host_types'] = self.serialize_host_profiles(profiles)
173 def get_host(self, hostname):
174 host = get_object_or_404(Host, labid=hostname, lab=self.lab)
176 "booked": host.booked,
177 "working": host.working,
178 "type": host.profile.name
181 def update_host(self, hostname, data):
182 host = get_object_or_404(Host, labid=hostname, lab=self.lab)
183 if "working" in data:
184 working = data['working'] == "true"
185 host.working = working
187 return self.get_host(hostname)
189 def get_status(self):
190 return {"status": self.lab.status}
192 def set_status(self, payload):
195 def get_current_jobs(self):
196 jobs = Job.objects.filter(booking__lab=self.lab)
198 return self.serialize_jobs(jobs, status=JobStatus.CURRENT)
200 def get_new_jobs(self):
201 jobs = Job.objects.filter(booking__lab=self.lab)
203 return self.serialize_jobs(jobs, status=JobStatus.NEW)
205 def get_done_jobs(self):
206 jobs = Job.objects.filter(booking__lab=self.lab)
208 return self.serialize_jobs(jobs, status=JobStatus.DONE)
210 def get_job(self, jobid):
211 return Job.objects.get(pk=jobid).to_dict()
213 def update_job(self, jobid, data):
216 def serialize_jobs(self, jobs, status=JobStatus.NEW):
219 jsonized_job = job.get_delta(status)
220 if len(jsonized_job['payload']) < 1:
222 job_ser.append(jsonized_job)
226 def serialize_hosts(self, hosts):
231 h['hostname'] = host.name
232 h['host_type'] = host.profile.name
233 for iface in host.interfaces.all():
235 eth['mac'] = iface.mac_address
236 eth['busaddr'] = iface.bus_address
237 eth['name'] = iface.name
238 eth['switchport'] = {"switch_name": iface.switch_name, "port_name": iface.port_name}
239 h['interfaces'].append(eth)
242 def serialize_images(self, images):
248 "lab_id": image.lab_id,
249 "dashboard_id": image.id
254 def serialize_host_profiles(self, profiles):
256 for profile in profiles:
259 "cores": profile.cpuprofile.first().cores,
260 "arch": profile.cpuprofile.first().architecture,
261 "cpus": profile.cpuprofile.first().cpus,
264 for disk in profile.storageprofile.all():
267 "type": disk.media_type,
271 p['description'] = profile.description
273 for iface in profile.interfaceprofile.all():
274 p['interfaces'].append(
276 "speed": iface.speed,
281 p['ram'] = {"amount": profile.ramprofile.first().amount}
282 p['name'] = profile.name
283 profile_ser.append(p)
287 class Job(models.Model):
289 This is the class that is serialized and put into the api
291 booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
292 status = models.IntegerField(default=JobStatus.NEW)
293 complete = models.BooleanField(default=False)
299 for relation in AccessRelation.objects.filter(job=self):
300 if 'access' not in d:
302 d['access'][relation.task_id] = relation.config.to_dict()
303 for relation in SoftwareRelation.objects.filter(job=self):
304 if 'software' not in d:
306 d['software'][relation.task_id] = relation.config.to_dict()
307 for relation in HostHardwareRelation.objects.filter(job=self):
308 if 'hardware' not in d:
310 d['hardware'][relation.task_id] = relation.config.to_dict()
311 for relation in HostNetworkRelation.objects.filter(job=self):
312 if 'network' not in d:
314 d['network'][relation.task_id] = relation.config.to_dict()
315 for relation in SnapshotRelation.objects.filter(job=self):
316 if 'snapshot' not in d:
318 d['snapshot'][relation.task_id] = relation.config.to_dict()
324 def get_tasklist(self, status="all"):
327 HostHardwareRelation,
335 tasklist += list(cls.objects.filter(job=self))
338 tasklist += list(cls.objects.filter(job=self).filter(status=status))
341 def is_fulfilled(self):
343 This method should return true if all of the job's tasks are done,
346 my_tasks = self.get_tasklist()
347 for task in my_tasks:
348 if task.status != JobStatus.DONE:
352 def get_delta(self, status):
356 for relation in AccessRelation.objects.filter(job=self).filter(status=status):
357 if 'access' not in d:
359 d['access'][relation.task_id] = relation.config.get_delta()
360 for relation in SoftwareRelation.objects.filter(job=self).filter(status=status):
361 if 'software' not in d:
363 d['software'][relation.task_id] = relation.config.get_delta()
364 for relation in HostHardwareRelation.objects.filter(job=self).filter(status=status):
365 if 'hardware' not in d:
367 d['hardware'][relation.task_id] = relation.config.get_delta()
368 for relation in HostNetworkRelation.objects.filter(job=self).filter(status=status):
369 if 'network' not in d:
371 d['network'][relation.task_id] = relation.config.get_delta()
372 for relation in SnapshotRelation.objects.filter(job=self).filter(status=status):
373 if 'snapshot' not in d:
375 d['snapshot'][relation.task_id] = relation.config.get_delta()
381 return json.dumps(self.to_dict())
384 class TaskConfig(models.Model):
392 return json.dumps(self.to_dict())
394 def clear_delta(self):
398 class BridgeConfig(models.Model):
400 Displays mapping between jumphost interfaces and
403 interfaces = models.ManyToManyField(Interface)
404 opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
408 hid = self.interfaces.first().host.labid
410 for interface in self.interfaces.all():
411 d[hid][interface.mac_address] = []
412 for vlan in interface.config.all():
413 network_role = self.opnfv_model.networks().filter(network=vlan.network)
414 bridge = IDFTemplater.bridge_names[network_role.name]
416 "vlan_id": vlan.vlan_id,
417 "tagged": vlan.tagged,
420 d[hid][interface.mac_address].append(br_config)
424 return json.dumps(self.to_dict())
427 class OpnfvApiConfig(models.Model):
429 installer = models.CharField(max_length=200)
430 scenario = models.CharField(max_length=300)
431 roles = models.ManyToManyField(Host)
432 # pdf and idf are url endpoints, not the actual file
433 pdf = models.CharField(max_length=100)
434 idf = models.CharField(max_length=100)
435 bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True)
436 delta = models.TextField()
437 opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL)
441 if not self.opnfv_config:
444 d['installer'] = self.installer
446 d['scenario'] = self.scenario
451 if self.bridge_config:
452 d['bridged_interfaces'] = self.bridge_config.to_dict()
454 hosts = self.roles.all()
459 host.labid: self.opnfv_config.host_opnfv_config.get(
460 host_config__pk=host.config.pk
467 return json.dumps(self.to_dict())
469 def set_installer(self, installer):
470 self.installer = installer
471 d = json.loads(self.delta)
472 d['installer'] = installer
473 self.delta = json.dumps(d)
475 def set_scenario(self, scenario):
476 self.scenario = scenario
477 d = json.loads(self.delta)
478 d['scenario'] = scenario
479 self.delta = json.dumps(d)
481 def set_xdf(self, booking, update_delta=True):
482 kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id}
483 self.pdf = reverse('get-pdf', kwargs=kwargs)
484 self.idf = reverse('get-idf', kwargs=kwargs)
486 d = json.loads(self.delta)
489 self.delta = json.dumps(d)
491 def add_role(self, host):
493 d = json.loads(self.delta)
496 d['roles'].append({host.labid: host.config.opnfvRole.name})
497 self.delta = json.dumps(d)
499 def clear_delta(self):
504 self.delta = self.to_json()
506 return json.loads(self.delta)
509 class AccessConfig(TaskConfig):
510 access_type = models.CharField(max_length=50)
511 user = models.ForeignKey(User, on_delete=models.CASCADE)
512 revoke = models.BooleanField(default=False)
513 context = models.TextField(default="")
514 delta = models.TextField(default="{}")
518 d['access_type'] = self.access_type
519 d['user'] = self.user.id
520 d['revoke'] = self.revoke
522 d['context'] = json.loads(self.context)
529 self.delta = self.to_json()
531 d = json.loads(self.delta)
532 d["lab_token"] = self.accessrelation.lab_token
537 return json.dumps(self.to_dict())
539 def clear_delta(self):
541 d["lab_token"] = self.accessrelation.lab_token
542 self.delta = json.dumps(d)
544 def set_access_type(self, access_type):
545 self.access_type = access_type
546 d = json.loads(self.delta)
547 d['access_type'] = access_type
548 self.delta = json.dumps(d)
550 def set_user(self, user):
552 d = json.loads(self.delta)
553 d['user'] = self.user.id
554 self.delta = json.dumps(d)
556 def set_revoke(self, revoke):
558 d = json.loads(self.delta)
560 self.delta = json.dumps(d)
562 def set_context(self, context):
563 self.context = json.dumps(context)
564 d = json.loads(self.delta)
565 d['context'] = context
566 self.delta = json.dumps(d)
569 class SoftwareConfig(TaskConfig):
571 handled opnfv installations, etc
573 opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
578 d['opnfv'] = self.opnfv.to_dict()
580 d["lab_token"] = self.softwarerelation.lab_token
581 self.delta = json.dumps(d)
587 d['opnfv'] = self.opnfv.get_delta()
588 d['lab_token'] = self.softwarerelation.lab_token
592 def clear_delta(self):
593 self.opnfv.clear_delta()
596 return json.dumps(self.to_dict())
599 class HardwareConfig(TaskConfig):
601 handles imaging, user accounts, etc
603 image = models.CharField(max_length=100, default="defimage")
604 power = models.CharField(max_length=100, default="off")
605 hostname = models.CharField(max_length=100, default="hostname")
606 ipmi_create = models.BooleanField(default=False)
607 delta = models.TextField()
611 d['image'] = self.image
612 d['power'] = self.power
613 d['hostname'] = self.hostname
614 d['ipmi_create'] = str(self.ipmi_create)
615 d['id'] = self.hosthardwarerelation.host.labid
619 return json.dumps(self.to_dict())
623 self.delta = self.to_json()
625 d = json.loads(self.delta)
626 d['lab_token'] = self.hosthardwarerelation.lab_token
629 def clear_delta(self):
631 d["id"] = self.hosthardwarerelation.host.labid
632 d["lab_token"] = self.hosthardwarerelation.lab_token
633 self.delta = json.dumps(d)
635 def set_image(self, image):
637 d = json.loads(self.delta)
638 d['image'] = self.image
639 self.delta = json.dumps(d)
641 def set_power(self, power):
643 d = json.loads(self.delta)
645 self.delta = json.dumps(d)
647 def set_hostname(self, hostname):
648 self.hostname = hostname
649 d = json.loads(self.delta)
650 d['hostname'] = hostname
651 self.delta = json.dumps(d)
653 def set_ipmi_create(self, ipmi_create):
654 self.ipmi_create = ipmi_create
655 d = json.loads(self.delta)
656 d['ipmi_create'] = ipmi_create
657 self.delta = json.dumps(d)
660 class NetworkConfig(TaskConfig):
662 handles network configuration
664 interfaces = models.ManyToManyField(Interface)
665 delta = models.TextField()
669 hid = self.hostnetworkrelation.host.labid
671 for interface in self.interfaces.all():
672 d[hid][interface.mac_address] = []
673 for vlan in interface.config.all():
674 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
679 return json.dumps(self.to_dict())
683 self.delta = self.to_json()
685 d = json.loads(self.delta)
686 d['lab_token'] = self.hostnetworkrelation.lab_token
689 def clear_delta(self):
690 self.delta = json.dumps(self.to_dict())
693 def add_interface(self, interface):
694 self.interfaces.add(interface)
695 d = json.loads(self.delta)
696 hid = self.hostnetworkrelation.host.labid
699 d[hid][interface.mac_address] = []
700 for vlan in interface.config.all():
701 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
702 self.delta = json.dumps(d)
705 class SnapshotConfig(TaskConfig):
707 host = models.ForeignKey(Host, null=True, on_delete=models.DO_NOTHING)
708 image = models.IntegerField(null=True)
709 dashboard_id = models.IntegerField()
710 delta = models.TextField(default="{}")
715 d['host'] = self.host.labid
717 d['image'] = self.image
718 d['dashboard_id'] = self.dashboard_id
722 return json.dumps(self.to_dict())
726 self.delta = self.to_json()
729 d = json.loads(self.delta)
732 def clear_delta(self):
733 self.delta = json.dumps(self.to_dict())
736 def set_host(self, host):
738 d = json.loads(self.delta)
739 d['host'] = host.labid
740 self.delta = json.dumps(d)
742 def set_image(self, image):
744 d = json.loads(self.delta)
745 d['image'] = self.image
746 self.delta = json.dumps(d)
748 def clear_image(self):
750 d = json.loads(self.delta)
752 self.delta = json.dumps(d)
754 def set_dashboard_id(self, dash):
755 self.dashboard_id = dash
756 d = json.loads(self.delta)
757 d['dashboard_id'] = self.dashboard_id
758 self.delta = json.dumps(d)
761 def get_task(task_id):
762 for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]:
764 ret = taskclass.objects.get(task_id=task_id)
766 except taskclass.DoesNotExist:
768 from django.core.exceptions import ObjectDoesNotExist
769 raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
773 return str(uuid.uuid4())
776 class TaskRelation(models.Model):
777 status = models.IntegerField(default=JobStatus.NEW)
778 job = models.ForeignKey(Job, on_delete=models.CASCADE)
779 config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
780 task_id = models.CharField(default=get_task_uuid, max_length=37)
781 lab_token = models.CharField(default="null", max_length=50)
782 message = models.TextField(default="")
784 def delete(self, *args, **kwargs):
786 return super(self.__class__, self).delete(*args, **kwargs)
789 return "Generic Task"
795 class AccessRelation(TaskRelation):
796 config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
801 def delete(self, *args, **kwargs):
803 return super(self.__class__, self).delete(*args, **kwargs)
806 class SoftwareRelation(TaskRelation):
807 config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
810 return "Software Configuration Task"
812 def delete(self, *args, **kwargs):
814 return super(self.__class__, self).delete(*args, **kwargs)
817 class HostHardwareRelation(TaskRelation):
818 host = models.ForeignKey(Host, on_delete=models.CASCADE)
819 config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
822 return "Hardware Configuration Task"
825 return self.config.to_dict()
827 def delete(self, *args, **kwargs):
829 return super(self.__class__, self).delete(*args, **kwargs)
832 class HostNetworkRelation(TaskRelation):
833 host = models.ForeignKey(Host, on_delete=models.CASCADE)
834 config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
837 return "Network Configuration Task"
839 def delete(self, *args, **kwargs):
841 return super(self.__class__, self).delete(*args, **kwargs)
844 class SnapshotRelation(TaskRelation):
845 snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
846 config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
849 return "Snapshot Task"
852 return self.config.to_dict()
854 def delete(self, *args, **kwargs):
856 return super(self.__class__, self).delete(*args, **kwargs)
859 class JobFactory(object):
862 def reimageHost(cls, new_image, booking, host):
864 This method will make all necessary changes to make a lab
867 job = Job.objects.get(booking=booking)
868 # make hardware task new
869 hardware_relation = HostHardwareRelation.objects.get(host=host, job=job)
870 hardware_relation.config.set_image(new_image.lab_id)
871 hardware_relation.config.save()
872 hardware_relation.status = JobStatus.NEW
874 # re-apply networking after host is reset
875 net_relation = HostNetworkRelation.objects.get(host=host, job=job)
876 net_relation.status = JobStatus.NEW
878 # re-apply ssh access after host is reset
879 for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
880 relation.status = JobStatus.NEW
883 hardware_relation.save()
887 def makeSnapshotTask(cls, image, booking, host):
888 relation = SnapshotRelation()
889 job = Job.objects.get(booking=booking)
890 config = SnapshotConfig.objects.create(dashboard_id=image.id)
893 relation.config = config
894 relation.config.save()
895 relation.config = relation.config
896 relation.snapshot = image
900 config.set_host(host)
904 def makeCompleteJob(cls, booking):
905 hosts = Host.objects.filter(bundle=booking.resource)
908 job = Job.objects.get(booking=booking)
910 job = Job.objects.create(status=JobStatus.NEW, booking=booking)
911 cls.makeHardwareConfigs(
915 cls.makeNetworkConfigs(
923 all_users = list(booking.collaborators.all())
924 all_users.append(booking.owner)
925 cls.makeAccessConfig(
931 for user in all_users:
933 cls.makeAccessConfig(
939 "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
940 "hosts": [host.labid for host in hosts]
947 def makeHardwareConfigs(cls, hosts=[], job=Job()):
949 hardware_config = None
951 hardware_config = HardwareConfig.objects.get(relation__host=host)
953 hardware_config = HardwareConfig()
955 relation = HostHardwareRelation()
958 relation.config = hardware_config
959 relation.config.save()
960 relation.config = relation.config
963 hardware_config.clear_delta()
964 hardware_config.set_image(host.config.image.lab_id)
965 hardware_config.set_hostname(host.template.resource.name)
966 hardware_config.set_power("on")
967 hardware_config.set_ipmi_create(True)
968 hardware_config.save()
971 def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
973 relation = AccessRelation()
975 config = AccessConfig()
976 config.access_type = access_type
979 relation.config = config
983 config.set_context(context)
984 config.set_access_type(access_type)
985 config.set_revoke(revoke)
986 config.set_user(user)
990 def makeNetworkConfigs(cls, hosts=[], job=Job()):
992 network_config = None
994 network_config = NetworkConfig.objects.get(relation__host=host)
996 network_config = NetworkConfig.objects.create()
998 relation = HostNetworkRelation()
1001 network_config.save()
1002 relation.config = network_config
1004 network_config.clear_delta()
1006 for interface in host.interfaces.all():
1007 network_config.add_interface(interface)
1008 network_config.save()
1011 def make_bridge_config(cls, booking):
1012 if booking.resource.hosts.count() < 2:
1015 jumphost_config = HostOPNFVConfig.objects.filter(
1016 role__name__iexact="jumphost"
1018 jumphost = Host.objects.get(
1019 bundle=booking.resource,
1020 config=jumphost_config.host_config
1024 br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
1025 for iface in jumphost.interfaces.all():
1026 br_config.interfaces.add(iface)
1030 def makeSoftware(cls, booking=None, job=Job()):
1032 if not booking.opnfv_config:
1035 opnfv_api_config = OpnfvApiConfig.objects.create(
1036 opnfv_config=booking.opnfv_config,
1037 installer=booking.opnfv_config.installer.name,
1038 scenario=booking.opnfv_config.scenario.name,
1039 bridge_config=cls.make_bridge_config(booking)
1042 opnfv_api_config.set_xdf(booking, False)
1043 opnfv_api_config.save()
1045 for host in booking.resource.hosts.all():
1046 opnfv_api_config.roles.add(host)
1047 software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
1048 software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
1049 return software_relation