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
24 from booking.models import Booking
25 from resource_inventory.models import (
36 ResourceConfiguration,
39 from resource_inventory.idf_templater import IDFTemplater
40 from resource_inventory.pdf_templater import PDFTemplater
41 from account.models import Downtime, UserProfile
42 from dashboard.utils import AbstractModelQuery
47 A poor man's enum for a job's status.
49 A job is NEW if it has not been started or recognized by the Lab
50 A job is CURRENT if it has been started by the lab but it is not yet completed
51 a job is DONE if all the tasks are complete and the booking is ready to use
60 class LabManagerTracker:
63 def get(cls, lab_name, token):
67 Takes in a lab name (from a url path)
68 returns a lab manager instance for that lab, if it exists
69 Also checks that the given API token is correct
72 lab = Lab.objects.get(name=lab_name)
74 raise PermissionDenied("Lab not found")
75 if lab.api_token == token:
76 return LabManager(lab)
77 raise PermissionDenied("Lab not authorized")
82 Handles all lab REST calls.
84 handles jobs, inventory, status, etc
85 may need to create helper classes
88 def __init__(self, lab):
92 return Opsys.objects.filter(from_lab=self.lab)
95 return Image.objects.filter(from_lab=self.lab)
97 def get_image(self, image_id):
98 return Image.objects.filter(from_lab=self.lab, lab_id=image_id)
100 def get_opsys(self, opsys_id):
101 return Opsys.objects.filter(from_lab=self.lab, lab_id=opsys_id)
103 def get_downtime(self):
104 return Downtime.objects.filter(start__lt=timezone.now(), end__gt=timezone.now(), lab=self.lab)
106 def get_downtime_json(self):
107 downtime = self.get_downtime().first() # should only be one item in queryset
111 "start": downtime.start,
113 "description": downtime.description
115 return {"is_down": False}
117 def create_downtime(self, form):
119 Create a downtime event.
121 Takes in a dictionary that describes the model.
123 "start": utc timestamp
125 "description": human text (optional)
127 For timestamp structure, https://docs.djangoproject.com/en/2.2/ref/forms/fields/#datetimefield
129 Downtime.objects.create(
130 start=form.cleaned_data['start'],
131 end=form.cleaned_data['end'],
132 description=form.cleaned_data['description'],
135 return self.get_downtime_json()
137 def update_host_remote_info(self, data, res_id):
138 resource = ResourceQuery.filter(labid=res_id, lab=self.lab)
139 if len(resource) != 1:
140 return HttpResponseNotFound("Could not find single host with id " + str(res_id))
141 resource = resource[0]
144 info['address'] = data['address']
145 info['mac_address'] = data['mac_address']
146 info['password'] = data['password']
147 info['user'] = data['user']
148 info['type'] = data['type']
149 info['versions'] = json.dumps(data['versions'])
150 except Exception as e:
151 return {"error": "invalid arguement: " + str(e)}
152 remote_info = resource.remote_management
153 if "default" in remote_info.mac_address:
154 remote_info = RemoteInfo()
155 remote_info.address = info['address']
156 remote_info.mac_address = info['mac_address']
157 remote_info.password = info['password']
158 remote_info.user = info['user']
159 remote_info.type = info['type']
160 remote_info.versions = info['versions']
162 resource.remote_management = remote_info
164 booking = Booking.objects.get(resource=resource.bundle)
165 self.update_xdf(booking)
166 return {"status": "success"}
168 def update_xdf(self, booking):
169 booking.pdf = PDFTemplater.makePDF(booking)
170 booking.idf = IDFTemplater().makeIDF(booking)
173 def get_pdf(self, booking_id):
174 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
177 def get_idf(self, booking_id):
178 booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
181 def get_profile(self):
183 prof['name'] = self.lab.name
185 "phone": self.lab.contact_phone,
186 "email": self.lab.contact_email
188 prof['host_count'] = [{
189 "type": profile.name,
190 "count": len(profile.get_resources(lab=self.lab))}
191 for profile in ResourceProfile.objects.filter(labs=self.lab)]
194 def format_user(self, userprofile):
196 "id": userprofile.user.id,
197 "username": userprofile.user.username,
198 "email": userprofile.email_addr,
199 "first_name": userprofile.user.first_name,
200 "last_name": userprofile.user.last_name,
201 "company": userprofile.company
205 userlist = [self.format_user(profile) for profile in UserProfile.objects.select_related("user").all()]
207 return json.dumps({"users": userlist})
209 def get_user(self, user_id):
210 user = User.objects.get(pk=user_id)
212 profile = get_object_or_404(UserProfile, user=user)
214 return json.dumps(self.format_user(profile))
216 def get_inventory(self):
218 resources = ResourceQuery.filter(lab=self.lab)
219 images = Image.objects.filter(from_lab=self.lab)
220 profiles = ResourceProfile.objects.filter(labs=self.lab)
221 inventory['resources'] = self.serialize_resources(resources)
222 inventory['images'] = self.serialize_images(images)
223 inventory['host_types'] = self.serialize_host_profiles(profiles)
226 def get_host(self, hostname):
227 resource = ResourceQuery.filter(labid=hostname, lab=self.lab)
228 if len(resource) != 1:
229 return HttpResponseNotFound("Could not find single host with id " + str(hostname))
230 resource = resource[0]
232 "booked": resource.booked,
233 "working": resource.working,
234 "type": resource.profile.name
237 def update_host(self, hostname, data):
238 resource = ResourceQuery.filter(labid=hostname, lab=self.lab)
239 if len(resource) != 1:
240 return HttpResponseNotFound("Could not find single host with id " + str(hostname))
241 resource = resource[0]
242 if "working" in data:
243 working = data['working'] == "true"
244 resource.working = working
246 return self.get_host(hostname)
248 def get_status(self):
249 return {"status": self.lab.status}
251 def set_status(self, payload):
254 def get_current_jobs(self):
255 jobs = Job.objects.filter(booking__lab=self.lab)
257 return self.serialize_jobs(jobs, status=JobStatus.CURRENT)
259 def get_new_jobs(self):
260 jobs = Job.objects.filter(booking__lab=self.lab)
262 return self.serialize_jobs(jobs, status=JobStatus.NEW)
264 def get_done_jobs(self):
265 jobs = Job.objects.filter(booking__lab=self.lab)
267 return self.serialize_jobs(jobs, status=JobStatus.DONE)
269 def get_analytics_job(self):
270 """ Get analytics job with status new """
271 jobs = Job.objects.filter(
272 booking__lab=self.lab,
276 return self.serialize_jobs(jobs, status=JobStatus.NEW)
278 def get_job(self, jobid):
279 return Job.objects.get(pk=jobid).to_dict()
281 def update_job(self, jobid, data):
284 def serialize_jobs(self, jobs, status=JobStatus.NEW):
287 jsonized_job = job.get_delta(status)
288 if len(jsonized_job['payload']) < 1:
290 job_ser.append(jsonized_job)
294 def serialize_resources(self, resources):
295 # TODO: rewrite for Resource model
297 for res in resources:
300 'hostname': res.name,
301 'host_type': res.profile.name
303 for iface in res.get_interfaces():
304 r['interfaces'].append({
305 'mac': iface.mac_address,
306 'busaddr': iface.bus_address,
308 'switchport': {"switch_name": iface.switch_name, "port_name": iface.port_name}
312 def serialize_images(self, images):
318 "lab_id": image.lab_id,
319 "dashboard_id": image.id
324 def serialize_resource_profiles(self, profiles):
326 for profile in profiles:
329 "cores": profile.cpuprofile.first().cores,
330 "arch": profile.cpuprofile.first().architecture,
331 "cpus": profile.cpuprofile.first().cpus,
334 for disk in profile.storageprofile.all():
337 "type": disk.media_type,
341 p['description'] = profile.description
343 for iface in profile.interfaceprofile.all():
344 p['interfaces'].append(
346 "speed": iface.speed,
351 p['ram'] = {"amount": profile.ramprofile.first().amount}
352 p['name'] = profile.name
353 profile_ser.append(p)
357 class GeneratedCloudConfig(models.Model):
358 resource_id = models.CharField(max_length=200)
359 booking = models.ForeignKey(Booking, on_delete=models.CASCADE)
360 rconfig = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE)
361 text = models.TextField(null=True, blank=True)
363 def _normalize_username(self, username: str) -> str:
364 # TODO: make usernames posix compliant
367 def _get_ssh_string(self, username: str) -> str:
368 user = User.objects.get(username=username)
369 uprofile = user.userprofile
371 ssh_file = uprofile.ssh_public_key
373 escaped_file = ssh_file.open().read().decode(encoding="UTF-8").replace("\n", " ")
377 def _serialize_users(self):
379 returns the dictionary to be placed behind the `users` field of the toplevel c-i dict
381 # conserves distro default user
382 user_array = ["default"]
384 users = list(self.booking.collaborators.all())
385 users.append(self.booking.owner)
386 for collaborator in users:
389 # TODO: validate if usernames are valid as linux usernames (and provide an override potentially)
390 userdict['name'] = self._normalize_username(collaborator.username)
392 userdict['groups'] = "sudo"
393 userdict['sudo'] = "ALL=(ALL) NOPASSWD:ALL"
395 userdict['ssh_authorized_keys'] = [self._get_ssh_string(collaborator.username)]
397 user_array.append(userdict)
399 # user_array.append({
401 # "passwd": "$6$k54L.vim1cLaEc4$5AyUIrufGlbtVBzuCWOlA1yV6QdD7Gr2MzwIs/WhuYR9ebSfh3Qlb7djkqzjwjxpnSAonK1YOabPP6NxUDccu.",
402 # "ssh_redirect_user": True,
403 # "sudo": "ALL=(ALL) NOPASSWD:ALL",
409 # TODO: make this configurable
410 def _serialize_sysinfo(self):
412 defuser['name'] = 'opnfv'
413 defuser['plain_text_passwd'] = 'OPNFV_HOST'
414 defuser['home'] = '/home/opnfv'
415 defuser['shell'] = '/bin/bash'
416 defuser['lock_passwd'] = True
417 defuser['gecos'] = 'Lab Manager User'
418 defuser['groups'] = 'sudo'
420 return {'default_user': defuser}
422 # TODO: make this configurable
423 def _serialize_runcmds(self):
426 # have hosts run dhcp on boot
427 cmdlist.append(['sudo', 'dhclient', '-r'])
428 cmdlist.append(['sudo', 'dhclient'])
432 def _serialize_netconf_v1(self):
433 # interfaces = {} # map from iface_name => dhcp_config
434 # vlans = {} # map from vlan_id => dhcp_config
438 for interface in self._resource().interfaces.all():
439 interface_name = interface.profile.name
440 interface_mac = interface.mac_address
444 "name": interface_name,
445 "mac_address": interface_mac,
448 for vlan in interface.config.all():
450 vlan_dict_entry = {'type': 'vlan'}
451 vlan_dict_entry['name'] = str(interface_name) + "." + str(vlan.vlan_id)
452 vlan_dict_entry['vlan_link'] = str(interface_name)
453 vlan_dict_entry['vlan_id'] = int(vlan.vlan_id)
454 vlan_dict_entry['mac_address'] = str(interface_mac)
456 vlan_dict_entry["subnets"] = [{"type": "dhcp"}]
457 config_arr.append(vlan_dict_entry)
458 if (not vlan.tagged) and vlan.public:
459 iface_dict_entry["subnets"] = [{"type": "dhcp"}]
461 # vlan_dict_entry['mtu'] = # TODO, determine override MTU if needed
463 config_arr.append(iface_dict_entry)
466 'type': 'nameserver',
467 'address': ['10.64.0.1', '8.8.8.8']
470 config_arr.append(ns_dict)
472 full_dict = {'version': 1, 'config': config_arr}
477 def get(cls, booking_id: int, resource_lab_id: str, file_id: int):
478 return GeneratedCloudConfig.objects.get(resource_id=resource_lab_id, booking__id=booking_id, file_id=file_id)
481 return ResourceQuery.get(labid=self.resource_id, lab=self.booking.lab)
483 # def _get_facts(self):
484 # resource = self._resource()
486 # hostname = self.rconfig.name
487 # iface_configs = for_config.interface_configs.all()
492 main_dict['users'] = self._serialize_users()
493 main_dict['network'] = self._serialize_netconf_v1()
494 main_dict['hostname'] = self.rconfig.name
496 # add first startup commands
497 main_dict['runcmd'] = self._serialize_runcmds()
499 # configure distro default user
500 main_dict['system_info'] = self._serialize_sysinfo()
504 def serialize(self) -> str:
505 return yaml.dump(self._to_dict(), width=float("inf"))
508 class APILog(models.Model):
509 user = models.ForeignKey(User, on_delete=models.PROTECT)
510 call_time = models.DateTimeField(auto_now=True)
511 method = models.CharField(null=True, max_length=6)
512 endpoint = models.CharField(null=True, max_length=300)
513 ip_addr = models.GenericIPAddressField(protocol="both", null=True, unpack_ipv4=False)
514 body = JSONField(null=True)
517 return "Call to {} at {} by {}".format(
524 class AutomationAPIManager:
526 def serialize_booking(booking):
528 sbook['id'] = booking.pk
529 sbook['owner'] = booking.owner.username
530 sbook['collaborators'] = [user.username for user in booking.collaborators.all()]
531 sbook['start'] = booking.start
532 sbook['end'] = booking.end
533 sbook['lab'] = AutomationAPIManager.serialize_lab(booking.lab)
534 sbook['purpose'] = booking.purpose
535 sbook['resourceBundle'] = AutomationAPIManager.serialize_bundle(booking.resource)
539 def serialize_lab(lab):
542 slab['name'] = lab.name
546 def serialize_bundle(bundle):
548 sbundle['id'] = bundle.pk
549 sbundle['resources'] = [
550 AutomationAPIManager.serialize_server(server)
551 for server in bundle.get_resources()]
555 def serialize_server(server):
557 sserver['id'] = server.pk
558 sserver['name'] = server.name
562 def serialize_resource_profile(profile):
564 sprofile['id'] = profile.pk
565 sprofile['name'] = profile.name
569 def serialize_template(rec_temp_and_count):
570 template = rec_temp_and_count[0]
571 count = rec_temp_and_count[1]
574 stemplate['id'] = template.pk
575 stemplate['name'] = template.name
576 stemplate['count_available'] = count
577 stemplate['resourceProfiles'] = [
578 AutomationAPIManager.serialize_resource_profile(config.profile)
579 for config in template.getConfigs()
584 def serialize_image(image):
586 simage['id'] = image.pk
587 simage['name'] = image.name
591 def serialize_userprofile(up):
594 sup['username'] = up.user.username
598 class Job(models.Model):
600 A Job to be performed by the Lab.
602 The API uses Jobs and Tasks to communicate actions that need to be taken to the Lab
603 that is hosting a booking. A booking from a user has an associated Job which tells
604 the lab how to configure the hardware, networking, etc to fulfill the booking
606 This is the class that is serialized and put into the api
611 ('DATA', 'Analytics')
614 booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
615 status = models.IntegerField(default=JobStatus.NEW)
616 complete = models.BooleanField(default=False)
617 job_type = models.CharField(
625 for relation in self.get_tasklist():
626 if relation.job_key not in d:
627 d[relation.job_key] = {}
628 d[relation.job_key][relation.task_id] = relation.config.to_dict()
630 return {"id": self.id, "payload": d}
632 def get_tasklist(self, status="all"):
634 return JobTaskQuery.filter(job=self, status=status)
635 return JobTaskQuery.filter(job=self)
637 def is_fulfilled(self):
639 If a job has been completed by the lab.
641 This method should return true if all of the job's tasks are done,
644 my_tasks = self.get_tasklist()
645 for task in my_tasks:
646 if task.status != JobStatus.DONE:
650 def get_delta(self, status):
652 for relation in self.get_tasklist(status=status):
653 if relation.job_key not in d:
654 d[relation.job_key] = {}
655 d[relation.job_key][relation.task_id] = relation.config.get_delta()
657 return {"id": self.id, "payload": d}
660 return json.dumps(self.to_dict())
663 class TaskConfig(models.Model):
664 state = models.IntegerField(default=ConfigState.NEW)
666 keys = set() # TODO: This needs to be an instance variable, not a class variable
667 delta_keys_list = models.CharField(max_length=200, default="[]")
670 def delta_keys(self):
671 return list(set(json.loads(self.delta_keys_list)))
674 def delta_keys(self, keylist):
675 self.delta_keys_list = json.dumps(keylist)
678 raise NotImplementedError
681 raise NotImplementedError
683 def format_delta(self, config, token):
684 delta = {k: config[k] for k in self.delta_keys}
685 delta['lab_token'] = token
689 return json.dumps(self.to_dict())
691 def clear_delta(self):
694 def set(self, *args):
695 dkeys = self.delta_keys
699 self.delta_keys = dkeys
702 class BridgeConfig(models.Model):
703 """Displays mapping between jumphost interfaces and bridges."""
705 interfaces = models.ManyToManyField(Interface)
706 opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
710 hid = ResourceQuery.get(interface__pk=self.interfaces.first().pk).labid
712 for interface in self.interfaces.all():
713 d[hid][interface.mac_address] = []
714 for vlan in interface.config.all():
715 network_role = self.opnfv_model.networks().filter(network=vlan.network)
716 bridge = IDFTemplater.bridge_names[network_role.name]
718 "vlan_id": vlan.vlan_id,
719 "tagged": vlan.tagged,
722 d[hid][interface.mac_address].append(br_config)
726 return json.dumps(self.to_dict())
729 class ActiveUsersConfig(models.Model):
731 Task for getting active VPN users
733 StackStorm needs no information to run this job
734 so this task is very bare, but neccessary to fit
735 job creation convention.
738 def clear_delta(self):
742 return json.loads(self.to_json())
745 return json.dumps(self.to_dict())
751 class OpnfvApiConfig(models.Model):
753 installer = models.CharField(max_length=200)
754 scenario = models.CharField(max_length=300)
755 roles = models.ManyToManyField(ResourceOPNFVConfig)
756 # pdf and idf are url endpoints, not the actual file
757 pdf = models.CharField(max_length=100)
758 idf = models.CharField(max_length=100)
759 bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True)
760 delta = models.TextField()
761 opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL)
765 if not self.opnfv_config:
768 d['installer'] = self.installer
770 d['scenario'] = self.scenario
775 if self.bridge_config:
776 d['bridged_interfaces'] = self.bridge_config.to_dict()
778 hosts = self.roles.all()
783 host.labid: self.opnfv_config.host_opnfv_config.get(
784 host_config__pk=host.config.pk
791 return json.dumps(self.to_dict())
793 def set_installer(self, installer):
794 self.installer = installer
795 d = json.loads(self.delta)
796 d['installer'] = installer
797 self.delta = json.dumps(d)
799 def set_scenario(self, scenario):
800 self.scenario = scenario
801 d = json.loads(self.delta)
802 d['scenario'] = scenario
803 self.delta = json.dumps(d)
805 def set_xdf(self, booking, update_delta=True):
806 kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id}
807 self.pdf = reverse('get-pdf', kwargs=kwargs)
808 self.idf = reverse('get-idf', kwargs=kwargs)
810 d = json.loads(self.delta)
813 self.delta = json.dumps(d)
815 def add_role(self, host):
817 d = json.loads(self.delta)
820 d['roles'].append({host.labid: host.config.opnfvRole.name})
821 self.delta = json.dumps(d)
823 def clear_delta(self):
827 return json.loads(self.to_json())
830 class AccessConfig(TaskConfig):
831 access_type = models.CharField(max_length=50)
832 user = models.ForeignKey(User, on_delete=models.CASCADE)
833 revoke = models.BooleanField(default=False)
834 context = models.TextField(default="")
835 delta = models.TextField(default="{}")
839 d['access_type'] = self.access_type
840 d['user'] = self.user.id
841 d['revoke'] = self.revoke
843 d['context'] = json.loads(self.context)
849 d = json.loads(self.to_json())
850 d["lab_token"] = self.accessrelation.lab_token
855 return json.dumps(self.to_dict())
857 def clear_delta(self):
859 d["lab_token"] = self.accessrelation.lab_token
860 self.delta = json.dumps(d)
862 def set_access_type(self, access_type):
863 self.access_type = access_type
864 d = json.loads(self.delta)
865 d['access_type'] = access_type
866 self.delta = json.dumps(d)
868 def set_user(self, user):
870 d = json.loads(self.delta)
871 d['user'] = self.user.id
872 self.delta = json.dumps(d)
874 def set_revoke(self, revoke):
876 d = json.loads(self.delta)
878 self.delta = json.dumps(d)
880 def set_context(self, context):
881 self.context = json.dumps(context)
882 d = json.loads(self.delta)
883 d['context'] = context
884 self.delta = json.dumps(d)
887 class SoftwareConfig(TaskConfig):
888 """Handles software installations, such as OPNFV or ONAP."""
890 opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
895 d['opnfv'] = self.opnfv.to_dict()
897 d["lab_token"] = self.softwarerelation.lab_token
898 self.delta = json.dumps(d)
904 d['opnfv'] = self.opnfv.get_delta()
905 d['lab_token'] = self.softwarerelation.lab_token
909 def clear_delta(self):
910 self.opnfv.clear_delta()
913 return json.dumps(self.to_dict())
916 class HardwareConfig(TaskConfig):
917 """Describes the desired configuration of the hardware."""
919 image = models.CharField(max_length=100, default="defimage")
920 power = models.CharField(max_length=100, default="off")
921 hostname = models.CharField(max_length=100, default="hostname")
922 ipmi_create = models.BooleanField(default=False)
923 delta = models.TextField()
925 keys = set(["id", "image", "power", "hostname", "ipmi_create"])
928 return self.get_delta()
931 # TODO: grab the GeneratedCloudConfig urls from self.hosthardwarerelation.get_resource()
932 return self.format_delta(
933 self.hosthardwarerelation.get_resource().get_configuration(self.state),
934 self.hosthardwarerelation.lab_token)
937 class NetworkConfig(TaskConfig):
938 """Handles network configuration."""
940 interfaces = models.ManyToManyField(Interface)
941 delta = models.TextField()
945 hid = self.hostnetworkrelation.resource_id
947 for interface in self.interfaces.all():
948 d[hid][interface.mac_address] = []
949 if self.state != ConfigState.CLEAN:
950 for vlan in interface.config.all():
951 # TODO: should this come from the interface?
952 # e.g. will different interfaces for different resources need different configs?
953 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
958 return json.dumps(self.to_dict())
961 d = json.loads(self.to_json())
962 d['lab_token'] = self.hostnetworkrelation.lab_token
965 def clear_delta(self):
966 self.delta = json.dumps(self.to_dict())
969 def add_interface(self, interface):
970 self.interfaces.add(interface)
971 d = json.loads(self.delta)
972 hid = self.hostnetworkrelation.resource_id
975 d[hid][interface.mac_address] = []
976 for vlan in interface.config.all():
977 d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged})
978 self.delta = json.dumps(d)
981 class SnapshotConfig(TaskConfig):
983 resource_id = models.CharField(max_length=200, default="default_id")
984 image = models.CharField(max_length=200, null=True) # cobbler ID
985 dashboard_id = models.IntegerField()
986 delta = models.TextField(default="{}")
991 d['host'] = self.host.labid
993 d['image'] = self.image
994 d['dashboard_id'] = self.dashboard_id
998 return json.dumps(self.to_dict())
1000 def get_delta(self):
1001 d = json.loads(self.to_json())
1004 def clear_delta(self):
1005 self.delta = json.dumps(self.to_dict())
1008 def set_host(self, host):
1010 d = json.loads(self.delta)
1011 d['host'] = host.labid
1012 self.delta = json.dumps(d)
1014 def set_image(self, image):
1016 d = json.loads(self.delta)
1017 d['image'] = self.image
1018 self.delta = json.dumps(d)
1020 def clear_image(self):
1022 d = json.loads(self.delta)
1023 d.pop("image", None)
1024 self.delta = json.dumps(d)
1026 def set_dashboard_id(self, dash):
1027 self.dashboard_id = dash
1028 d = json.loads(self.delta)
1029 d['dashboard_id'] = self.dashboard_id
1030 self.delta = json.dumps(d)
1032 def save(self, *args, **kwargs):
1033 if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
1034 raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
1035 super().save(*args, **kwargs)
1038 def get_task(task_id):
1039 for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]:
1041 ret = taskclass.objects.get(task_id=task_id)
1043 except taskclass.DoesNotExist:
1045 from django.core.exceptions import ObjectDoesNotExist
1046 raise ObjectDoesNotExist("Could not find matching TaskRelation instance")
1049 def get_task_uuid():
1050 return str(uuid.uuid4())
1053 class TaskRelation(models.Model):
1055 Relates a Job to a TaskConfig.
1057 superclass that relates a Job to tasks anc maintains information
1058 like status and messages from the lab
1061 status = models.IntegerField(default=JobStatus.NEW)
1062 job = models.ForeignKey(Job, on_delete=models.CASCADE)
1063 config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
1064 task_id = models.CharField(default=get_task_uuid, max_length=37)
1065 lab_token = models.CharField(default="null", max_length=50)
1066 message = models.TextField(default="")
1070 def delete(self, *args, **kwargs):
1071 self.config.delete()
1072 return super(self.__class__, self).delete(*args, **kwargs)
1075 return "Generic Task"
1081 class AccessRelation(TaskRelation):
1082 config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE)
1086 return "Access Task"
1088 def delete(self, *args, **kwargs):
1089 self.config.delete()
1090 return super(self.__class__, self).delete(*args, **kwargs)
1093 class SoftwareRelation(TaskRelation):
1094 config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE)
1095 job_key = "software"
1098 return "Software Configuration Task"
1100 def delete(self, *args, **kwargs):
1101 self.config.delete()
1102 return super(self.__class__, self).delete(*args, **kwargs)
1105 class HostHardwareRelation(TaskRelation):
1106 resource_id = models.CharField(max_length=200, default="default_id")
1107 config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
1108 job_key = "hardware"
1111 return "Hardware Configuration Task"
1113 def get_delta(self):
1114 return self.config.to_dict()
1116 def delete(self, *args, **kwargs):
1117 self.config.delete()
1118 return super(self.__class__, self).delete(*args, **kwargs)
1120 def save(self, *args, **kwargs):
1121 if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
1122 raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
1123 super().save(*args, **kwargs)
1125 def get_resource(self):
1126 return ResourceQuery.get(labid=self.resource_id)
1129 class HostNetworkRelation(TaskRelation):
1130 resource_id = models.CharField(max_length=200, default="default_id")
1131 config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
1135 return "Network Configuration Task"
1137 def delete(self, *args, **kwargs):
1138 self.config.delete()
1139 return super(self.__class__, self).delete(*args, **kwargs)
1141 def save(self, *args, **kwargs):
1142 if len(ResourceQuery.filter(labid=self.resource_id)) != 1:
1143 raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource")
1144 super().save(*args, **kwargs)
1146 def get_resource(self):
1147 return ResourceQuery.get(labid=self.resource_id)
1150 class SnapshotRelation(TaskRelation):
1151 snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
1152 config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
1153 job_key = "snapshot"
1156 return "Snapshot Task"
1158 def get_delta(self):
1159 return self.config.to_dict()
1161 def delete(self, *args, **kwargs):
1162 self.config.delete()
1163 return super(self.__class__, self).delete(*args, **kwargs)
1166 class ActiveUsersRelation(TaskRelation):
1167 config = models.OneToOneField(ActiveUsersConfig, on_delete=models.CASCADE)
1168 job_key = "active users task"
1171 return "Active Users Task"
1174 class JobFactory(object):
1175 """This class creates all the API models (jobs, tasks, etc) needed to fulfill a booking."""
1178 def reimageHost(cls, new_image, booking, host):
1179 """Modify an existing job to reimage the given host."""
1180 job = Job.objects.get(booking=booking)
1181 # make hardware task new
1182 hardware_relation = HostHardwareRelation.objects.get(resource_id=host, job=job)
1183 hardware_relation.config.image = new_image.lab_id
1184 hardware_relation.config.save()
1185 hardware_relation.status = JobStatus.NEW
1187 # re-apply networking after host is reset
1188 net_relation = HostNetworkRelation.objects.get(resource_id=host, job=job)
1189 net_relation.status = JobStatus.NEW
1191 # re-apply ssh access after host is reset
1192 for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
1193 relation.status = JobStatus.NEW
1196 hardware_relation.save()
1200 def makeSnapshotTask(cls, image, booking, host):
1201 relation = SnapshotRelation()
1202 job = Job.objects.get(booking=booking)
1203 config = SnapshotConfig.objects.create(dashboard_id=image.id)
1206 relation.config = config
1207 relation.config.save()
1208 relation.config = relation.config
1209 relation.snapshot = image
1212 config.clear_delta()
1213 config.set_host(host)
1217 def makeActiveUsersTask(cls):
1218 """ Append active users task to analytics job """
1219 config = ActiveUsersConfig()
1220 relation = ActiveUsersRelation()
1221 job = Job.objects.get(job_type='DATA')
1223 job.status = JobStatus.NEW
1226 relation.config = config
1227 relation.config.save()
1228 relation.config = relation.config
1233 def makeAnalyticsJob(cls, booking):
1235 Create the analytics job
1237 This will only run once since there will only be one analytics job.
1238 All analytics tasks get appended to analytics job.
1241 if len(Job.objects.filter(job_type='DATA')) > 0:
1242 raise Exception("Cannot have more than one analytics job")
1244 if booking.resource:
1245 raise Exception("Booking is not marker for analytics job, has resoure")
1248 job.booking = booking
1249 job.job_type = 'DATA'
1252 cls.makeActiveUsersTask()
1255 def makeCompleteJob(cls, booking):
1256 """Create everything that is needed to fulfill the given booking."""
1257 resources = booking.resource.get_resources()
1260 job = Job.objects.get(booking=booking)
1262 job = Job.objects.create(status=JobStatus.NEW, booking=booking)
1263 cls.makeHardwareConfigs(
1264 resources=resources,
1267 cls.makeNetworkConfigs(
1268 resources=resources,
1275 cls.makeGeneratedCloudConfigs(
1276 resources=resources,
1279 all_users = list(booking.collaborators.all())
1280 all_users.append(booking.owner)
1281 cls.makeAccessConfig(
1287 for user in all_users:
1289 cls.makeAccessConfig(
1295 "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
1296 "hosts": [r.labid for r in resources]
1303 def makeGeneratedCloudConfigs(cls, resources=[], job=Job()):
1304 for res in resources:
1305 cif = GeneratedCloudConfig.objects.create(resource_id=res.labid, booking=job.booking, rconfig=res.config)
1308 cif = CloudInitFile.create(priority=0, text=cif.serialize())
1311 res.config.cloud_init_files.add(cif)
1315 def makeHardwareConfigs(cls, resources=[], job=Job()):
1317 Create and save HardwareConfig.
1319 Helper function to create the tasks related to
1320 configuring the hardware
1322 for res in resources:
1323 hardware_config = None
1325 hardware_config = HardwareConfig.objects.get(relation__resource_id=res.labid)
1327 hardware_config = HardwareConfig()
1329 relation = HostHardwareRelation()
1330 relation.resource_id = res.labid
1332 relation.config = hardware_config
1333 relation.config.save()
1334 relation.config = relation.config
1337 hardware_config.set("id", "image", "hostname", "power", "ipmi_create")
1338 hardware_config.save()
1341 def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
1343 Create and save AccessConfig.
1345 Helper function to create the tasks related to
1346 configuring the VPN, SSH, etc access for users
1349 relation = AccessRelation()
1351 config = AccessConfig()
1352 config.access_type = access_type
1355 relation.config = config
1357 config.clear_delta()
1359 config.set_context(context)
1360 config.set_access_type(access_type)
1361 config.set_revoke(revoke)
1362 config.set_user(user)
1366 def makeNetworkConfigs(cls, resources=[], job=Job()):
1368 Create and save NetworkConfig.
1370 Helper function to create the tasks related to
1371 configuring the networking
1373 for res in resources:
1374 network_config = None
1376 network_config = NetworkConfig.objects.get(relation__host=res)
1378 network_config = NetworkConfig.objects.create()
1380 relation = HostNetworkRelation()
1381 relation.resource_id = res.labid
1383 network_config.save()
1384 relation.config = network_config
1386 network_config.clear_delta()
1388 # TODO: use get_interfaces() on resource
1389 for interface in res.interfaces.all():
1390 network_config.add_interface(interface)
1391 network_config.save()
1394 def make_bridge_config(cls, booking):
1395 if len(booking.resource.get_resources()) < 2:
1398 jumphost_config = ResourceOPNFVConfig.objects.filter(
1399 role__name__iexact="jumphost"
1401 jumphost = ResourceQuery.filter(
1402 bundle=booking.resource,
1403 config=jumphost_config.resource_config
1407 br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
1408 for iface in jumphost.interfaces.all():
1409 br_config.interfaces.add(iface)
1413 def makeSoftware(cls, booking=None, job=Job()):
1415 Create and save SoftwareConfig.
1417 Helper function to create the tasks related to
1418 configuring the desired software, e.g. an OPNFV deployment
1420 if not booking.opnfv_config:
1423 opnfv_api_config = OpnfvApiConfig.objects.create(
1424 opnfv_config=booking.opnfv_config,
1425 installer=booking.opnfv_config.installer.name,
1426 scenario=booking.opnfv_config.scenario.name,
1427 bridge_config=cls.make_bridge_config(booking)
1430 opnfv_api_config.set_xdf(booking, False)
1431 opnfv_api_config.save()
1433 for host in booking.resource.get_resources():
1434 opnfv_api_config.roles.add(host)
1435 software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
1436 software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
1437 return software_relation
1440 JOB_TASK_CLASSLIST = [
1441 HostHardwareRelation,
1443 HostNetworkRelation,
1450 class JobTaskQuery(AbstractModelQuery):
1451 model_list = JOB_TASK_CLASSLIST