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 (
33 from resource_inventory.idf_templater import IDFTemplater
34 from resource_inventory.pdf_templater import PDFTemplater
35 from account.models import Downtime
38 class JobStatus(object):
45 class LabManagerTracker(object):
48 def get(cls, lab_name, token):
50 Takes in a lab name (from a url path)
51 returns a lab manager instance for that lab, if it exists
54 lab = Lab.objects.get(name=lab_name)
56 raise PermissionDenied("Lab not found")
57 if lab.api_token == token:
58 return LabManager(lab)
59 raise PermissionDenied("Lab not authorized")
62 class LabManager(object):
64 This is the class that will ultimately handle all REST calls to
66 handles jobs, inventory, status, etc
67 may need to create helper classes
70 def __init__(self, lab):
73 def get_downtime(self):
74 return Downtime.objects.filter(start__lt=timezone.now(), end__gt=timezone.now(), lab=self.lab)
76 def get_downtime_json(self):
77 downtime = self.get_downtime().first() # should only be one item in queryset
81 "start": downtime.start,
83 "description": downtime.description
85 return {"is_down": False}
87 def create_downtime(self, form):
89 takes in a dictionary that describes the model.
91 "start": utc timestamp
93 "description": human text (optional)
95 For timestamp structure, https://docs.djangoproject.com/en/2.2/ref/forms/fields/#datetimefield
97 Downtime.objects.create(
98 start=form.cleaned_data['start'],
99 end=form.cleaned_data['end'],
100 description=form.cleaned_data['description'],
103 return self.get_downtime_json()
105 def update_host_remote_info(self, data, host_id):
106 host = get_object_or_404(Host, labid=host_id, lab=self.lab)
109 info['address'] = data['address']
110 info['mac_address'] = data['mac_address']
111 info['password'] = data['password']
112 info['user'] = data['user']
113 info['type'] = data['type']
114 info['versions'] = json.dumps(data['versions'])
115 except Exception as e:
116 return {"error": "invalid arguement: " + str(e)}
117 remote_info = host.remote_management
118 if "default" in remote_info.mac_address:
119 remote_info = RemoteInfo()
120 remote_info.address = info['address']
121 remote_info.mac_address = info['mac_address']
122 remote_info.password = info['password']
123 remote_info.user = info['user']
124 remote_info.type = info['type']
125 remote_info.versions = info['versions']
127 host.remote_management = remote_info
129 booking = Booking.objects.get(resource=host.bundle)
130 self.update_xdf(booking)
131 return {"status": "success"}
133 def update_xdf(self, booking):
134 booking.pdf = PDFTemplater.makePDF(booking)
135 booking.idf = IDFTemplater().makeIDF(booking)
138 def get_pdf(self, booking_id):
139 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
142 def get_idf(self, booking_id):
143 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
146 def get_profile(self):
148 prof['name'] = self.lab.name
150 "phone": self.lab.contact_phone,
151 "email": self.lab.contact_email
153 prof['host_count'] = []
154 for host in HostProfile.objects.filter(labs=self.lab):
155 count = Host.objects.filter(profile=host, lab=self.lab).count()
156 prof['host_count'].append(
164 def get_inventory(self):
166 hosts = Host.objects.filter(lab=self.lab)
167 images = Image.objects.filter(from_lab=self.lab)
168 profiles = HostProfile.objects.filter(labs=self.lab)
169 inventory['hosts'] = self.serialize_hosts(hosts)
170 inventory['images'] = self.serialize_images(images)
171 inventory['host_types'] = self.serialize_host_profiles(profiles)
174 def get_host(self, hostname):
175 host = get_object_or_404(Host, labid=hostname, lab=self.lab)
177 "booked": host.booked,
178 "working": host.working,
179 "type": host.profile.name
182 def update_host(self, hostname, data):
183 host = get_object_or_404(Host, labid=hostname, lab=self.lab)
184 if "working" in data:
185 working = data['working'] == "true"
186 host.working = working
188 return self.get_host(hostname)
190 def get_status(self):
191 return {"status": self.lab.status}
193 def set_status(self, payload):
196 def get_current_jobs(self):
197 jobs = Job.objects.filter(booking__lab=self.lab)
199 return self.serialize_jobs(jobs, status=JobStatus.CURRENT)
201 def get_new_jobs(self):
202 jobs = Job.objects.filter(booking__lab=self.lab)
204 return self.serialize_jobs(jobs, status=JobStatus.NEW)
206 def get_done_jobs(self):
207 jobs = Job.objects.filter(booking__lab=self.lab)
209 return self.serialize_jobs(jobs, status=JobStatus.DONE)
211 def get_job(self, jobid):
212 return Job.objects.get(pk=jobid).to_dict()
214 def update_job(self, jobid, data):
217 def serialize_jobs(self, jobs, status=JobStatus.NEW):
220 jsonized_job = job.get_delta(status)
221 if len(jsonized_job['payload']) < 1:
223 job_ser.append(jsonized_job)
227 def serialize_hosts(self, hosts):
232 h['hostname'] = host.name
233 h['host_type'] = host.profile.name
234 for iface in host.interfaces.all():
236 eth['mac'] = iface.mac_address
237 eth['busaddr'] = iface.bus_address
238 eth['name'] = iface.name
239 eth['switchport'] = {"switch_name": iface.switch_name, "port_name": iface.port_name}
240 h['interfaces'].append(eth)
243 def serialize_images(self, images):
249 "lab_id": image.lab_id,
250 "dashboard_id": image.id
255 def serialize_host_profiles(self, profiles):
257 for profile in profiles:
260 "cores": profile.cpuprofile.first().cores,
261 "arch": profile.cpuprofile.first().architecture,
262 "cpus": profile.cpuprofile.first().cpus,
265 for disk in profile.storageprofile.all():
268 "type": disk.media_type,
272 p['description'] = profile.description
274 for iface in profile.interfaceprofile.all():
275 p['interfaces'].append(
277 "speed": iface.speed,
282 p['ram'] = {"amount": profile.ramprofile.first().amount}
283 p['name'] = profile.name
284 profile_ser.append(p)
288 class Job(models.Model):
290 This is the class that is serialized and put into the api
292 booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
293 status = models.IntegerField(default=JobStatus.NEW)
294 complete = models.BooleanField(default=False)
298 for relation in self.get_tasklist():
299 if relation.job_key not in d:
300 d[relation.job_key] = {}
301 d[relation.job_key][relation.task_id] = relation.config.to_dict()
303 return {"id": self.id, "payload": d}
305 def get_tasklist(self, status="all"):
308 HostHardwareRelation,
316 tasklist += list(cls.objects.filter(job=self))
319 tasklist += list(cls.objects.filter(job=self).filter(status=status))
322 def is_fulfilled(self):
324 This method should return true if all of the job's tasks are done,
327 my_tasks = self.get_tasklist()
328 for task in my_tasks:
329 if task.status != JobStatus.DONE:
333 def get_delta(self, status):
335 for relation in self.get_tasklist(status=status):
336 if relation.job_key not in d:
337 d[relation.job_key] = {}
338 d[relation.job_key][relation.task_id] = relation.config.get_delta()
340 return {"id": self.id, "payload": d}
343 return json.dumps(self.to_dict())
346 class TaskConfig(models.Model):
347 state = models.IntegerField(default=ConfigState.CLEAN)
349 keys = set() # TODO: This needs to be an instance variable, not a class variable
350 delta_keys_list = models.CharField(max_length=200, default="[]")
353 def delta_keys(self):
354 return list(set(json.loads(self.delta_keys_list)))
357 def delta_keys(self, keylist):
358 self.delta_keys_list = json.dumps(keylist)
361 raise NotImplementedError
364 raise NotImplementedError
366 def format_delta(self, config, token):
367 delta = {k: config[k] for k in self.delta_keys}
368 delta['lab_token'] = token
372 return json.dumps(self.to_dict())
374 def clear_delta(self):
377 def set(self, *args):
378 dkeys = self.delta_keys
382 self.delta_keys = dkeys
385 class BridgeConfig(models.Model):
387 Displays mapping between jumphost interfaces and
390 interfaces = models.ManyToManyField(Interface)
391 opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
395 hid = self.interfaces.first().host.labid
397 for interface in self.interfaces.all():
398 d[hid][interface.mac_address] = []
399 for vlan in interface.config.all():
400 network_role = self.opnfv_model.networks().filter(network=vlan.network)
401 bridge = IDFTemplater.bridge_names[network_role.name]
403 "vlan_id": vlan.vlan_id,
404 "tagged": vlan.tagged,
407 d[hid][interface.mac_address].append(br_config)
411 return json.dumps(self.to_dict())
414 class OpnfvApiConfig(models.Model):
416 installer = models.CharField(max_length=200)
417 scenario = models.CharField(max_length=300)
418 roles = models.ManyToManyField(Host)
419 # pdf and idf are url endpoints, not the actual file
420 pdf = models.CharField(max_length=100)
421 idf = models.CharField(max_length=100)
422 bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True)
423 delta = models.TextField()
424 opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL)
428 if not self.opnfv_config:
431 d['installer'] = self.installer
433 d['scenario'] = self.scenario
438 if self.bridge_config:
439 d['bridged_interfaces'] = self.bridge_config.to_dict()
441 hosts = self.roles.all()
446 host.labid: self.opnfv_config.host_opnfv_config.get(
447 host_config__pk=host.config.pk
454 return json.dumps(self.to_dict())
456 def set_installer(self, installer):
457 self.installer = installer
458 d = json.loads(self.delta)
459 d['installer'] = installer
460 self.delta = json.dumps(d)
462 def set_scenario(self, scenario):
463 self.scenario = scenario
464 d = json.loads(self.delta)
465 d['scenario'] = scenario
466 self.delta = json.dumps(d)
468 def set_xdf(self, booking, update_delta=True):
469 kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id}
470 self.pdf = reverse('get-pdf', kwargs=kwargs)
471 self.idf = reverse('get-idf', kwargs=kwargs)
473 d = json.loads(self.delta)
476 self.delta = json.dumps(d)
478 def add_role(self, host):
480 d = json.loads(self.delta)
483 d['roles'].append({host.labid: host.config.opnfvRole.name})
484 self.delta = json.dumps(d)
486 def clear_delta(self):
491 self.delta = self.to_json()
493 return json.loads(self.delta)
496 class AccessConfig(TaskConfig):
497 access_type = models.CharField(max_length=50)
498 user = models.ForeignKey(User, on_delete=models.CASCADE)
499 revoke = models.BooleanField(default=False)
500 context = models.TextField(default="")
501 delta = models.TextField(default="{}")
505 d['access_type'] = self.access_type
506 d['user'] = self.user.id
507 d['revoke'] = self.revoke
509 d['context'] = json.loads(self.context)
516 self.delta = self.to_json()
518 d = json.loads(self.delta)
519 d["lab_token"] = self.accessrelation.lab_token
524 return json.dumps(self.to_dict())
526 def clear_delta(self):
528 d["lab_token"] = self.accessrelation.lab_token
529 self.delta = json.dumps(d)
531 def set_access_type(self, access_type):
532 self.access_type = access_type
533 d = json.loads(self.delta)
534 d['access_type'] = access_type
535 self.delta = json.dumps(d)
537 def set_user(self, user):
539 d = json.loads(self.delta)
540 d['user'] = self.user.id
541 self.delta = json.dumps(d)
543 def set_revoke(self, revoke):
545 d = json.loads(self.delta)
547 self.delta = json.dumps(d)
549 def set_context(self, context):
550 self.context = json.dumps(context)
551 d = json.loads(self.delta)
552 d['context'] = context
553 self.delta = json.dumps(d)
556 class SoftwareConfig(TaskConfig):
558 handled opnfv installations, etc
560 opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
565 d['opnfv'] = self.opnfv.to_dict()
567 d["lab_token"] = self.softwarerelation.lab_token
568 self.delta = json.dumps(d)
574 d['opnfv'] = self.opnfv.get_delta()
575 d['lab_token'] = self.softwarerelation.lab_token
579 def clear_delta(self):
580 self.opnfv.clear_delta()
583 return json.dumps(self.to_dict())
586 class HardwareConfig(TaskConfig):
588 handles imaging, user accounts, etc
590 image = models.CharField(max_length=100, default="defimage")
591 power = models.CharField(max_length=100, default="off")
592 hostname = models.CharField(max_length=100, default="hostname")
593 ipmi_create = models.BooleanField(default=False)
594 delta = models.TextField()
596 keys = set(["id", "image", "power", "hostname", "ipmi_create"])
599 return self.format_delta(
600 self.hosthardwarerelation.host.get_configuration(self.state),
601 self.hosthardwarerelation.lab_token)
604 class NetworkConfig(TaskConfig):
606 handles network configuration
608 interfaces = models.ManyToManyField(Interface)
609 delta = models.TextField()
613 hid = self.hostnetworkrelation.host.labid
615 for interface in self.interfaces.all():
616 d[hid][interface.mac_address] = []
617 for vlan in interface.config.all():
618 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
623 return json.dumps(self.to_dict())
627 self.delta = self.to_json()
629 d = json.loads(self.delta)
630 d['lab_token'] = self.hostnetworkrelation.lab_token
633 def clear_delta(self):
634 self.delta = json.dumps(self.to_dict())
637 def add_interface(self, interface):
638 self.interfaces.add(interface)
639 d = json.loads(self.delta)
640 hid = self.hostnetworkrelation.host.labid
643 d[hid][interface.mac_address] = []
644 for vlan in interface.config.all():
645 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
646 self.delta = json.dumps(d)
649 class SnapshotConfig(TaskConfig):
651 host = models.ForeignKey(Host, null=True, on_delete=models.DO_NOTHING)
652 image = models.IntegerField(null=True)
653 dashboard_id = models.IntegerField()
654 delta = models.TextField(default="{}")
659 d['host'] = self.host.labid
661 d['image'] = self.image
662 d['dashboard_id'] = self.dashboard_id
666 return json.dumps(self.to_dict())
670 self.delta = self.to_json()
673 d = json.loads(self.delta)
676 def clear_delta(self):
677 self.delta = json.dumps(self.to_dict())
680 def set_host(self, host):
682 d = json.loads(self.delta)
683 d['host'] = host.labid
684 self.delta = json.dumps(d)
686 def set_image(self, image):
688 d = json.loads(self.delta)
689 d['image'] = self.image
690 self.delta = json.dumps(d)
692 def clear_image(self):
694 d = json.loads(self.delta)
696 self.delta = json.dumps(d)
698 def set_dashboard_id(self, dash):
699 self.dashboard_id = dash
700 d = json.loads(self.delta)
701 d['dashboard_id'] = self.dashboard_id
702 self.delta = json.dumps(d)
705 def get_task(task_id):
706 for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]:
708 ret = taskclass.objects.get(task_id=task_id)
710 except taskclass.DoesNotExist:
712 from django.core.exceptions import ObjectDoesNotExist
713 raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
717 return str(uuid.uuid4())
720 class TaskRelation(models.Model):
721 status = models.IntegerField(default=JobStatus.NEW)
722 job = models.ForeignKey(Job, on_delete=models.CASCADE)
723 config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
724 task_id = models.CharField(default=get_task_uuid, max_length=37)
725 lab_token = models.CharField(default="null", max_length=50)
726 message = models.TextField(default="")
730 def delete(self, *args, **kwargs):
732 return super(self.__class__, self).delete(*args, **kwargs)
735 return "Generic Task"
741 class AccessRelation(TaskRelation):
742 config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
748 def delete(self, *args, **kwargs):
750 return super(self.__class__, self).delete(*args, **kwargs)
753 class SoftwareRelation(TaskRelation):
754 config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
758 return "Software Configuration Task"
760 def delete(self, *args, **kwargs):
762 return super(self.__class__, self).delete(*args, **kwargs)
765 class HostHardwareRelation(TaskRelation):
766 host = models.ForeignKey(Host, on_delete=models.CASCADE)
767 config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
771 return "Hardware Configuration Task"
774 return self.config.to_dict()
776 def delete(self, *args, **kwargs):
778 return super(self.__class__, self).delete(*args, **kwargs)
781 class HostNetworkRelation(TaskRelation):
782 host = models.ForeignKey(Host, on_delete=models.CASCADE)
783 config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
787 return "Network Configuration Task"
789 def delete(self, *args, **kwargs):
791 return super(self.__class__, self).delete(*args, **kwargs)
794 class SnapshotRelation(TaskRelation):
795 snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
796 config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
800 return "Snapshot Task"
803 return self.config.to_dict()
805 def delete(self, *args, **kwargs):
807 return super(self.__class__, self).delete(*args, **kwargs)
810 class JobFactory(object):
813 def reimageHost(cls, new_image, booking, host):
815 This method will make all necessary changes to make a lab
818 job = Job.objects.get(booking=booking)
819 # make hardware task new
820 hardware_relation = HostHardwareRelation.objects.get(host=host, job=job)
821 hardware_relation.config.set_image(new_image.lab_id)
822 hardware_relation.config.save()
823 hardware_relation.status = JobStatus.NEW
825 # re-apply networking after host is reset
826 net_relation = HostNetworkRelation.objects.get(host=host, job=job)
827 net_relation.status = JobStatus.NEW
829 # re-apply ssh access after host is reset
830 for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
831 relation.status = JobStatus.NEW
834 hardware_relation.save()
838 def makeSnapshotTask(cls, image, booking, host):
839 relation = SnapshotRelation()
840 job = Job.objects.get(booking=booking)
841 config = SnapshotConfig.objects.create(dashboard_id=image.id)
844 relation.config = config
845 relation.config.save()
846 relation.config = relation.config
847 relation.snapshot = image
851 config.set_host(host)
855 def makeCompleteJob(cls, booking):
856 hosts = Host.objects.filter(bundle=booking.resource)
859 job = Job.objects.get(booking=booking)
861 job = Job.objects.create(status=JobStatus.NEW, booking=booking)
862 cls.makeHardwareConfigs(
866 cls.makeNetworkConfigs(
874 all_users = list(booking.collaborators.all())
875 all_users.append(booking.owner)
876 cls.makeAccessConfig(
882 for user in all_users:
884 cls.makeAccessConfig(
890 "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
891 "hosts": [host.labid for host in hosts]
898 def makeHardwareConfigs(cls, hosts=[], job=Job()):
900 hardware_config = None
902 hardware_config = HardwareConfig.objects.get(relation__host=host)
904 hardware_config = HardwareConfig()
906 relation = HostHardwareRelation()
909 relation.config = hardware_config
910 relation.config.save()
911 relation.config = relation.config
914 hardware_config.set("image", "hostname", "power", "ipmi_create")
915 hardware_config.save()
918 def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
920 relation = AccessRelation()
922 config = AccessConfig()
923 config.access_type = access_type
926 relation.config = config
930 config.set_context(context)
931 config.set_access_type(access_type)
932 config.set_revoke(revoke)
933 config.set_user(user)
937 def makeNetworkConfigs(cls, hosts=[], job=Job()):
939 network_config = None
941 network_config = NetworkConfig.objects.get(relation__host=host)
943 network_config = NetworkConfig.objects.create()
945 relation = HostNetworkRelation()
948 network_config.save()
949 relation.config = network_config
951 network_config.clear_delta()
953 for interface in host.interfaces.all():
954 network_config.add_interface(interface)
955 network_config.save()
958 def make_bridge_config(cls, booking):
959 if booking.resource.hosts.count() < 2:
962 jumphost_config = HostOPNFVConfig.objects.filter(
963 role__name__iexact="jumphost"
965 jumphost = Host.objects.get(
966 bundle=booking.resource,
967 config=jumphost_config.host_config
971 br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
972 for iface in jumphost.interfaces.all():
973 br_config.interfaces.add(iface)
977 def makeSoftware(cls, booking=None, job=Job()):
979 if not booking.opnfv_config:
982 opnfv_api_config = OpnfvApiConfig.objects.create(
983 opnfv_config=booking.opnfv_config,
984 installer=booking.opnfv_config.installer.name,
985 scenario=booking.opnfv_config.scenario.name,
986 bridge_config=cls.make_bridge_config(booking)
989 opnfv_api_config.set_xdf(booking, False)
990 opnfv_api_config.save()
992 for host in booking.resource.hosts.all():
993 opnfv_api_config.roles.add(host)
994 software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
995 software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
996 return software_relation