Limit total number of active bookings per user
[laas.git] / src / booking / quick_deployer.py
1 ##############################################################################
2 # Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
3 #
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 ##############################################################################
9
10
11 import json
12 import uuid
13 import re
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
19
20 from resource_inventory.models import (
21     Installer,
22     Image,
23     GenericResourceBundle,
24     ConfigBundle,
25     Vlan,
26     Host,
27     HostProfile,
28     HostConfiguration,
29     GenericResource,
30     GenericHost,
31     GenericInterface,
32     OPNFVRole,
33     OPNFVConfig
34 )
35 from resource_inventory.resource_manager import ResourceManager
36 from resource_inventory.pdf_templater import PDFTemplater
37 from notifier.manager import NotificationHandler
38 from booking.models import Booking
39 from dashboard.exceptions import (
40     InvalidHostnameException,
41     ResourceAvailabilityException,
42     ModelValidationException,
43     BookingLengthException
44 )
45 from api.models import JobFactory
46
47
48 # model validity exceptions
49 class IncompatibleInstallerForOS(Exception):
50     pass
51
52
53 class IncompatibleScenarioForInstaller(Exception):
54     pass
55
56
57 class IncompatibleImageForHost(Exception):
58     pass
59
60
61 class ImageOwnershipInvalid(Exception):
62     pass
63
64
65 class ImageNotAvailableAtLab(Exception):
66     pass
67
68
69 class LabDNE(Exception):
70     pass
71
72
73 class HostProfileDNE(Exception):
74     pass
75
76
77 class HostNotAvailable(Exception):
78     pass
79
80
81 class NoLabSelectedError(Exception):
82     pass
83
84
85 class OPNFVRoleDNE(Exception):
86     pass
87
88
89 class NoRemainingPublicNetwork(Exception):
90     pass
91
92
93 class BookingPermissionException(Exception):
94     pass
95
96
97 def parse_host_field(host_field_contents):
98     host_json = json.loads(host_field_contents)
99     lab_dict = host_json['labs'][0]
100     lab_id = list(lab_dict.keys())[0]
101     lab_user_id = int(lab_id.split("_")[-1])
102     lab = Lab.objects.get(lab_user__id=lab_user_id)
103
104     host_dict = host_json['hosts'][0]
105     profile_id = list(host_dict.keys())[0]
106     profile_id = int(profile_id.split("_")[-1])
107     profile = HostProfile.objects.get(id=profile_id)
108
109     # check validity of field data before trying to apply to models
110     if len(host_json['labs']) != 1:
111         raise NoLabSelectedError("No lab was selected")
112     if not lab:
113         raise LabDNE("Lab with provided ID does not exist")
114     if not profile:
115         raise HostProfileDNE("Host type with provided ID does not exist")
116
117     return lab, profile
118
119
120 def check_available_matching_host(lab, hostprofile):
121     available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
122     if hostprofile not in available_host_types:
123         # TODO: handle deleting generic resource in this instance along with grb
124         raise HostNotAvailable('Requested host type is not available. Please try again later. Host availability can be viewed in the "Hosts" tab to the left.')
125
126     hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
127     if not hostset.exists():
128         raise HostNotAvailable("Couldn't find any matching unbooked hosts")
129
130     return True
131
132
133 def generate_grb(owner, lab, common_id):
134     grbundle = GenericResourceBundle(owner=owner)
135     grbundle.lab = lab
136     grbundle.name = "grbundle for quick booking with uid " + common_id
137     grbundle.description = "grbundle created for quick-deploy booking"
138     grbundle.save()
139
140     return grbundle
141
142
143 def generate_gresource(bundle, hostname):
144     if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
145         raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
146     gresource = GenericResource(bundle=bundle, name=hostname)
147     gresource.save()
148
149     return gresource
150
151
152 def generate_ghost(generic_resource, host_profile):
153     ghost = GenericHost()
154     ghost.resource = generic_resource
155     ghost.profile = host_profile
156     ghost.save()
157
158     return ghost
159
160
161 def generate_config_bundle(owner, common_id, grbundle):
162     cbundle = ConfigBundle()
163     cbundle.owner = owner
164     cbundle.name = "configbundle for quick booking with uid " + common_id
165     cbundle.description = "configbundle created for quick-deploy booking"
166     cbundle.bundle = grbundle
167     cbundle.save()
168
169     return cbundle
170
171
172 def generate_opnfvconfig(scenario, installer, config_bundle):
173     opnfvconfig = OPNFVConfig()
174     opnfvconfig.scenario = scenario
175     opnfvconfig.installer = installer
176     opnfvconfig.bundle = config_bundle
177     opnfvconfig.save()
178
179     return opnfvconfig
180
181
182 def generate_hostconfig(generic_host, image, config_bundle):
183     hconf = HostConfiguration()
184     hconf.host = generic_host
185     hconf.image = image
186
187     opnfvrole = OPNFVRole.objects.get(name="Jumphost")
188     if not opnfvrole:
189         raise OPNFVRoleDNE("No jumphost role was found.")
190
191     hconf.opnfvRole = opnfvrole
192     hconf.bundle = config_bundle
193     hconf.save()
194
195     return hconf
196
197
198 def generate_resource_bundle(generic_resource_bundle, config_bundle):  # warning: requires cleanup
199     try:
200         resource_manager = ResourceManager.getInstance()
201         resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
202         return resource_bundle
203     except ResourceAvailabilityException:
204         raise ResourceAvailabilityException("Requested resources not available")
205     except ModelValidationException:
206         raise ModelValidationException("Encountered error while saving grbundle")
207
208
209 def check_invariants(request, **kwargs):
210     installer = kwargs['installer']
211     image = kwargs['image']
212     scenario = kwargs['scenario']
213     lab = kwargs['lab']
214     host_profile = kwargs['host_profile']
215     length = kwargs['length']
216     # check that image os is compatible with installer
217     if installer in image.os.sup_installers.all():
218         # if installer not here, we can omit that and not check for scenario
219         if not scenario:
220             raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
221         if scenario not in installer.sup_scenarios.all():
222             raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
223     if image.from_lab != lab:
224         raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
225     if image.host_type != host_profile:
226         raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
227     if not image.public and image.owner != request.user:
228         raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
229     if length < 1 or length > 21:
230         raise BookingLengthException("Booking must be between 1 and 21 days long")
231
232
233 def create_from_form(form, request):
234     quick_booking_id = str(uuid.uuid4())
235
236     host_field = form.cleaned_data['filter_field']
237     purpose_field = form.cleaned_data['purpose']
238     project_field = form.cleaned_data['project']
239     users_field = form.cleaned_data['users']
240     hostname = form.cleaned_data['hostname']
241     length = form.cleaned_data['length']
242
243     image = form.cleaned_data['image']
244     scenario = form.cleaned_data['scenario']
245     installer = form.cleaned_data['installer']
246
247     lab, host_profile = parse_host_field(host_field)
248     data = form.cleaned_data
249     data['lab'] = lab
250     data['host_profile'] = host_profile
251     check_invariants(request, **data)
252
253     # check booking privileges
254     if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
255         raise BookingPermissionException("You do not have permission to have more than 3 bookings at a time.")
256
257     check_available_matching_host(lab, host_profile)  # requires cleanup if failure after this point
258
259     grbundle = generate_grb(request.user, lab, quick_booking_id)
260
261     gresource = generate_gresource(grbundle, hostname)
262
263     ghost = generate_ghost(gresource, host_profile)
264
265     cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
266
267     # if no installer provided, just create blank host
268     if installer:
269         generate_opnfvconfig(scenario, installer, cbundle)
270
271     generate_hostconfig(ghost, image, cbundle)
272
273     # construct generic interfaces
274     for interface_profile in host_profile.interfaceprofile.all():
275         generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
276         generic_interface.save()
277
278     # get vlan, assign to first interface
279     publicnetwork = lab.vlan_manager.get_public_vlan()
280     if not publicnetwork:
281         raise NoRemainingPublicNetwork("No public networks were available for your pod")
282     publicvlan = publicnetwork.vlan
283     lab.vlan_manager.reserve_public_vlan(publicvlan)
284
285     vlan = Vlan.objects.create(vlan_id=publicvlan, tagged=False, public=True)
286     vlan.save()
287
288     ghost.generic_interfaces.first().vlans.add(vlan)
289     ghost.generic_interfaces.first().save()
290
291     # generate resource bundle
292     resource_bundle = generate_resource_bundle(grbundle, cbundle)
293
294     # generate booking
295     booking = Booking()
296     booking.purpose = purpose_field
297     booking.project = project_field
298     booking.lab = lab
299     booking.owner = request.user
300     booking.start = timezone.now()
301     booking.end = timezone.now() + timedelta(days=int(length))
302     booking.resource = resource_bundle
303     booking.pdf = PDFTemplater.makePDF(booking.resource)
304     booking.config_bundle = cbundle
305     booking.save()
306     users_field = users_field[2:-2]
307     if users_field:  # may be empty after split, if no collaborators entered
308         users_field = json.loads(users_field)
309         for collaborator in users_field:
310             user = User.objects.get(id=collaborator['id'])
311             booking.collaborators.add(user)
312         booking.save()
313
314     # generate job
315     JobFactory.makeCompleteJob(booking)
316     NotificationHandler.notify_new_booking(booking)
317
318
319 def drop_filter(user):
320     installer_filter = {}
321     for image in Image.objects.all():
322         installer_filter[image.id] = {}
323         for installer in image.os.sup_installers.all():
324             installer_filter[image.id][installer.id] = 1
325
326     scenario_filter = {}
327     for installer in Installer.objects.all():
328         scenario_filter[installer.id] = {}
329         for scenario in installer.sup_scenarios.all():
330             scenario_filter[installer.id][scenario.id] = 1
331
332     images = Image.objects.filter(Q(public=True) | Q(owner=user))
333     image_filter = {}
334     for image in images:
335         image_filter[image.id] = {}
336         image_filter[image.id]['lab'] = 'lab_' + str(image.from_lab.lab_user.id)
337         image_filter[image.id]['host_profile'] = 'host_' + str(image.host_type.id)
338         image_filter[image.id]['name'] = image.name
339
340     return {'installer_filter': json.dumps(installer_filter),
341             'scenario_filter': json.dumps(scenario_filter),
342             'image_filter': json.dumps(image_filter)}