1 ##############################################################################
2 # Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 ##############################################################################
13 from django.db.models import Q
14 from django.db import transaction
15 from datetime import timedelta
16 from django.utils import timezone
17 from django.core.exceptions import ValidationError
18 from account.models import Lab, UserProfile
20 from resource_inventory.models import (
26 ResourceConfiguration,
28 InterfaceConfiguration,
32 from resource_inventory.resource_manager import ResourceManager
33 from resource_inventory.pdf_templater import PDFTemplater
34 from notifier.manager import NotificationHandler
35 from booking.models import Booking
36 from dashboard.exceptions import BookingLengthException
37 from api.models import JobFactory
40 def parse_resource_field(resource_json):
42 Parse the json from the frontend.
44 returns a reference to the selected Lab and ResourceTemplate objects
46 lab, template = (None, None)
47 lab_dict = resource_json['lab']
48 for lab_info in lab_dict.values():
49 if lab_info['selected']:
50 lab = Lab.objects.get(lab_user__id=lab_info['id'])
52 resource_dict = resource_json['resource']
53 for resource_info in resource_dict.values():
54 if resource_info['selected']:
55 template = ResourceTemplate.objects.get(pk=resource_info['id'])
58 raise ValidationError("No lab was selected")
60 raise ValidationError("No Host was selected")
65 def update_template(old_template, image, hostname, user, global_cloud_config=None):
67 Duplicate a template to the users account and update configured fields.
69 The dashboard presents users with preconfigured resource templates,
70 but the user might want to make small modifications, e.g hostname and
71 linux distro. So we copy the public template and create a private version
72 to the user's profile, and mark it temporary. When the booking ends the
73 new template is deleted
75 name = user.username + "'s Copy of '" + old_template.name + "'"
76 num_copies = ResourceTemplate.objects.filter(name__startswith=name).count()
77 template = ResourceTemplate.objects.create(
78 name=name if num_copies == 0 else name + " (" + str(num_copies) + ")",
82 description=old_template.description,
88 for old_network in old_template.networks.all():
89 Network.objects.create(
90 name=old_network.name,
92 is_public=old_network.is_public
94 # We are assuming there is only one opnfv config per public resource template
95 old_opnfv = template.opnfv_config.first()
97 opnfv_config = OPNFVConfig.objects.create(
98 installer=old_opnfv.installer,
99 scenario=old_opnfv.installer,
101 name=old_opnfv.installer,
103 # I am explicitly leaving opnfv_config.networks empty to avoid
104 # problems with duplicated / shared networks. In the quick deploy,
105 # there is never multiple networks anyway. This may have to change in the future
107 for old_config in old_template.getConfigs():
110 image_to_set = old_config.image
112 config = ResourceConfiguration.objects.create(
113 profile=old_config.profile,
116 is_head_node=old_config.is_head_node,
117 name=hostname if len(old_template.getConfigs()) == 1 else old_config.name,
118 # cloud_init_files=old_config.cloud_init_files.set()
121 for file in old_config.cloud_init_files.all():
122 config.cloud_init_files.add(file)
124 if global_cloud_config:
125 config.cloud_init_files.add(global_cloud_config)
128 for old_iface_config in old_config.interface_configs.all():
129 iface_config = InterfaceConfiguration.objects.create(
130 profile=old_iface_config.profile,
131 resource_config=config
134 for old_connection in old_iface_config.connections.all():
135 iface_config.connections.add(NetworkConnection.objects.create(
136 network=template.networks.get(name=old_connection.network.name),
137 vlan_is_tagged=old_connection.vlan_is_tagged
140 for old_res_opnfv in old_config.resource_opnfv_config.all():
142 ResourceOPNFVConfig.objects.create(
144 resource_config=config,
145 opnfv_config=opnfv_config
150 def generate_opnfvconfig(scenario, installer, template):
151 return OPNFVConfig.objects.create(
158 def generate_hostopnfv(hostconfig, opnfvconfig):
161 role = OPNFVRole.objects.get(name="Jumphost")
163 role = OPNFVRole.objects.create(
165 description="Single server jumphost role"
167 return ResourceOPNFVConfig.objects.create(
169 host_config=hostconfig,
170 opnfv_config=opnfvconfig
174 def generate_resource_bundle(template):
175 resource_manager = ResourceManager.getInstance()
176 resource_bundle = resource_manager.instantiateTemplate(template)
177 return resource_bundle
180 def check_invariants(**kwargs):
181 # TODO: This should really happen in the BookingForm validation methods
182 image = kwargs['image']
184 length = kwargs['length']
185 # check that image os is compatible with installer
187 if image.from_lab != lab:
188 raise ValidationError("The chosen image is not available at the chosen hosting lab")
190 # if image.host_type != host_profile:
191 # raise ValidationError("The chosen image is not available for the chosen host type")
192 if not image.public and image.owner != kwargs['owner']:
193 raise ValidationError("You are not the owner of the chosen private image")
194 if length < 1 or length > 21:
195 raise BookingLengthException("Booking must be between 1 and 21 days long")
198 def create_from_form(form, request):
200 Parse data from QuickBookingForm to create booking
202 resource_field = form.cleaned_data['filter_field']
203 # users_field = form.cleaned_data['users']
204 hostname = 'opnfv_host' if not form.cleaned_data['hostname'] else form.cleaned_data['hostname']
206 global_cloud_config = None if not form.cleaned_data['global_cloud_config'] else form.cleaned_data['global_cloud_config']
208 if global_cloud_config:
209 form.cleaned_data['global_cloud_config'] = create_ci_file(global_cloud_config)
211 # image = form.cleaned_data['image']
212 # scenario = form.cleaned_data['scenario']
213 # installer = form.cleaned_data['installer']
215 lab, resource_template = parse_resource_field(resource_field)
216 data = form.cleaned_data
217 data['hostname'] = hostname
219 data['resource_template'] = resource_template
220 data['owner'] = request.user
222 return _create_booking(data)
225 def create_from_API(body, user):
227 Parse data from Automation API to create booking
229 booking_info = json.loads(body.decode('utf-8'))
232 data['purpose'] = booking_info['purpose']
233 data['project'] = booking_info['project']
234 data['users'] = [UserProfile.objects.get(user__username=username)
235 for username in booking_info['collaborators']]
236 data['hostname'] = booking_info['hostname']
237 data['length'] = booking_info['length']
238 data['installer'] = None
239 data['scenario'] = None
241 data['image'] = Image.objects.get(pk=booking_info['imageLabID'])
243 data['resource_template'] = ResourceTemplate.objects.get(pk=booking_info['templateID'])
244 data['lab'] = data['resource_template'].lab
247 if 'global_cloud_config' in data.keys():
248 data['global_cloud_config'] = CloudInitFile.objects.get(id=data['global_cloud_config'])
250 return _create_booking(data)
253 def create_ci_file(data: str) -> CloudInitFile:
256 if not (type(d) is dict):
257 raise Exception("CI file was valid yaml but was not a dict")
259 raise ValidationError("The provided Cloud Config is not valid yaml, please refer to the Cloud Init documentation for expected structure")
260 print("about to create global cloud config")
261 config = CloudInitFile.create(text=data, priority=CloudInitFile.objects.count())
262 print("made global cloud config")
268 def _create_booking(data):
269 check_invariants(**data)
271 # check booking privileges
272 # TODO: use the canonical booking_allowed method because now template might have multiple
274 if Booking.objects.filter(owner=data['owner'], end__gt=timezone.now()).count() >= 3 and not data['owner'].userprofile.booking_privledge:
275 raise PermissionError("You do not have permission to have more than 3 bookings at a time.")
277 ResourceManager.getInstance().templateIsReservable(data['resource_template'])
279 resource_template = update_template(data['resource_template'], data['image'], data['hostname'], data['owner'], global_cloud_config=data['global_cloud_config'])
281 # generate resource bundle
282 resource_bundle = generate_resource_bundle(resource_template)
285 booking = Booking.objects.create(
286 purpose=data['purpose'],
287 project=data['project'],
290 start=timezone.now(),
291 end=timezone.now() + timedelta(days=int(data['length'])),
292 resource=resource_bundle,
296 booking.pdf = PDFTemplater.makePDF(booking)
298 for collaborator in data['users']: # list of Users (not UserProfile)
299 booking.collaborators.add(collaborator.user)
304 JobFactory.makeCompleteJob(booking)
305 NotificationHandler.notify_new_booking(booking)
310 def drop_filter(user):
312 Return a dictionary that contains filters.
314 Only certain installlers are supported on certain images, etc
315 so the image filter indexed at [imageid][installerid] is truthy if
316 that installer is supported on that image
318 installer_filter = {}
321 images = Image.objects.filter(Q(public=True) | Q(owner=user))
324 image_filter[image.id] = {
325 'lab': 'lab_' + str(image.from_lab.lab_user.id),
326 'architecture': str(image.architecture),
331 templates = ResourceTemplate.objects.filter(Q(public=True) | Q(owner=user))
333 profiles = [conf.profile for conf in rt.getConfigs()]
334 resource_filter["resource_" + str(rt.id)] = [str(p.architecture) for p in profiles]
337 'installer_filter': json.dumps(installer_filter),
338 'scenario_filter': json.dumps(scenario_filter),
339 'image_filter': json.dumps(image_filter),
340 'resource_profile_map': json.dumps(resource_filter),