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 ##############################################################################
14 from django.db.models import Q
15 from django.contrib.auth.models import User
16 from datetime import timedelta
17 from django.utils import timezone
18 from account.models import Lab
20 from resource_inventory.models import (
23 GenericResourceBundle,
38 from resource_inventory.resource_manager import ResourceManager
39 from resource_inventory.pdf_templater import PDFTemplater
40 from notifier.manager import NotificationHandler
41 from booking.models import Booking
42 from dashboard.exceptions import (
43 InvalidHostnameException,
44 ResourceAvailabilityException,
45 ModelValidationException,
46 BookingLengthException
48 from api.models import JobFactory
51 # model validity exceptions
52 class IncompatibleInstallerForOS(Exception):
56 class IncompatibleScenarioForInstaller(Exception):
60 class IncompatibleImageForHost(Exception):
64 class ImageOwnershipInvalid(Exception):
68 class ImageNotAvailableAtLab(Exception):
72 class LabDNE(Exception):
76 class HostProfileDNE(Exception):
80 class HostNotAvailable(Exception):
84 class NoLabSelectedError(Exception):
88 class OPNFVRoleDNE(Exception):
92 class NoRemainingPublicNetwork(Exception):
96 class BookingPermissionException(Exception):
100 def parse_host_field(host_field_contents):
101 host_json = json.loads(host_field_contents)
102 lab_dict = host_json['labs'][0]
103 lab_id = list(lab_dict.keys())[0]
104 lab_user_id = int(lab_id.split("_")[-1])
105 lab = Lab.objects.get(lab_user__id=lab_user_id)
107 host_dict = host_json['hosts'][0]
108 profile_id = list(host_dict.keys())[0]
109 profile_id = int(profile_id.split("_")[-1])
110 profile = HostProfile.objects.get(id=profile_id)
112 # check validity of field data before trying to apply to models
113 if len(host_json['labs']) != 1:
114 raise NoLabSelectedError("No lab was selected")
116 raise LabDNE("Lab with provided ID does not exist")
118 raise HostProfileDNE("Host type with provided ID does not exist")
123 def check_available_matching_host(lab, hostprofile):
124 available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
125 if hostprofile not in available_host_types:
126 # TODO: handle deleting generic resource in this instance along with grb
127 raise HostNotAvailable('Requested host type is not available. Please try again later. Host availability can be viewed in the "Hosts" tab to the left.')
129 hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
130 if not hostset.exists():
131 raise HostNotAvailable("Couldn't find any matching unbooked hosts")
136 def generate_grb(owner, lab, common_id):
137 grbundle = GenericResourceBundle(owner=owner)
139 grbundle.name = "grbundle for quick booking with uid " + common_id
140 grbundle.description = "grbundle created for quick-deploy booking"
146 def generate_gresource(bundle, hostname):
147 if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
148 raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
149 gresource = GenericResource(bundle=bundle, name=hostname)
155 def generate_ghost(generic_resource, host_profile):
156 ghost = GenericHost()
157 ghost.resource = generic_resource
158 ghost.profile = host_profile
164 def generate_config_bundle(owner, common_id, grbundle):
165 cbundle = ConfigBundle()
166 cbundle.owner = owner
167 cbundle.name = "configbundle for quick booking with uid " + common_id
168 cbundle.description = "configbundle created for quick-deploy booking"
169 cbundle.bundle = grbundle
175 def generate_opnfvconfig(scenario, installer, config_bundle):
176 opnfvconfig = OPNFVConfig()
177 opnfvconfig.scenario = scenario
178 opnfvconfig.installer = installer
179 opnfvconfig.bundle = config_bundle
185 def generate_hostconfig(generic_host, image, config_bundle):
186 hconf = HostConfiguration()
187 hconf.host = generic_host
189 hconf.bundle = config_bundle
190 hconf.is_head_node = True
196 def generate_hostopnfv(hostconfig, opnfvconfig):
197 config = HostOPNFVConfig()
200 role = OPNFVRole.objects.get(name="Jumphost")
202 role = OPNFVRole.objects.create(
204 description="Single server jumphost role"
207 config.host_config = hostconfig
208 config.opnfv_config = opnfvconfig
213 def generate_resource_bundle(generic_resource_bundle, config_bundle): # warning: requires cleanup
215 resource_manager = ResourceManager.getInstance()
216 resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
217 return resource_bundle
218 except ResourceAvailabilityException:
219 raise ResourceAvailabilityException("Requested resources not available")
220 except ModelValidationException:
221 raise ModelValidationException("Encountered error while saving grbundle")
224 def check_invariants(request, **kwargs):
225 installer = kwargs['installer']
226 image = kwargs['image']
227 scenario = kwargs['scenario']
229 host_profile = kwargs['host_profile']
230 length = kwargs['length']
231 # check that image os is compatible with installer
232 if installer in image.os.sup_installers.all():
233 # if installer not here, we can omit that and not check for scenario
235 raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
236 if scenario not in installer.sup_scenarios.all():
237 raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
238 if image.from_lab != lab:
239 raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
240 if image.host_type != host_profile:
241 raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
242 if not image.public and image.owner != request.user:
243 raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
244 if length < 1 or length > 21:
245 raise BookingLengthException("Booking must be between 1 and 21 days long")
248 def configure_networking(grb, config):
250 net = Network.objects.create(name="public", bundle=grb, is_public=True)
251 # connect network to generic host
252 grb.getHosts()[0].generic_interfaces.first().connections.add(
253 NetworkConnection.objects.create(network=net, vlan_is_tagged=False)
256 role = NetworkRole.objects.create(name="public", network=net)
257 opnfv_config = config.opnfv_config.first()
259 opnfv_config.networks.add(role)
262 def create_from_form(form, request):
263 quick_booking_id = str(uuid.uuid4())
265 host_field = form.cleaned_data['filter_field']
266 purpose_field = form.cleaned_data['purpose']
267 project_field = form.cleaned_data['project']
268 users_field = form.cleaned_data['users']
269 hostname = form.cleaned_data['hostname']
270 length = form.cleaned_data['length']
272 image = form.cleaned_data['image']
273 scenario = form.cleaned_data['scenario']
274 installer = form.cleaned_data['installer']
276 lab, host_profile = parse_host_field(host_field)
277 data = form.cleaned_data
279 data['host_profile'] = host_profile
280 check_invariants(request, **data)
282 # check booking privileges
283 if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
284 raise BookingPermissionException("You do not have permission to have more than 3 bookings at a time.")
286 check_available_matching_host(lab, host_profile) # requires cleanup if failure after this point
288 grbundle = generate_grb(request.user, lab, quick_booking_id)
289 gresource = generate_gresource(grbundle, hostname)
290 ghost = generate_ghost(gresource, host_profile)
291 cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
292 hconf = generate_hostconfig(ghost, image, cbundle)
294 # if no installer provided, just create blank host
297 opnfv_config = generate_opnfvconfig(scenario, installer, cbundle)
298 generate_hostopnfv(hconf, opnfv_config)
300 # construct generic interfaces
301 for interface_profile in host_profile.interfaceprofile.all():
302 generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
303 generic_interface.save()
305 configure_networking(grbundle, cbundle)
307 # generate resource bundle
308 resource_bundle = generate_resource_bundle(grbundle, cbundle)
311 booking = Booking.objects.create(
312 purpose=purpose_field,
313 project=project_field,
316 start=timezone.now(),
317 end=timezone.now() + timedelta(days=int(length)),
318 resource=resource_bundle,
319 config_bundle=cbundle,
320 opnfv_config=opnfv_config
322 booking.pdf = PDFTemplater.makePDF(booking)
324 users_field = users_field[2:-2]
325 if users_field: # may be empty after split, if no collaborators entered
326 users_field = json.loads(users_field)
327 for collaborator in users_field:
328 user = User.objects.get(id=collaborator['id'])
329 booking.collaborators.add(user)
334 JobFactory.makeCompleteJob(booking)
335 NotificationHandler.notify_new_booking(booking)
338 def drop_filter(user):
339 installer_filter = {}
340 for image in Image.objects.all():
341 installer_filter[image.id] = {}
342 for installer in image.os.sup_installers.all():
343 installer_filter[image.id][installer.id] = 1
346 for installer in Installer.objects.all():
347 scenario_filter[installer.id] = {}
348 for scenario in installer.sup_scenarios.all():
349 scenario_filter[installer.id][scenario.id] = 1
351 images = Image.objects.filter(Q(public=True) | Q(owner=user))
354 image_filter[image.id] = {}
355 image_filter[image.id]['lab'] = 'lab_' + str(image.from_lab.lab_user.id)
356 image_filter[image.id]['host_profile'] = 'host_' + str(image.host_type.id)
357 image_filter[image.id]['name'] = image.name
359 return {'installer_filter': json.dumps(installer_filter),
360 'scenario_filter': json.dumps(scenario_filter),
361 'image_filter': json.dumps(image_filter)}