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, ValidationError
14 from django.shortcuts import get_object_or_404
15 from django.contrib.postgres.fields import JSONField
16 from django.http import HttpResponseNotFound
17 from django.urls import reverse
18 from django.utils import timezone
25 from booking.models import Booking
26 from resource_inventory.models import (
37 ResourceConfiguration,
40 from resource_inventory.idf_templater import IDFTemplater
41 from resource_inventory.pdf_templater import PDFTemplater
42 from account.models import Downtime, UserProfile
43 from dashboard.utils import AbstractModelQuery
48 A poor man's enum for a job's status.
50 A job is NEW if it has not been started or recognized by the Lab
51 A job is CURRENT if it has been started by the lab but it is not yet completed
52 a job is DONE if all the tasks are complete and the booking is ready to use
61 class LabManagerTracker:
64 def get(cls, lab_name, token):
68 Takes in a lab name (from a url path)
69 returns a lab manager instance for that lab, if it exists
70 Also checks that the given API token is correct
73 lab = Lab.objects.get(name=lab_name)
75 raise PermissionDenied("Lab not found")
76 if lab.api_token == token:
77 return LabManager(lab)
78 raise PermissionDenied("Lab not authorized")
83 Handles all lab REST calls.
85 handles jobs, inventory, status, etc
86 may need to create helper classes
89 def __init__(self, lab):
93 return Opsys.objects.filter(from_lab=self.lab)
96 return Image.objects.filter(from_lab=self.lab)
98 def get_image(self, image_id):
99 return Image.objects.filter(from_lab=self.lab, lab_id=image_id)
101 def get_opsys(self, opsys_id):
102 return Opsys.objects.filter(from_lab=self.lab, lab_id=opsys_id)
104 def get_downtime(self):
105 return Downtime.objects.filter(start__lt=timezone.now(), end__gt=timezone.now(), lab=self.lab)
107 def get_downtime_json(self):
108 downtime = self.get_downtime().first() # should only be one item in queryset
112 "start": downtime.start,
114 "description": downtime.description
116 return {"is_down": False}
118 def create_downtime(self, form):
120 Create a downtime event.
122 Takes in a dictionary that describes the model.
124 "start": utc timestamp
126 "description": human text (optional)
128 For timestamp structure, https://docs.djangoproject.com/en/2.2/ref/forms/fields/#datetimefield
130 Downtime.objects.create(
131 start=form.cleaned_data['start'],
132 end=form.cleaned_data['end'],
133 description=form.cleaned_data['description'],
136 return self.get_downtime_json()
138 def update_host_remote_info(self, data, res_id):
139 resource = ResourceQuery.filter(labid=res_id, lab=self.lab)
140 if len(resource) != 1:
141 return HttpResponseNotFound("Could not find single host with id " + str(res_id))
142 resource = resource[0]
145 info['address'] = data['address']
146 info['mac_address'] = data['mac_address']
147 info['password'] = data['password']
148 info['user'] = data['user']
149 info['type'] = data['type']
150 info['versions'] = json.dumps(data['versions'])
151 except Exception as e:
152 return {"error": "invalid arguement: " + str(e)}
153 remote_info = resource.remote_management
154 if "default" in remote_info.mac_address:
155 remote_info = RemoteInfo()
156 remote_info.address = info['address']
157 remote_info.mac_address = info['mac_address']
158 remote_info.password = info['password']
159 remote_info.user = info['user']
160 remote_info.type = info['type']
161 remote_info.versions = info['versions']
163 resource.remote_management = remote_info
165 booking = Booking.objects.get(resource=resource.bundle)
166 self.update_xdf(booking)
167 return {"status": "success"}
169 def update_xdf(self, booking):
170 booking.pdf = PDFTemplater.makePDF(booking)
171 booking.idf = IDFTemplater().makeIDF(booking)
174 def get_pdf(self, booking_id):
175 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
178 def get_idf(self, booking_id):
179 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
182 def get_profile(self):
184 prof['name'] = self.lab.name
186 "phone": self.lab.contact_phone,
187 "email": self.lab.contact_email
189 prof['host_count'] = [{
190 "type": profile.name,
191 "count": len(profile.get_resources(lab=self.lab))}
192 for profile in ResourceProfile.objects.filter(labs=self.lab)]
195 def format_user(self, userprofile):
197 "id": userprofile.user.id,
198 "username": userprofile.user.username,
199 "email": userprofile.email_addr,
200 "first_name": userprofile.user.first_name,
201 "last_name": userprofile.user.last_name,
202 "company": userprofile.company
206 userlist = [self.format_user(profile) for profile in UserProfile.objects.select_related("user").all()]
208 return json.dumps({"users": userlist})
210 def get_user(self, user_id):
211 user = User.objects.get(pk=user_id)
213 profile = get_object_or_404(UserProfile, user=user)
215 return json.dumps(self.format_user(profile))
217 def get_inventory(self):
219 resources = ResourceQuery.filter(lab=self.lab)
220 images = Image.objects.filter(from_lab=self.lab)
221 profiles = ResourceProfile.objects.filter(labs=self.lab)
222 inventory['resources'] = self.serialize_resources(resources)
223 inventory['images'] = self.serialize_images(images)
224 inventory['host_types'] = self.serialize_host_profiles(profiles)
227 def get_host(self, hostname):
228 resource = ResourceQuery.filter(labid=hostname, lab=self.lab)
229 if len(resource) != 1:
230 return HttpResponseNotFound("Could not find single host with id " + str(hostname))
231 resource = resource[0]
233 "booked": resource.booked,
234 "working": resource.working,
235 "type": resource.profile.name
238 def update_host(self, hostname, data):
239 resource = ResourceQuery.filter(labid=hostname, lab=self.lab)
240 if len(resource) != 1:
241 return HttpResponseNotFound("Could not find single host with id " + str(hostname))
242 resource = resource[0]
243 if "working" in data:
244 working = data['working'] == "true"
245 resource.working = working
247 return self.get_host(hostname)
249 def get_status(self):
250 return {"status": self.lab.status}
252 def set_status(self, payload):
255 def get_current_jobs(self):
256 jobs = Job.objects.filter(booking__lab=self.lab)
258 return self.serialize_jobs(jobs, status=JobStatus.CURRENT)
260 def get_new_jobs(self):
261 jobs = Job.objects.filter(booking__lab=self.lab)
263 return self.serialize_jobs(jobs, status=JobStatus.NEW)
265 def get_done_jobs(self):
266 jobs = Job.objects.filter(booking__lab=self.lab)
268 return self.serialize_jobs(jobs, status=JobStatus.DONE)
270 def get_analytics_job(self):
271 """ Get analytics job with status new """
272 jobs = Job.objects.filter(
273 booking__lab=self.lab,
277 return self.serialize_jobs(jobs, status=JobStatus.NEW)
279 def get_job(self, jobid):
280 return Job.objects.get(pk=jobid).to_dict()
282 def update_job(self, jobid, data):
285 def serialize_jobs(self, jobs, status=JobStatus.NEW):
288 jsonized_job = job.get_delta(status)
289 if len(jsonized_job['payload']) < 1:
291 job_ser.append(jsonized_job)
295 def serialize_resources(self, resources):
296 # TODO: rewrite for Resource model
298 for res in resources:
301 'hostname': res.name,
302 'host_type': res.profile.name
304 for iface in res.get_interfaces():
305 r['interfaces'].append({
306 'mac': iface.mac_address,
307 'busaddr': iface.bus_address,
309 'switchport': {"switch_name": iface.switch_name, "port_name": iface.port_name}
313 def serialize_images(self, images):
319 "lab_id": image.lab_id,
320 "dashboard_id": image.id
325 def serialize_resource_profiles(self, profiles):
327 for profile in profiles:
330 "cores": profile.cpuprofile.first().cores,
331 "arch": profile.cpuprofile.first().architecture,
332 "cpus": profile.cpuprofile.first().cpus,
335 for disk in profile.storageprofile.all():
338 "type": disk.media_type,
342 p['description'] = profile.description
344 for iface in profile.interfaceprofile.all():
345 p['interfaces'].append(
347 "speed": iface.speed,
352 p['ram'] = {"amount": profile.ramprofile.first().amount}
353 p['name'] = profile.name
354 profile_ser.append(p)
358 class GeneratedCloudConfig(models.Model):
359 resource_id = models.CharField(max_length=200)
360 booking = models.ForeignKey(Booking, on_delete=models.CASCADE)
361 rconfig = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE)
362 text = models.TextField(null=True, blank=True)
364 def _normalize_username(self, username: str) -> str:
365 # TODO: make usernames posix compliant
366 s = re.sub(r'\W+', '', username)
369 def _get_ssh_string(self, username: str) -> str:
370 user = User.objects.get(username=username)
371 uprofile = user.userprofile
373 ssh_file = uprofile.ssh_public_key
375 escaped_file = ssh_file.open().read().decode(encoding="UTF-8").replace("\n", " ")
379 def _serialize_users(self):
381 returns the dictionary to be placed behind the `users` field of the toplevel c-i dict
383 # conserves distro default user
384 user_array = ["default"]
386 users = list(self.booking.collaborators.all())
387 users.append(self.booking.owner)
388 for collaborator in users:
391 # TODO: validate if usernames are valid as linux usernames (and provide an override potentially)
392 userdict['name'] = self._normalize_username(collaborator.username)
394 userdict['groups'] = "sudo"
395 userdict['sudo'] = "ALL=(ALL) NOPASSWD:ALL"
397 userdict['ssh_authorized_keys'] = [self._get_ssh_string(collaborator.username)]
399 user_array.append(userdict)
401 # user_array.append({
403 # "passwd": "$6$k54L.vim1cLaEc4$5AyUIrufGlbtVBzuCWOlA1yV6QdD7Gr2MzwIs/WhuYR9ebSfh3Qlb7djkqzjwjxpnSAonK1YOabPP6NxUDccu.",
404 # "ssh_redirect_user": True,
405 # "sudo": "ALL=(ALL) NOPASSWD:ALL",
411 # TODO: make this configurable
412 def _serialize_sysinfo(self):
414 defuser['name'] = 'opnfv'
415 defuser['plain_text_passwd'] = 'OPNFV_HOST'
416 defuser['home'] = '/home/opnfv'
417 defuser['shell'] = '/bin/bash'
418 defuser['lock_passwd'] = True
419 defuser['gecos'] = 'Lab Manager User'
420 defuser['groups'] = 'sudo'
422 return {'default_user': defuser}
424 # TODO: make this configurable
425 def _serialize_runcmds(self):
428 # have hosts run dhcp on boot
429 cmdlist.append(['sudo', 'dhclient', '-r'])
430 cmdlist.append(['sudo', 'dhclient'])
434 def _serialize_netconf_v1(self):
435 # interfaces = {} # map from iface_name => dhcp_config
436 # vlans = {} # map from vlan_id => dhcp_config
440 for interface in self._resource().interfaces.all():
441 interface_name = interface.profile.name
442 interface_mac = interface.mac_address
446 "name": interface_name,
447 "mac_address": interface_mac,
450 for vlan in interface.config.all():
452 vlan_dict_entry = {'type': 'vlan'}
453 vlan_dict_entry['name'] = str(interface_name) + "." + str(vlan.vlan_id)
454 vlan_dict_entry['vlan_link'] = str(interface_name)
455 vlan_dict_entry['vlan_id'] = int(vlan.vlan_id)
456 vlan_dict_entry['mac_address'] = str(interface_mac)
458 vlan_dict_entry["subnets"] = [{"type": "dhcp"}]
459 config_arr.append(vlan_dict_entry)
460 if (not vlan.tagged) and vlan.public:
461 iface_dict_entry["subnets"] = [{"type": "dhcp"}]
463 # vlan_dict_entry['mtu'] = # TODO, determine override MTU if needed
465 config_arr.append(iface_dict_entry)
468 'type': 'nameserver',
469 'address': ['10.64.0.1', '8.8.8.8']
472 config_arr.append(ns_dict)
474 full_dict = {'version': 1, 'config': config_arr}
479 def get(cls, booking_id: int, resource_lab_id: str, file_id: int):
480 return GeneratedCloudConfig.objects.get(resource_id=resource_lab_id, booking__id=booking_id, file_id=file_id)
483 return ResourceQuery.get(labid=self.resource_id, lab=self.booking.lab)
485 # def _get_facts(self):
486 # resource = self._resource()
488 # hostname = self.rconfig.name
489 # iface_configs = for_config.interface_configs.all()
494 main_dict['users'] = self._serialize_users()
495 main_dict['network'] = self._serialize_netconf_v1()
496 main_dict['hostname'] = self.rconfig.name
498 # add first startup commands
499 main_dict['runcmd'] = self._serialize_runcmds()
501 # configure distro default user
502 main_dict['system_info'] = self._serialize_sysinfo()
506 def serialize(self) -> str:
507 return yaml.dump(self._to_dict(), width=float("inf"))
510 class APILog(models.Model):
511 user = models.ForeignKey(User, on_delete=models.PROTECT)
512 call_time = models.DateTimeField(auto_now=True)
513 method = models.CharField(null=True, max_length=6)
514 endpoint = models.CharField(null=True, max_length=300)
515 ip_addr = models.GenericIPAddressField(protocol="both", null=True, unpack_ipv4=False)
516 body = JSONField(null=True)
519 return "Call to {} at {} by {}".format(
526 class AutomationAPIManager:
528 def serialize_booking(booking):
530 sbook['id'] = booking.pk
531 sbook['owner'] = booking.owner.username
532 sbook['collaborators'] = [user.username for user in booking.collaborators.all()]
533 sbook['start'] = booking.start
534 sbook['end'] = booking.end
535 sbook['lab'] = AutomationAPIManager.serialize_lab(booking.lab)
536 sbook['purpose'] = booking.purpose
537 sbook['resourceBundle'] = AutomationAPIManager.serialize_bundle(booking.resource)
541 def serialize_lab(lab):
544 slab['name'] = lab.name
548 def serialize_bundle(bundle):
550 sbundle['id'] = bundle.pk
551 sbundle['resources'] = [
552 AutomationAPIManager.serialize_server(server)
553 for server in bundle.get_resources()]
557 def serialize_server(server):
559 sserver['id'] = server.pk
560 sserver['name'] = server.name
564 def serialize_resource_profile(profile):
566 sprofile['id'] = profile.pk
567 sprofile['name'] = profile.name
571 def serialize_template(rec_temp_and_count):
572 template = rec_temp_and_count[0]
573 count = rec_temp_and_count[1]
576 stemplate['id'] = template.pk
577 stemplate['name'] = template.name
578 stemplate['count_available'] = count
579 stemplate['resourceProfiles'] = [
580 AutomationAPIManager.serialize_resource_profile(config.profile)
581 for config in template.getConfigs()
586 def serialize_image(image):
588 simage['id'] = image.pk
589 simage['name'] = image.name
593 def serialize_userprofile(up):
596 sup['username'] = up.user.username
600 class Job(models.Model):
602 A Job to be performed by the Lab.
604 The API uses Jobs and Tasks to communicate actions that need to be taken to the Lab
605 that is hosting a booking. A booking from a user has an associated Job which tells
606 the lab how to configure the hardware, networking, etc to fulfill the booking
608 This is the class that is serialized and put into the api
613 ('DATA', 'Analytics')
616 booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
617 status = models.IntegerField(default=JobStatus.NEW)
618 complete = models.BooleanField(default=False)
619 job_type = models.CharField(
627 for relation in self.get_tasklist():
628 if relation.job_key not in d:
629 d[relation.job_key] = {}
630 d[relation.job_key][relation.task_id] = relation.config.to_dict()
632 return {"id": self.id, "payload": d}
634 def get_tasklist(self, status="all"):
636 return JobTaskQuery.filter(job=self, status=status)
637 return JobTaskQuery.filter(job=self)
639 def is_fulfilled(self):
641 If a job has been completed by the lab.
643 This method should return true if all of the job's tasks are done,
646 my_tasks = self.get_tasklist()
647 for task in my_tasks:
648 if task.status != JobStatus.DONE:
652 def get_delta(self, status):
654 for relation in self.get_tasklist(status=status):
655 if relation.job_key not in d:
656 d[relation.job_key] = {}
657 d[relation.job_key][relation.task_id] = relation.config.get_delta()
659 return {"id": self.id, "payload": d}
662 return json.dumps(self.to_dict())
665 class TaskConfig(models.Model):
666 state = models.IntegerField(default=ConfigState.NEW)
668 keys = set() # TODO: This needs to be an instance variable, not a class variable
669 delta_keys_list = models.CharField(max_length=200, default="[]")
672 def delta_keys(self):
673 return list(set(json.loads(self.delta_keys_list)))
676 def delta_keys(self, keylist):
677 self.delta_keys_list = json.dumps(keylist)
680 raise NotImplementedError
683 raise NotImplementedError
685 def format_delta(self, config, token):
686 delta = {k: config[k] for k in self.delta_keys}
687 delta['lab_token'] = token
691 return json.dumps(self.to_dict())
693 def clear_delta(self):
696 def set(self, *args):
697 dkeys = self.delta_keys
701 self.delta_keys = dkeys
704 class BridgeConfig(models.Model):
705 """Displays mapping between jumphost interfaces and bridges."""
707 interfaces = models.ManyToManyField(Interface)
708 opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
712 hid = ResourceQuery.get(interface__pk=self.interfaces.first().pk).labid
714 for interface in self.interfaces.all():
715 d[hid][interface.mac_address] = []
716 for vlan in interface.config.all():
717 network_role = self.opnfv_model.networks().filter(network=vlan.network)
718 bridge = IDFTemplater.bridge_names[network_role.name]
720 "vlan_id": vlan.vlan_id,
721 "tagged": vlan.tagged,
724 d[hid][interface.mac_address].append(br_config)
728 return json.dumps(self.to_dict())
731 class ActiveUsersConfig(models.Model):
733 Task for getting active VPN users
735 StackStorm needs no information to run this job
736 so this task is very bare, but neccessary to fit
737 job creation convention.
740 def clear_delta(self):
744 return json.loads(self.to_json())
747 return json.dumps(self.to_dict())
753 class OpnfvApiConfig(models.Model):
755 installer = models.CharField(max_length=200)
756 scenario = models.CharField(max_length=300)
757 roles = models.ManyToManyField(ResourceOPNFVConfig)
758 # pdf and idf are url endpoints, not the actual file
759 pdf = models.CharField(max_length=100)
760 idf = models.CharField(max_length=100)
761 bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True)
762 delta = models.TextField()
763 opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL)
767 if not self.opnfv_config:
770 d['installer'] = self.installer
772 d['scenario'] = self.scenario
777 if self.bridge_config:
778 d['bridged_interfaces'] = self.bridge_config.to_dict()
780 hosts = self.roles.all()
785 host.labid: self.opnfv_config.host_opnfv_config.get(
786 host_config__pk=host.config.pk
793 return json.dumps(self.to_dict())
795 def set_installer(self, installer):
796 self.installer = installer
797 d = json.loads(self.delta)
798 d['installer'] = installer
799 self.delta = json.dumps(d)
801 def set_scenario(self, scenario):
802 self.scenario = scenario
803 d = json.loads(self.delta)
804 d['scenario'] = scenario
805 self.delta = json.dumps(d)
807 def set_xdf(self, booking, update_delta=True):
808 kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id}
809 self.pdf = reverse('get-pdf', kwargs=kwargs)
810 self.idf = reverse('get-idf', kwargs=kwargs)
812 d = json.loads(self.delta)
815 self.delta = json.dumps(d)
817 def add_role(self, host):
819 d = json.loads(self.delta)
822 d['roles'].append({host.labid: host.config.opnfvRole.name})
823 self.delta = json.dumps(d)
825 def clear_delta(self):
829 return json.loads(self.to_json())
832 class AccessConfig(TaskConfig):
833 access_type = models.CharField(max_length=50)
834 user = models.ForeignKey(User, on_delete=models.CASCADE)
835 revoke = models.BooleanField(default=False)
836 context = models.TextField(default="")
837 delta = models.TextField(default="{}")
841 d['access_type'] = self.access_type
842 d['user'] = self.user.id
843 d['revoke'] = self.revoke
845 d['context'] = json.loads(self.context)
851 d = json.loads(self.to_json())
852 d["lab_token"] = self.accessrelation.lab_token
857 return json.dumps(self.to_dict())
859 def clear_delta(self):
861 d["lab_token"] = self.accessrelation.lab_token
862 self.delta = json.dumps(d)
864 def set_access_type(self, access_type):
865 self.access_type = access_type
866 d = json.loads(self.delta)
867 d['access_type'] = access_type
868 self.delta = json.dumps(d)
870 def set_user(self, user):
872 d = json.loads(self.delta)
873 d['user'] = self.user.id
874 self.delta = json.dumps(d)
876 def set_revoke(self, revoke):
878 d = json.loads(self.delta)
880 self.delta = json.dumps(d)
882 def set_context(self, context):
883 self.context = json.dumps(context)
884 d = json.loads(self.delta)
885 d['context'] = context
886 self.delta = json.dumps(d)
889 class SoftwareConfig(TaskConfig):
890 """Handles software installations, such as OPNFV or ONAP."""
892 opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
897 d['opnfv'] = self.opnfv.to_dict()
899 d["lab_token"] = self.softwarerelation.lab_token
900 self.delta = json.dumps(d)
906 d['opnfv'] = self.opnfv.get_delta()
907 d['lab_token'] = self.softwarerelation.lab_token
911 def clear_delta(self):
912 self.opnfv.clear_delta()
915 return json.dumps(self.to_dict())
918 class HardwareConfig(TaskConfig):
919 """Describes the desired configuration of the hardware."""
921 image = models.CharField(max_length=100, default="defimage")
922 power = models.CharField(max_length=100, default="off")
923 hostname = models.CharField(max_length=100, default="hostname")
924 ipmi_create = models.BooleanField(default=False)
925 delta = models.TextField()
927 keys = set(["id", "image", "power", "hostname", "ipmi_create"])
930 return self.get_delta()
933 # TODO: grab the GeneratedCloudConfig urls from self.hosthardwarerelation.get_resource()
934 return self.format_delta(
935 self.hosthardwarerelation.get_resource().get_configuration(self.state),
936 self.hosthardwarerelation.lab_token)
939 class NetworkConfig(TaskConfig):
940 """Handles network configuration."""
942 interfaces = models.ManyToManyField(Interface)
943 delta = models.TextField()
947 hid = self.hostnetworkrelation.resource_id
949 for interface in self.interfaces.all():
950 d[hid][interface.mac_address] = []
951 if self.state != ConfigState.CLEAN:
952 for vlan in interface.config.all():
953 # TODO: should this come from the interface?
954 # e.g. will different interfaces for different resources need different configs?
955 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
960 return json.dumps(self.to_dict())
963 d = json.loads(self.to_json())
964 d['lab_token'] = self.hostnetworkrelation.lab_token
967 def clear_delta(self):
968 self.delta = json.dumps(self.to_dict())
971 def add_interface(self, interface):
972 self.interfaces.add(interface)
973 d = json.loads(self.delta)
974 hid = self.hostnetworkrelation.resource_id
977 d[hid][interface.mac_address] = []
978 for vlan in interface.config.all():
979 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
980 self.delta = json.dumps(d)
983 class SnapshotConfig(TaskConfig):
985 resource_id = models.CharField(max_length=200, default="default_id")
986 image = models.CharField(max_length=200, null=True) # cobbler ID
987 dashboard_id = models.IntegerField()
988 delta = models.TextField(default="{}")
993 d['host'] = self.host.labid
995 d['image'] = self.image
996 d['dashboard_id'] = self.dashboard_id
1000 return json.dumps(self.to_dict())
1002 def get_delta(self):
1003 d = json.loads(self.to_json())
1006 def clear_delta(self):
1007 self.delta = json.dumps(self.to_dict())
1010 def set_host(self, host):
1012 d = json.loads(self.delta)
1013 d['host'] = host.labid
1014 self.delta = json.dumps(d)
1016 def set_image(self, image):
1018 d = json.loads(self.delta)
1019 d['image'] = self.image
1020 self.delta = json.dumps(d)
1022 def clear_image(self):
1024 d = json.loads(self.delta)
1025 d.pop("image", None)
1026 self.delta = json.dumps(d)
1028 def set_dashboard_id(self, dash):
1029 self.dashboard_id = dash
1030 d = json.loads(self.delta)
1031 d['dashboard_id'] = self.dashboard_id
1032 self.delta = json.dumps(d)
1034 def save(self, *args, **kwargs):
1035 if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
1036 raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
1037 super().save(*args, **kwargs)
1040 def get_task(task_id):
1041 for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]:
1043 ret = taskclass.objects.get(task_id=task_id)
1045 except taskclass.DoesNotExist:
1047 from django.core.exceptions import ObjectDoesNotExist
1048 raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
1051 def get_task_uuid():
1052 return str(uuid.uuid4())
1055 class TaskRelation(models.Model):
1057 Relates a Job to a TaskConfig.
1059 superclass that relates a Job to tasks anc maintains information
1060 like status and messages from the lab
1063 status = models.IntegerField(default=JobStatus.NEW)
1064 job = models.ForeignKey(Job, on_delete=models.CASCADE)
1065 config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
1066 task_id = models.CharField(default=get_task_uuid, max_length=37)
1067 lab_token = models.CharField(default="null", max_length=50)
1068 message = models.TextField(default="")
1072 def delete(self, *args, **kwargs):
1073 self.config.delete()
1074 return super(self.__class__, self).delete(*args, **kwargs)
1077 return "Generic Task"
1083 class AccessRelation(TaskRelation):
1084 config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
1088 return "Access Task"
1090 def delete(self, *args, **kwargs):
1091 self.config.delete()
1092 return super(self.__class__, self).delete(*args, **kwargs)
1095 class SoftwareRelation(TaskRelation):
1096 config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
1097 job_key = "software"
1100 return "Software Configuration Task"
1102 def delete(self, *args, **kwargs):
1103 self.config.delete()
1104 return super(self.__class__, self).delete(*args, **kwargs)
1107 class HostHardwareRelation(TaskRelation):
1108 resource_id = models.CharField(max_length=200, default="default_id")
1109 config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
1110 job_key = "hardware"
1113 return "Hardware Configuration Task"
1115 def get_delta(self):
1116 return self.config.to_dict()
1118 def delete(self, *args, **kwargs):
1119 self.config.delete()
1120 return super(self.__class__, self).delete(*args, **kwargs)
1122 def save(self, *args, **kwargs):
1123 if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
1124 raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
1125 super().save(*args, **kwargs)
1127 def get_resource(self):
1128 return ResourceQuery.get(labid=self.resource_id)
1131 class HostNetworkRelation(TaskRelation):
1132 resource_id = models.CharField(max_length=200, default="default_id")
1133 config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
1137 return "Network Configuration Task"
1139 def delete(self, *args, **kwargs):
1140 self.config.delete()
1141 return super(self.__class__, self).delete(*args, **kwargs)
1143 def save(self, *args, **kwargs):
1144 if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
1145 raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
1146 super().save(*args, **kwargs)
1148 def get_resource(self):
1149 return ResourceQuery.get(labid=self.resource_id)
1152 class SnapshotRelation(TaskRelation):
1153 snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
1154 config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
1155 job_key = "snapshot"
1158 return "Snapshot Task"
1160 def get_delta(self):
1161 return self.config.to_dict()
1163 def delete(self, *args, **kwargs):
1164 self.config.delete()
1165 return super(self.__class__, self).delete(*args, **kwargs)
1168 class ActiveUsersRelation(TaskRelation):
1169 config = models.OneToOneField(ActiveUsersConfig, on_delete=models.CASCADE)
1170 job_key = "active users task"
1173 return "Active Users Task"
1176 class JobFactory(object):
1177 """This class creates all the API models (jobs, tasks, etc) needed to fulfill a booking."""
1180 def reimageHost(cls, new_image, booking, host):
1181 """Modify an existing job to reimage the given host."""
1182 job = Job.objects.get(booking=booking)
1183 # make hardware task new
1184 hardware_relation = HostHardwareRelation.objects.get(resource_id=host, job=job)
1185 hardware_relation.config.image = new_image.lab_id
1186 hardware_relation.config.save()
1187 hardware_relation.status = JobStatus.NEW
1189 # re-apply networking after host is reset
1190 net_relation = HostNetworkRelation.objects.get(resource_id=host, job=job)
1191 net_relation.status = JobStatus.NEW
1193 # re-apply ssh access after host is reset
1194 for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
1195 relation.status = JobStatus.NEW
1198 hardware_relation.save()
1202 def makeSnapshotTask(cls, image, booking, host):
1203 relation = SnapshotRelation()
1204 job = Job.objects.get(booking=booking)
1205 config = SnapshotConfig.objects.create(dashboard_id=image.id)
1208 relation.config = config
1209 relation.config.save()
1210 relation.config = relation.config
1211 relation.snapshot = image
1214 config.clear_delta()
1215 config.set_host(host)
1219 def makeActiveUsersTask(cls):
1220 """ Append active users task to analytics job """
1221 config = ActiveUsersConfig()
1222 relation = ActiveUsersRelation()
1223 job = Job.objects.get(job_type='DATA')
1225 job.status = JobStatus.NEW
1228 relation.config = config
1229 relation.config.save()
1230 relation.config = relation.config
1235 def makeAnalyticsJob(cls, booking):
1237 Create the analytics job
1239 This will only run once since there will only be one analytics job.
1240 All analytics tasks get appended to analytics job.
1243 if len(Job.objects.filter(job_type='DATA')) > 0:
1244 raise Exception("Cannot have more than one analytics job")
1246 if booking.resource:
1247 raise Exception("Booking is not marker for analytics job, has resoure")
1250 job.booking = booking
1251 job.job_type = 'DATA'
1254 cls.makeActiveUsersTask()
1257 def makeCompleteJob(cls, booking):
1258 """Create everything that is needed to fulfill the given booking."""
1259 resources = booking.resource.get_resources()
1262 job = Job.objects.get(booking=booking)
1264 job = Job.objects.create(status=JobStatus.NEW, booking=booking)
1265 cls.makeHardwareConfigs(
1266 resources=resources,
1269 cls.makeNetworkConfigs(
1270 resources=resources,
1277 cls.makeGeneratedCloudConfigs(
1278 resources=resources,
1281 all_users = list(booking.collaborators.all())
1282 all_users.append(booking.owner)
1283 cls.makeAccessConfig(
1289 for user in all_users:
1291 cls.makeAccessConfig(
1297 "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
1298 "hosts": [r.labid for r in resources]
1305 def makeGeneratedCloudConfigs(cls, resources=[], job=Job()):
1306 for res in resources:
1307 cif = GeneratedCloudConfig.objects.create(resource_id=res.labid, booking=job.booking, rconfig=res.config)
1310 cif = CloudInitFile.create(priority=0, text=cif.serialize())
1313 res.config.cloud_init_files.add(cif)
1317 def makeHardwareConfigs(cls, resources=[], job=Job()):
1319 Create and save HardwareConfig.
1321 Helper function to create the tasks related to
1322 configuring the hardware
1324 for res in resources:
1325 hardware_config = None
1327 hardware_config = HardwareConfig.objects.get(relation__resource_id=res.labid)
1329 hardware_config = HardwareConfig()
1331 relation = HostHardwareRelation()
1332 relation.resource_id = res.labid
1334 relation.config = hardware_config
1335 relation.config.save()
1336 relation.config = relation.config
1339 hardware_config.set("id", "image", "hostname", "power", "ipmi_create")
1340 hardware_config.save()
1343 def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
1345 Create and save AccessConfig.
1347 Helper function to create the tasks related to
1348 configuring the VPN, SSH, etc access for users
1351 relation = AccessRelation()
1353 config = AccessConfig()
1354 config.access_type = access_type
1357 relation.config = config
1359 config.clear_delta()
1361 config.set_context(context)
1362 config.set_access_type(access_type)
1363 config.set_revoke(revoke)
1364 config.set_user(user)
1368 def makeNetworkConfigs(cls, resources=[], job=Job()):
1370 Create and save NetworkConfig.
1372 Helper function to create the tasks related to
1373 configuring the networking
1375 for res in resources:
1376 network_config = None
1378 network_config = NetworkConfig.objects.get(relation__host=res)
1380 network_config = NetworkConfig.objects.create()
1382 relation = HostNetworkRelation()
1383 relation.resource_id = res.labid
1385 network_config.save()
1386 relation.config = network_config
1388 network_config.clear_delta()
1390 # TODO: use get_interfaces() on resource
1391 for interface in res.interfaces.all():
1392 network_config.add_interface(interface)
1393 network_config.save()
1396 def make_bridge_config(cls, booking):
1397 if len(booking.resource.get_resources()) < 2:
1400 jumphost_config = ResourceOPNFVConfig.objects.filter(
1401 role__name__iexact="jumphost"
1403 jumphost = ResourceQuery.filter(
1404 bundle=booking.resource,
1405 config=jumphost_config.resource_config
1409 br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
1410 for iface in jumphost.interfaces.all():
1411 br_config.interfaces.add(iface)
1415 def makeSoftware(cls, booking=None, job=Job()):
1417 Create and save SoftwareConfig.
1419 Helper function to create the tasks related to
1420 configuring the desired software, e.g. an OPNFV deployment
1422 if not booking.opnfv_config:
1425 opnfv_api_config = OpnfvApiConfig.objects.create(
1426 opnfv_config=booking.opnfv_config,
1427 installer=booking.opnfv_config.installer.name,
1428 scenario=booking.opnfv_config.scenario.name,
1429 bridge_config=cls.make_bridge_config(booking)
1432 opnfv_api_config.set_xdf(booking, False)
1433 opnfv_api_config.save()
1435 for host in booking.resource.get_resources():
1436 opnfv_api_config.roles.add(host)
1437 software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
1438 software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
1439 return software_relation
1442 JOB_TASK_CLASSLIST = [
1443 HostHardwareRelation,
1445 HostNetworkRelation,
1452 class JobTaskQuery(AbstractModelQuery):
1453 model_list = JOB_TASK_CLASSLIST