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,
85 private_vlan_pool=old_template.private_vlan_pool,
86 public_vlan_pool=old_template.public_vlan_pool,
90 for old_network in old_template.networks.all():
91 Network.objects.create(
92 name=old_network.name,
94 is_public=old_network.is_public
96 # We are assuming there is only one opnfv config per public resource template
97 old_opnfv = template.opnfv_config.first()
99 opnfv_config = OPNFVConfig.objects.create(
100 installer=old_opnfv.installer,
101 scenario=old_opnfv.installer,
103 name=old_opnfv.installer,
105 # I am explicitly leaving opnfv_config.networks empty to avoid
106 # problems with duplicated / shared networks. In the quick deploy,
107 # there is never multiple networks anyway. This may have to change in the future
109 for old_config in old_template.getConfigs():
112 image_to_set = old_config.image
114 config = ResourceConfiguration.objects.create(
115 profile=old_config.profile,
118 is_head_node=old_config.is_head_node,
119 name=hostname if len(old_template.getConfigs()) == 1 else old_config.name,
120 # cloud_init_files=old_config.cloud_init_files.set()
123 for file in old_config.cloud_init_files.all():
124 config.cloud_init_files.add(file)
126 if global_cloud_config:
127 config.cloud_init_files.add(global_cloud_config)
130 for old_iface_config in old_config.interface_configs.all():
131 iface_config = InterfaceConfiguration.objects.create(
132 profile=old_iface_config.profile,
133 resource_config=config
136 for old_connection in old_iface_config.connections.all():
137 iface_config.connections.add(NetworkConnection.objects.create(
138 network=template.networks.get(name=old_connection.network.name),
139 vlan_is_tagged=old_connection.vlan_is_tagged
142 for old_res_opnfv in old_config.resource_opnfv_config.all():
144 ResourceOPNFVConfig.objects.create(
146 resource_config=config,
147 opnfv_config=opnfv_config
152 def generate_opnfvconfig(scenario, installer, template):
153 return OPNFVConfig.objects.create(
160 def generate_hostopnfv(hostconfig, opnfvconfig):
163 role = OPNFVRole.objects.get(name="Jumphost")
165 role = OPNFVRole.objects.create(
167 description="Single server jumphost role"
169 return ResourceOPNFVConfig.objects.create(
171 host_config=hostconfig,
172 opnfv_config=opnfvconfig
176 def generate_resource_bundle(template):
177 resource_manager = ResourceManager.getInstance()
178 resource_bundle = resource_manager.instantiateTemplate(template)
179 return resource_bundle
182 def check_invariants(**kwargs):
183 # TODO: This should really happen in the BookingForm validation methods
184 image = kwargs['image']
186 length = kwargs['length']
187 # check that image os is compatible with installer
189 if image.from_lab != lab:
190 raise ValidationError("The chosen image is not available at the chosen hosting lab")
192 # if image.host_type != host_profile:
193 # raise ValidationError("The chosen image is not available for the chosen host type")
194 if not image.public and image.owner != kwargs['owner']:
195 raise ValidationError("You are not the owner of the chosen private image")
196 if length < 1 or length > 21:
197 raise BookingLengthException("Booking must be between 1 and 21 days long")
200 def create_from_form(form, request):
202 Parse data from QuickBookingForm to create booking
204 resource_field = form.cleaned_data['filter_field']
205 # users_field = form.cleaned_data['users']
206 hostname = 'opnfv_host' if not form.cleaned_data['hostname'] else form.cleaned_data['hostname']
208 global_cloud_config = None if not form.cleaned_data['global_cloud_config'] else form.cleaned_data['global_cloud_config']
210 if global_cloud_config:
211 form.cleaned_data['global_cloud_config'] = create_ci_file(global_cloud_config)
213 # image = form.cleaned_data['image']
214 # scenario = form.cleaned_data['scenario']
215 # installer = form.cleaned_data['installer']
217 lab, resource_template = parse_resource_field(resource_field)
218 data = form.cleaned_data
219 data['hostname'] = hostname
221 data['resource_template'] = resource_template
222 data['owner'] = request.user
224 return _create_booking(data)
227 def create_from_API(body, user):
229 Parse data from Automation API to create booking
231 booking_info = json.loads(body.decode('utf-8'))
234 data['purpose'] = booking_info['purpose']
235 data['project'] = booking_info['project']
236 data['users'] = [UserProfile.objects.get(user__username=username)
237 for username in booking_info['collaborators']]
238 data['hostname'] = booking_info['hostname']
239 data['length'] = booking_info['length']
240 data['installer'] = None
241 data['scenario'] = None
243 data['image'] = Image.objects.get(pk=booking_info['imageLabID'])
245 data['resource_template'] = ResourceTemplate.objects.get(pk=booking_info['templateID'])
246 data['lab'] = data['resource_template'].lab
249 if 'global_cloud_config' in data.keys():
250 data['global_cloud_config'] = CloudInitFile.objects.get(id=data['global_cloud_config'])
252 return _create_booking(data)
255 def create_ci_file(data: str) -> CloudInitFile:
258 if not (type(d) is dict):
259 raise Exception("CI file was valid yaml but was not a dict")
261 raise ValidationError("The provided Cloud Config is not valid yaml, please refer to the Cloud Init documentation for expected structure")
262 print("about to create global cloud config")
263 config = CloudInitFile.create(text=data, priority=CloudInitFile.objects.count())
264 print("made global cloud config")
270 def _create_booking(data):
271 check_invariants(**data)
273 # check booking privileges
274 # TODO: use the canonical booking_allowed method because now template might have multiple
276 if Booking.objects.filter(owner=data['owner'], end__gt=timezone.now()).count() >= 3 and not data['owner'].userprofile.booking_privledge:
277 raise PermissionError("You do not have permission to have more than 3 bookings at a time.")
279 ResourceManager.getInstance().templateIsReservable(data['resource_template'])
281 resource_template = update_template(data['resource_template'], data['image'], data['hostname'], data['owner'], global_cloud_config=data['global_cloud_config'])
283 # generate resource bundle
284 resource_bundle = generate_resource_bundle(resource_template)
287 booking = Booking.objects.create(
288 purpose=data['purpose'],
289 project=data['project'],
292 start=timezone.now(),
293 end=timezone.now() + timedelta(days=int(data['length'])),
294 resource=resource_bundle,
298 booking.pdf = PDFTemplater.makePDF(booking)
300 for collaborator in data['users']: # list of Users (not UserProfile)
301 booking.collaborators.add(collaborator.user)
306 JobFactory.makeCompleteJob(booking)
307 NotificationHandler.notify_new_booking(booking)
312 def drop_filter(user):
314 Return a dictionary that contains filters.
316 Only certain installlers are supported on certain images, etc
317 so the image filter indexed at [imageid][installerid] is truthy if
318 that installer is supported on that image
320 installer_filter = {}
323 images = Image.objects.filter(Q(public=True) | Q(owner=user))
326 image_filter[image.id] = {
327 'lab': 'lab_' + str(image.from_lab.lab_user.id),
328 'architecture': str(image.architecture),
333 templates = ResourceTemplate.objects.filter(Q(public=True) | Q(owner=user))
335 profiles = [conf.profile for conf in rt.getConfigs()]
336 resource_filter["resource_" + str(rt.id)] = [str(p.architecture) for p in profiles]
339 'installer_filter': json.dumps(installer_filter),
340 'scenario_filter': json.dumps(scenario_filter),
341 'image_filter': json.dumps(image_filter),
342 'resource_profile_map': json.dumps(resource_filter),