Merge "Comments and Documentation"
[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 datetime import timedelta
16 from django.utils import timezone
17 from account.models import Lab
18
19 from resource_inventory.models import (
20     Installer,
21     Image,
22     GenericResourceBundle,
23     ConfigBundle,
24     Host,
25     HostProfile,
26     HostConfiguration,
27     GenericResource,
28     GenericHost,
29     GenericInterface,
30     OPNFVRole,
31     OPNFVConfig,
32     Network,
33     NetworkConnection,
34     NetworkRole,
35     HostOPNFVConfig,
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_json):
100     """
101     Parse the json from the frontend.
102
103     returns a reference to the selected Lab and HostProfile objects
104     """
105     lab, profile = (None, None)
106     lab_dict = host_json['lab']
107     for lab_info in lab_dict.values():
108         if lab_info['selected']:
109             lab = Lab.objects.get(lab_user__id=lab_info['id'])
110
111     host_dict = host_json['host']
112     for host_info in host_dict.values():
113         if host_info['selected']:
114             profile = HostProfile.objects.get(pk=host_info['id'])
115
116     if lab is None:
117         raise NoLabSelectedError("No lab was selected")
118     if profile is None:
119         raise HostProfileDNE("No Host was selected")
120
121     return lab, profile
122
123
124 def check_available_matching_host(lab, hostprofile):
125     """
126     Check the resources are available.
127
128     Returns true if the requested host type is availble,
129     Or throws an exception
130     """
131     available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
132     if hostprofile not in available_host_types:
133         # TODO: handle deleting generic resource in this instance along with grb
134         raise HostNotAvailable('Requested host type is not available. Please try again later. Host availability can be viewed in the "Hosts" tab to the left.')
135
136     hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
137     if not hostset.exists():
138         raise HostNotAvailable("Couldn't find any matching unbooked hosts")
139
140     return True
141
142
143 # Functions to create models
144
145 def generate_grb(owner, lab, common_id):
146     """Create a Generic Resource Bundle."""
147     grbundle = GenericResourceBundle(owner=owner)
148     grbundle.lab = lab
149     grbundle.name = "grbundle for quick booking with uid " + common_id
150     grbundle.description = "grbundle created for quick-deploy booking"
151     grbundle.save()
152
153     return grbundle
154
155
156 def generate_gresource(bundle, hostname):
157     """Create a Generic Resource."""
158     if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
159         raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
160     gresource = GenericResource(bundle=bundle, name=hostname)
161     gresource.save()
162
163     return gresource
164
165
166 def generate_ghost(generic_resource, host_profile):
167     """Create a Generic Host."""
168     ghost = GenericHost()
169     ghost.resource = generic_resource
170     ghost.profile = host_profile
171     ghost.save()
172
173     return ghost
174
175
176 def generate_config_bundle(owner, common_id, grbundle):
177     """Create a Configuration Bundle."""
178     cbundle = ConfigBundle()
179     cbundle.owner = owner
180     cbundle.name = "configbundle for quick booking with uid " + common_id
181     cbundle.description = "configbundle created for quick-deploy booking"
182     cbundle.bundle = grbundle
183     cbundle.save()
184
185     return cbundle
186
187
188 def generate_opnfvconfig(scenario, installer, config_bundle):
189     """Create an OPNFV Configuration."""
190     opnfvconfig = OPNFVConfig()
191     opnfvconfig.scenario = scenario
192     opnfvconfig.installer = installer
193     opnfvconfig.bundle = config_bundle
194     opnfvconfig.save()
195
196     return opnfvconfig
197
198
199 def generate_hostconfig(generic_host, image, config_bundle):
200     """Create a Host Configuration."""
201     hconf = HostConfiguration()
202     hconf.host = generic_host
203     hconf.image = image
204     hconf.bundle = config_bundle
205     hconf.is_head_node = True
206     hconf.save()
207
208     return hconf
209
210
211 def generate_hostopnfv(hostconfig, opnfvconfig):
212     """Relate the Host and OPNFV Configs."""
213     config = HostOPNFVConfig()
214     role = None
215     try:
216         role = OPNFVRole.objects.get(name="Jumphost")
217     except Exception:
218         role = OPNFVRole.objects.create(
219             name="Jumphost",
220             description="Single server jumphost role"
221         )
222     config.role = role
223     config.host_config = hostconfig
224     config.opnfv_config = opnfvconfig
225     config.save()
226     return config
227
228
229 def generate_resource_bundle(generic_resource_bundle, config_bundle):  # warning: requires cleanup
230     """Create a Resource Bundle."""
231     try:
232         resource_manager = ResourceManager.getInstance()
233         resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
234         return resource_bundle
235     except ResourceAvailabilityException:
236         raise ResourceAvailabilityException("Requested resources not available")
237     except ModelValidationException:
238         raise ModelValidationException("Encountered error while saving grbundle")
239
240
241 def check_invariants(request, **kwargs):
242     """
243     Verify all the contraints on the requested booking.
244
245     verifies software compatibility, booking length, etc
246     """
247     installer = kwargs['installer']
248     image = kwargs['image']
249     scenario = kwargs['scenario']
250     lab = kwargs['lab']
251     host_profile = kwargs['host_profile']
252     length = kwargs['length']
253     # check that image os is compatible with installer
254     if installer in image.os.sup_installers.all():
255         # if installer not here, we can omit that and not check for scenario
256         if not scenario:
257             raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
258         if scenario not in installer.sup_scenarios.all():
259             raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
260     if image.from_lab != lab:
261         raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
262     if image.host_type != host_profile:
263         raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
264     if not image.public and image.owner != request.user:
265         raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
266     if length < 1 or length > 21:
267         raise BookingLengthException("Booking must be between 1 and 21 days long")
268
269
270 def configure_networking(grb, config):
271     # create network
272     net = Network.objects.create(name="public", bundle=grb, is_public=True)
273     # connect network to generic host
274     grb.getResources()[0].generic_interfaces.first().connections.add(
275         NetworkConnection.objects.create(network=net, vlan_is_tagged=False)
276     )
277     # asign network role
278     role = NetworkRole.objects.create(name="public", network=net)
279     opnfv_config = config.opnfv_config.first()
280     if opnfv_config:
281         opnfv_config.networks.add(role)
282
283
284 def create_from_form(form, request):
285     """
286     Create a Booking from the user's form.
287
288     Large, nasty method to create a booking or return a useful error
289     based on the form from the frontend
290     """
291     quick_booking_id = str(uuid.uuid4())
292
293     host_field = form.cleaned_data['filter_field']
294     purpose_field = form.cleaned_data['purpose']
295     project_field = form.cleaned_data['project']
296     users_field = form.cleaned_data['users']
297     hostname = form.cleaned_data['hostname']
298     length = form.cleaned_data['length']
299
300     image = form.cleaned_data['image']
301     scenario = form.cleaned_data['scenario']
302     installer = form.cleaned_data['installer']
303
304     lab, host_profile = parse_host_field(host_field)
305     data = form.cleaned_data
306     data['lab'] = lab
307     data['host_profile'] = host_profile
308     check_invariants(request, **data)
309
310     # check booking privileges
311     if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
312         raise BookingPermissionException("You do not have permission to have more than 3 bookings at a time.")
313
314     check_available_matching_host(lab, host_profile)  # requires cleanup if failure after this point
315
316     grbundle = generate_grb(request.user, lab, quick_booking_id)
317     gresource = generate_gresource(grbundle, hostname)
318     ghost = generate_ghost(gresource, host_profile)
319     cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
320     hconf = generate_hostconfig(ghost, image, cbundle)
321
322     # if no installer provided, just create blank host
323     opnfv_config = None
324     if installer:
325         opnfv_config = generate_opnfvconfig(scenario, installer, cbundle)
326         generate_hostopnfv(hconf, opnfv_config)
327
328     # construct generic interfaces
329     for interface_profile in host_profile.interfaceprofile.all():
330         generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
331         generic_interface.save()
332
333     configure_networking(grbundle, cbundle)
334
335     # generate resource bundle
336     resource_bundle = generate_resource_bundle(grbundle, cbundle)
337
338     # generate booking
339     booking = Booking.objects.create(
340         purpose=purpose_field,
341         project=project_field,
342         lab=lab,
343         owner=request.user,
344         start=timezone.now(),
345         end=timezone.now() + timedelta(days=int(length)),
346         resource=resource_bundle,
347         config_bundle=cbundle,
348         opnfv_config=opnfv_config
349     )
350     booking.pdf = PDFTemplater.makePDF(booking)
351
352     for collaborator in users_field:  # list of UserProfiles
353         booking.collaborators.add(collaborator.user)
354
355     booking.save()
356
357     # generate job
358     JobFactory.makeCompleteJob(booking)
359     NotificationHandler.notify_new_booking(booking)
360
361     return booking
362
363
364 def drop_filter(user):
365     """
366     Return a dictionary that contains filters.
367
368     Only certain installlers are supported on certain images, etc
369     so the image filter indexed at [imageid][installerid] is truthy if
370     that installer is supported on that image
371     """
372     installer_filter = {}
373     for image in Image.objects.all():
374         installer_filter[image.id] = {}
375         for installer in image.os.sup_installers.all():
376             installer_filter[image.id][installer.id] = 1
377
378     scenario_filter = {}
379     for installer in Installer.objects.all():
380         scenario_filter[installer.id] = {}
381         for scenario in installer.sup_scenarios.all():
382             scenario_filter[installer.id][scenario.id] = 1
383
384     images = Image.objects.filter(Q(public=True) | Q(owner=user))
385     image_filter = {}
386     for image in images:
387         image_filter[image.id] = {}
388         image_filter[image.id]['lab'] = 'lab_' + str(image.from_lab.lab_user.id)
389         image_filter[image.id]['host_profile'] = 'host_' + str(image.host_type.id)
390         image_filter[image.id]['name'] = image.name
391
392     return {'installer_filter': json.dumps(installer_filter),
393             'scenario_filter': json.dumps(scenario_filter),
394             'image_filter': json.dumps(image_filter)}