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