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
20 from booking.models import Booking
21 from resource_inventory.models import (
31 from resource_inventory.idf_templater import IDFTemplater
32 from resource_inventory.pdf_templater import PDFTemplater
35 class JobStatus(object):
42 class LabManagerTracker(object):
45 def get(cls, lab_name, token):
47 Takes in a lab name (from a url path)
48 returns a lab manager instance for that lab, if it exists
51 lab = Lab.objects.get(name=lab_name)
53 raise PermissionDenied("Lab not found")
54 if lab.api_token == token:
55 return LabManager(lab)
56 raise PermissionDenied("Lab not authorized")
59 class LabManager(object):
61 This is the class that will ultimately handle all REST calls to
63 handles jobs, inventory, status, etc
64 may need to create helper classes
67 def __init__(self, lab):
70 def update_host_remote_info(self, data, host_id):
71 host = get_object_or_404(Host, labid=host_id, lab=self.lab)
74 info['address'] = data['address']
75 info['mac_address'] = data['mac_address']
76 info['password'] = data['password']
77 info['user'] = data['user']
78 info['type'] = data['type']
79 info['versions'] = json.dumps(data['versions'])
80 except Exception as e:
81 return {"error": "invalid arguement: " + str(e)}
82 remote_info = host.remote_management
83 if "default" in remote_info.mac_address:
84 remote_info = RemoteInfo()
85 remote_info.address = info['address']
86 remote_info.mac_address = info['mac_address']
87 remote_info.password = info['password']
88 remote_info.user = info['user']
89 remote_info.type = info['type']
90 remote_info.versions = info['versions']
92 host.remote_management = remote_info
94 booking = Booking.objects.get(resource=host.bundle)
95 self.update_xdf(booking)
96 return {"status": "success"}
98 def update_xdf(self, booking):
99 booking.pdf = PDFTemplater.makePDF(booking)
100 booking.idf = IDFTemplater().makeIDF(booking)
103 def get_pdf(self, booking_id):
104 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
107 def get_idf(self, booking_id):
108 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
111 def get_profile(self):
113 prof['name'] = self.lab.name
115 "phone": self.lab.contact_phone,
116 "email": self.lab.contact_email
118 prof['host_count'] = []
119 for host in HostProfile.objects.filter(labs=self.lab):
120 count = Host.objects.filter(profile=host, lab=self.lab).count()
121 prof['host_count'].append(
129 def get_inventory(self):
131 hosts = Host.objects.filter(lab=self.lab)
132 images = Image.objects.filter(from_lab=self.lab)
133 profiles = HostProfile.objects.filter(labs=self.lab)
134 inventory['hosts'] = self.serialize_hosts(hosts)
135 inventory['images'] = self.serialize_images(images)
136 inventory['host_types'] = self.serialize_host_profiles(profiles)
139 def get_host(self, hostname):
140 host = get_object_or_404(Host, labid=hostname, lab=self.lab)
142 "booked": host.booked,
143 "working": host.working,
144 "type": host.profile.name
147 def update_host(self, hostname, data):
148 host = get_object_or_404(Host, labid=hostname, lab=self.lab)
149 if "working" in data:
150 working = data['working'] == "true"
151 host.working = working
153 return self.get_host(hostname)
155 def get_status(self):
156 return {"status": self.lab.status}
158 def set_status(self, payload):
161 def get_current_jobs(self):
162 jobs = Job.objects.filter(booking__lab=self.lab)
164 return self.serialize_jobs(jobs, status=JobStatus.CURRENT)
166 def get_new_jobs(self):
167 jobs = Job.objects.filter(booking__lab=self.lab)
169 return self.serialize_jobs(jobs, status=JobStatus.NEW)
171 def get_done_jobs(self):
172 jobs = Job.objects.filter(booking__lab=self.lab)
174 return self.serialize_jobs(jobs, status=JobStatus.DONE)
176 def get_job(self, jobid):
177 return Job.objects.get(pk=jobid).to_dict()
179 def update_job(self, jobid, data):
182 def serialize_jobs(self, jobs, status=JobStatus.NEW):
185 jsonized_job = job.get_delta(status)
186 if len(jsonized_job['payload']) < 1:
188 job_ser.append(jsonized_job)
192 def serialize_hosts(self, hosts):
197 h['hostname'] = host.name
198 h['host_type'] = host.profile.name
199 for iface in host.interfaces.all():
201 eth['mac'] = iface.mac_address
202 eth['busaddr'] = iface.bus_address
203 eth['name'] = iface.name
204 eth['switchport'] = {"switch_name": iface.switch_name, "port_name": iface.port_name}
205 h['interfaces'].append(eth)
208 def serialize_images(self, images):
214 "lab_id": image.lab_id,
215 "dashboard_id": image.id
220 def serialize_host_profiles(self, profiles):
222 for profile in profiles:
225 "cores": profile.cpuprofile.first().cores,
226 "arch": profile.cpuprofile.first().architecture,
227 "cpus": profile.cpuprofile.first().cpus,
230 for disk in profile.storageprofile.all():
233 "type": disk.media_type,
237 p['description'] = profile.description
239 for iface in profile.interfaceprofile.all():
240 p['interfaces'].append(
242 "speed": iface.speed,
247 p['ram'] = {"amount": profile.ramprofile.first().amount}
248 p['name'] = profile.name
249 profile_ser.append(p)
253 class Job(models.Model):
255 This is the class that is serialized and put into the api
257 booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
258 status = models.IntegerField(default=JobStatus.NEW)
259 complete = models.BooleanField(default=False)
265 for relation in AccessRelation.objects.filter(job=self):
266 if 'access' not in d:
268 d['access'][relation.task_id] = relation.config.to_dict()
269 for relation in SoftwareRelation.objects.filter(job=self):
270 if 'software' not in d:
272 d['software'][relation.task_id] = relation.config.to_dict()
273 for relation in HostHardwareRelation.objects.filter(job=self):
274 if 'hardware' not in d:
276 d['hardware'][relation.task_id] = relation.config.to_dict()
277 for relation in HostNetworkRelation.objects.filter(job=self):
278 if 'network' not in d:
280 d['network'][relation.task_id] = relation.config.to_dict()
281 for relation in SnapshotRelation.objects.filter(job=self):
282 if 'snapshot' not in d:
284 d['snapshot'][relation.task_id] = relation.config.to_dict()
290 def get_tasklist(self, status="all"):
293 HostHardwareRelation,
301 tasklist += list(cls.objects.filter(job=self))
304 tasklist += list(cls.objects.filter(job=self).filter(status=status))
307 def is_fulfilled(self):
309 This method should return true if all of the job's tasks are done,
312 my_tasks = self.get_tasklist()
313 for task in my_tasks:
314 if task.status != JobStatus.DONE:
318 def get_delta(self, status):
322 for relation in AccessRelation.objects.filter(job=self).filter(status=status):
323 if 'access' not in d:
325 d['access'][relation.task_id] = relation.config.get_delta()
326 for relation in SoftwareRelation.objects.filter(job=self).filter(status=status):
327 if 'software' not in d:
329 d['software'][relation.task_id] = relation.config.get_delta()
330 for relation in HostHardwareRelation.objects.filter(job=self).filter(status=status):
331 if 'hardware' not in d:
333 d['hardware'][relation.task_id] = relation.config.get_delta()
334 for relation in HostNetworkRelation.objects.filter(job=self).filter(status=status):
335 if 'network' not in d:
337 d['network'][relation.task_id] = relation.config.get_delta()
338 for relation in SnapshotRelation.objects.filter(job=self).filter(status=status):
339 if 'snapshot' not in d:
341 d['snapshot'][relation.task_id] = relation.config.get_delta()
347 return json.dumps(self.to_dict())
350 class TaskConfig(models.Model):
358 return json.dumps(self.to_dict())
360 def clear_delta(self):
364 class BridgeConfig(models.Model):
366 Displays mapping between jumphost interfaces and
369 interfaces = models.ManyToManyField(Interface)
370 opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
374 hid = self.interfaces.first().host.labid
376 for interface in self.interfaces.all():
377 d[hid][interface.mac_address] = []
378 for vlan in interface.config.all():
379 network_role = self.opnfv_model.networks().filter(network=vlan.network)
380 bridge = IDFTemplater.bridge_names[network_role.name]
382 "vlan_id": vlan.vlan_id,
383 "tagged": vlan.tagged,
386 d[hid][interface.mac_address].append(br_config)
390 return json.dumps(self.to_dict())
393 class OpnfvApiConfig(models.Model):
395 installer = models.CharField(max_length=200)
396 scenario = models.CharField(max_length=300)
397 roles = models.ManyToManyField(Host)
398 # pdf and idf are url endpoints, not the actual file
399 pdf = models.CharField(max_length=100)
400 idf = models.CharField(max_length=100)
401 bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True)
402 delta = models.TextField()
403 opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL)
407 if not self.opnfv_config:
410 d['installer'] = self.installer
412 d['scenario'] = self.scenario
417 if self.bridge_config:
418 d['bridged_interfaces'] = self.bridge_config.to_dict()
420 hosts = self.roles.all()
425 host.labid: self.opnfv_config.host_opnfv_config.get(
426 host_config__pk=host.config.pk
433 return json.dumps(self.to_dict())
435 def set_installer(self, installer):
436 self.installer = installer
437 d = json.loads(self.delta)
438 d['installer'] = installer
439 self.delta = json.dumps(d)
441 def set_scenario(self, scenario):
442 self.scenario = scenario
443 d = json.loads(self.delta)
444 d['scenario'] = scenario
445 self.delta = json.dumps(d)
447 def set_xdf(self, booking, update_delta=True):
448 kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id}
449 self.pdf = reverse('get-pdf', kwargs=kwargs)
450 self.idf = reverse('get-idf', kwargs=kwargs)
452 d = json.loads(self.delta)
455 self.delta = json.dumps(d)
457 def add_role(self, host):
459 d = json.loads(self.delta)
462 d['roles'].append({host.labid: host.config.opnfvRole.name})
463 self.delta = json.dumps(d)
465 def clear_delta(self):
470 self.delta = self.to_json()
472 return json.loads(self.delta)
475 class AccessConfig(TaskConfig):
476 access_type = models.CharField(max_length=50)
477 user = models.ForeignKey(User, on_delete=models.CASCADE)
478 revoke = models.BooleanField(default=False)
479 context = models.TextField(default="")
480 delta = models.TextField(default="{}")
484 d['access_type'] = self.access_type
485 d['user'] = self.user.id
486 d['revoke'] = self.revoke
488 d['context'] = json.loads(self.context)
495 self.delta = self.to_json()
497 d = json.loads(self.delta)
498 d["lab_token"] = self.accessrelation.lab_token
503 return json.dumps(self.to_dict())
505 def clear_delta(self):
507 d["lab_token"] = self.accessrelation.lab_token
508 self.delta = json.dumps(d)
510 def set_access_type(self, access_type):
511 self.access_type = access_type
512 d = json.loads(self.delta)
513 d['access_type'] = access_type
514 self.delta = json.dumps(d)
516 def set_user(self, user):
518 d = json.loads(self.delta)
519 d['user'] = self.user.id
520 self.delta = json.dumps(d)
522 def set_revoke(self, revoke):
524 d = json.loads(self.delta)
526 self.delta = json.dumps(d)
528 def set_context(self, context):
529 self.context = json.dumps(context)
530 d = json.loads(self.delta)
531 d['context'] = context
532 self.delta = json.dumps(d)
535 class SoftwareConfig(TaskConfig):
537 handled opnfv installations, etc
539 opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
544 d['opnfv'] = self.opnfv.to_dict()
546 d["lab_token"] = self.softwarerelation.lab_token
547 self.delta = json.dumps(d)
553 d['opnfv'] = self.opnfv.get_delta()
554 d['lab_token'] = self.softwarerelation.lab_token
558 def clear_delta(self):
559 self.opnfv.clear_delta()
562 return json.dumps(self.to_dict())
565 class HardwareConfig(TaskConfig):
567 handles imaging, user accounts, etc
569 image = models.CharField(max_length=100, default="defimage")
570 power = models.CharField(max_length=100, default="off")
571 hostname = models.CharField(max_length=100, default="hostname")
572 ipmi_create = models.BooleanField(default=False)
573 delta = models.TextField()
577 d['image'] = self.image
578 d['power'] = self.power
579 d['hostname'] = self.hostname
580 d['ipmi_create'] = str(self.ipmi_create)
581 d['id'] = self.hosthardwarerelation.host.labid
585 return json.dumps(self.to_dict())
589 self.delta = self.to_json()
591 d = json.loads(self.delta)
592 d['lab_token'] = self.hosthardwarerelation.lab_token
595 def clear_delta(self):
597 d["id"] = self.hosthardwarerelation.host.labid
598 d["lab_token"] = self.hosthardwarerelation.lab_token
599 self.delta = json.dumps(d)
601 def set_image(self, image):
603 d = json.loads(self.delta)
604 d['image'] = self.image
605 self.delta = json.dumps(d)
607 def set_power(self, power):
609 d = json.loads(self.delta)
611 self.delta = json.dumps(d)
613 def set_hostname(self, hostname):
614 self.hostname = hostname
615 d = json.loads(self.delta)
616 d['hostname'] = hostname
617 self.delta = json.dumps(d)
619 def set_ipmi_create(self, ipmi_create):
620 self.ipmi_create = ipmi_create
621 d = json.loads(self.delta)
622 d['ipmi_create'] = ipmi_create
623 self.delta = json.dumps(d)
626 class NetworkConfig(TaskConfig):
628 handles network configuration
630 interfaces = models.ManyToManyField(Interface)
631 delta = models.TextField()
635 hid = self.hostnetworkrelation.host.labid
637 for interface in self.interfaces.all():
638 d[hid][interface.mac_address] = []
639 for vlan in interface.config.all():
640 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
645 return json.dumps(self.to_dict())
649 self.delta = self.to_json()
651 d = json.loads(self.delta)
652 d['lab_token'] = self.hostnetworkrelation.lab_token
655 def clear_delta(self):
656 self.delta = json.dumps(self.to_dict())
659 def add_interface(self, interface):
660 self.interfaces.add(interface)
661 d = json.loads(self.delta)
662 hid = self.hostnetworkrelation.host.labid
665 d[hid][interface.mac_address] = []
666 for vlan in interface.config.all():
667 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
668 self.delta = json.dumps(d)
671 class SnapshotConfig(TaskConfig):
673 host = models.ForeignKey(Host, null=True, on_delete=models.DO_NOTHING)
674 image = models.IntegerField(null=True)
675 dashboard_id = models.IntegerField()
676 delta = models.TextField(default="{}")
681 d['host'] = self.host.labid
683 d['image'] = self.image
684 d['dashboard_id'] = self.dashboard_id
688 return json.dumps(self.to_dict())
692 self.delta = self.to_json()
695 d = json.loads(self.delta)
698 def clear_delta(self):
699 self.delta = json.dumps(self.to_dict())
702 def set_host(self, host):
704 d = json.loads(self.delta)
705 d['host'] = host.labid
706 self.delta = json.dumps(d)
708 def set_image(self, image):
710 d = json.loads(self.delta)
711 d['image'] = self.image
712 self.delta = json.dumps(d)
714 def clear_image(self):
716 d = json.loads(self.delta)
718 self.delta = json.dumps(d)
720 def set_dashboard_id(self, dash):
721 self.dashboard_id = dash
722 d = json.loads(self.delta)
723 d['dashboard_id'] = self.dashboard_id
724 self.delta = json.dumps(d)
727 def get_task(task_id):
728 for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]:
730 ret = taskclass.objects.get(task_id=task_id)
732 except taskclass.DoesNotExist:
734 from django.core.exceptions import ObjectDoesNotExist
735 raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
739 return str(uuid.uuid4())
742 class TaskRelation(models.Model):
743 status = models.IntegerField(default=JobStatus.NEW)
744 job = models.ForeignKey(Job, on_delete=models.CASCADE)
745 config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
746 task_id = models.CharField(default=get_task_uuid, max_length=37)
747 lab_token = models.CharField(default="null", max_length=50)
748 message = models.TextField(default="")
750 def delete(self, *args, **kwargs):
752 return super(self.__class__, self).delete(*args, **kwargs)
755 return "Generic Task"
761 class AccessRelation(TaskRelation):
762 config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
767 def delete(self, *args, **kwargs):
769 return super(self.__class__, self).delete(*args, **kwargs)
772 class SoftwareRelation(TaskRelation):
773 config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
776 return "Software Configuration Task"
778 def delete(self, *args, **kwargs):
780 return super(self.__class__, self).delete(*args, **kwargs)
783 class HostHardwareRelation(TaskRelation):
784 host = models.ForeignKey(Host, on_delete=models.CASCADE)
785 config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
788 return "Hardware Configuration Task"
791 return self.config.to_dict()
793 def delete(self, *args, **kwargs):
795 return super(self.__class__, self).delete(*args, **kwargs)
798 class HostNetworkRelation(TaskRelation):
799 host = models.ForeignKey(Host, on_delete=models.CASCADE)
800 config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
803 return "Network Configuration Task"
805 def delete(self, *args, **kwargs):
807 return super(self.__class__, self).delete(*args, **kwargs)
810 class SnapshotRelation(TaskRelation):
811 snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
812 config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
815 return "Snapshot Task"
818 return self.config.to_dict()
820 def delete(self, *args, **kwargs):
822 return super(self.__class__, self).delete(*args, **kwargs)
825 class JobFactory(object):
828 def reimageHost(cls, new_image, booking, host):
830 This method will make all necessary changes to make a lab
833 job = Job.objects.get(booking=booking)
834 # make hardware task new
835 hardware_relation = HostHardwareRelation.objects.get(host=host, job=job)
836 hardware_relation.config.set_image(new_image.lab_id)
837 hardware_relation.config.save()
838 hardware_relation.status = JobStatus.NEW
840 # re-apply networking after host is reset
841 net_relation = HostNetworkRelation.objects.get(host=host, job=job)
842 net_relation.status = JobStatus.NEW
844 # re-apply ssh access after host is reset
845 for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
846 relation.status = JobStatus.NEW
849 hardware_relation.save()
853 def makeSnapshotTask(cls, image, booking, host):
854 relation = SnapshotRelation()
855 job = Job.objects.get(booking=booking)
856 config = SnapshotConfig.objects.create(dashboard_id=image.id)
859 relation.config = config
860 relation.config.save()
861 relation.config = relation.config
862 relation.snapshot = image
866 config.set_host(host)
870 def makeCompleteJob(cls, booking):
871 hosts = Host.objects.filter(bundle=booking.resource)
874 job = Job.objects.get(booking=booking)
876 job = Job.objects.create(status=JobStatus.NEW, booking=booking)
877 cls.makeHardwareConfigs(
881 cls.makeNetworkConfigs(
889 all_users = list(booking.collaborators.all())
890 all_users.append(booking.owner)
891 cls.makeAccessConfig(
897 for user in all_users:
899 cls.makeAccessConfig(
905 "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
906 "hosts": [host.labid for host in hosts]
913 def makeHardwareConfigs(cls, hosts=[], job=Job()):
915 hardware_config = None
917 hardware_config = HardwareConfig.objects.get(relation__host=host)
919 hardware_config = HardwareConfig()
921 relation = HostHardwareRelation()
924 relation.config = hardware_config
925 relation.config.save()
926 relation.config = relation.config
929 hardware_config.clear_delta()
930 hardware_config.set_image(host.config.image.lab_id)
931 hardware_config.set_hostname(host.template.resource.name)
932 hardware_config.set_power("on")
933 hardware_config.set_ipmi_create(True)
934 hardware_config.save()
937 def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
939 relation = AccessRelation()
941 config = AccessConfig()
942 config.access_type = access_type
945 relation.config = config
949 config.set_context(context)
950 config.set_access_type(access_type)
951 config.set_revoke(revoke)
952 config.set_user(user)
956 def makeNetworkConfigs(cls, hosts=[], job=Job()):
958 network_config = None
960 network_config = NetworkConfig.objects.get(relation__host=host)
962 network_config = NetworkConfig.objects.create()
964 relation = HostNetworkRelation()
967 network_config.save()
968 relation.config = network_config
970 network_config.clear_delta()
972 for interface in host.interfaces.all():
973 network_config.add_interface(interface)
974 network_config.save()
977 def make_bridge_config(cls, booking):
978 if booking.resource.hosts.count() < 2:
981 jumphost_config = HostOPNFVConfig.objects.filter(
982 role__name__iexact="jumphost"
984 jumphost = Host.objects.get(
985 bundle=booking.resource,
986 config=jumphost_config.host_config
990 br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
991 for iface in jumphost.interfaces.all():
992 br_config.interfaces.add(iface)
996 def makeSoftware(cls, booking=None, job=Job()):
998 if not booking.opnfv_config:
1001 opnfv_api_config = OpnfvApiConfig.objects.create(
1002 opnfv_config=booking.opnfv_config,
1003 installer=booking.opnfv_config.installer.name,
1004 scenario=booking.opnfv_config.scenario.name,
1005 bridge_config=cls.make_bridge_config(booking)
1008 opnfv_api_config.set_xdf(booking, False)
1009 opnfv_api_config.save()
1011 for host in booking.resource.hosts.all():
1012 opnfv_api_config.roles.add(host)
1013 software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
1014 software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
1015 return software_relation