Merge "Enhance PDF/IDF Support"
[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 def parse_host_field(host_field_contents):
94     host_json = json.loads(host_field_contents)
95     lab_dict = host_json['labs'][0]
96     lab_id = list(lab_dict.keys())[0]
97     lab_user_id = int(lab_id.split("_")[-1])
98     lab = Lab.objects.get(lab_user__id=lab_user_id)
99
100     host_dict = host_json['hosts'][0]
101     profile_id = list(host_dict.keys())[0]
102     profile_id = int(profile_id.split("_")[-1])
103     profile = HostProfile.objects.get(id=profile_id)
104
105     # check validity of field data before trying to apply to models
106     if len(host_json['labs']) != 1:
107         raise NoLabSelectedError("No lab was selected")
108     if not lab:
109         raise LabDNE("Lab with provided ID does not exist")
110     if not profile:
111         raise HostProfileDNE("Host type with provided ID does not exist")
112
113     return lab, profile
114
115
116 def check_available_matching_host(lab, hostprofile):
117     available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
118     if hostprofile not in available_host_types:
119         # TODO: handle deleting generic resource in this instance along with grb
120         raise HostNotAvailable("Could not book selected host due to changed availability. Try again later")
121
122     hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
123     if not hostset.exists():
124         raise HostNotAvailable("Couldn't find any matching unbooked hosts")
125
126     return True
127
128
129 def generate_grb(owner, lab, common_id):
130     grbundle = GenericResourceBundle(owner=owner)
131     grbundle.lab = lab
132     grbundle.name = "grbundle for quick booking with uid " + common_id
133     grbundle.description = "grbundle created for quick-deploy booking"
134     grbundle.save()
135
136     return grbundle
137
138
139 def generate_gresource(bundle, hostname):
140     if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
141         raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
142     gresource = GenericResource(bundle=bundle, name=hostname)
143     gresource.save()
144
145     return gresource
146
147
148 def generate_ghost(generic_resource, host_profile):
149     ghost = GenericHost()
150     ghost.resource = generic_resource
151     ghost.profile = host_profile
152     ghost.save()
153
154     return ghost
155
156
157 def generate_config_bundle(owner, common_id, grbundle):
158     cbundle = ConfigBundle()
159     cbundle.owner = owner
160     cbundle.name = "configbundle for quick booking with uid " + common_id
161     cbundle.description = "configbundle created for quick-deploy booking"
162     cbundle.bundle = grbundle
163     cbundle.save()
164
165     return cbundle
166
167
168 def generate_opnfvconfig(scenario, installer, config_bundle):
169     opnfvconfig = OPNFVConfig()
170     opnfvconfig.scenario = scenario
171     opnfvconfig.installer = installer
172     opnfvconfig.bundle = config_bundle
173     opnfvconfig.save()
174
175     return opnfvconfig
176
177
178 def generate_hostconfig(generic_host, image, config_bundle):
179     hconf = HostConfiguration()
180     hconf.host = generic_host
181     hconf.image = image
182
183     opnfvrole = OPNFVRole.objects.get(name="Jumphost")
184     if not opnfvrole:
185         raise OPNFVRoleDNE("No jumphost role was found.")
186
187     hconf.opnfvRole = opnfvrole
188     hconf.bundle = config_bundle
189     hconf.save()
190
191     return hconf
192
193
194 def generate_resource_bundle(generic_resource_bundle, config_bundle):  # warning: requires cleanup
195     try:
196         resource_manager = ResourceManager.getInstance()
197         resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
198         return resource_bundle
199     except ResourceAvailabilityException:
200         raise ResourceAvailabilityException("Requested resources not available")
201     except ModelValidationException:
202         raise ModelValidationException("Encountered error while saving grbundle")
203
204
205 def check_invariants(request, **kwargs):
206     installer = kwargs['installer']
207     image = kwargs['image']
208     scenario = kwargs['scenario']
209     lab = kwargs['lab']
210     host_profile = kwargs['host_profile']
211     length = kwargs['length']
212     # check that image os is compatible with installer
213     if installer in image.os.sup_installers.all():
214         # if installer not here, we can omit that and not check for scenario
215         if not scenario:
216             raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
217         if scenario not in installer.sup_scenarios.all():
218             raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
219     if image.from_lab != lab:
220         raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
221     if image.host_type != host_profile:
222         raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
223     if not image.public and image.owner != request.user:
224         raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
225     if length < 1 or length > 21:
226         raise BookingLengthException("Booking must be between 1 and 21 days long")
227
228
229 def create_from_form(form, request):
230     quick_booking_id = str(uuid.uuid4())
231
232     host_field = form.cleaned_data['filter_field']
233     purpose_field = form.cleaned_data['purpose']
234     project_field = form.cleaned_data['project']
235     users_field = form.cleaned_data['users']
236     hostname = form.cleaned_data['hostname']
237     length = form.cleaned_data['length']
238
239     image = form.cleaned_data['image']
240     scenario = form.cleaned_data['scenario']
241     installer = form.cleaned_data['installer']
242
243     lab, host_profile = parse_host_field(host_field)
244     data = form.cleaned_data
245     data['lab'] = lab
246     data['host_profile'] = host_profile
247     check_invariants(request, **data)
248
249     check_available_matching_host(lab, host_profile)  # requires cleanup if failure after this point
250
251     grbundle = generate_grb(request.user, lab, quick_booking_id)
252
253     gresource = generate_gresource(grbundle, hostname)
254
255     ghost = generate_ghost(gresource, host_profile)
256
257     cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
258
259     # if no installer provided, just create blank host
260     if installer:
261         generate_opnfvconfig(scenario, installer, cbundle)
262
263     generate_hostconfig(ghost, image, cbundle)
264
265     # construct generic interfaces
266     for interface_profile in host_profile.interfaceprofile.all():
267         generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
268         generic_interface.save()
269
270     # get vlan, assign to first interface
271     publicnetwork = lab.vlan_manager.get_public_vlan()
272     if not publicnetwork:
273         raise NoRemainingPublicNetwork("No public networks were available for your pod")
274     publicvlan = publicnetwork.vlan
275     lab.vlan_manager.reserve_public_vlan(publicvlan)
276
277     vlan = Vlan.objects.create(vlan_id=publicvlan, tagged=False, public=True)
278     vlan.save()
279
280     ghost.generic_interfaces.first().vlans.add(vlan)
281     ghost.generic_interfaces.first().save()
282
283     # generate resource bundle
284     resource_bundle = generate_resource_bundle(grbundle, cbundle)
285
286     # generate booking
287     booking = Booking()
288     booking.purpose = purpose_field
289     booking.project = project_field
290     booking.lab = lab
291     booking.owner = request.user
292     booking.start = timezone.now()
293     booking.end = timezone.now() + timedelta(days=int(length))
294     booking.resource = resource_bundle
295     booking.pdf = PDFTemplater.makePDF(booking.resource)
296     booking.config_bundle = cbundle
297     booking.save()
298     users_field = users_field[2:-2]
299     if users_field:  # may be empty after split, if no collaborators entered
300         users_field = json.loads(users_field)
301         for collaborator in users_field:
302             user = User.objects.get(id=collaborator['id'])
303             booking.collaborators.add(user)
304         booking.save()
305
306     # generate job
307     JobFactory.makeCompleteJob(booking)
308     NotificationHandler.notify_new_booking(booking)
309
310
311 def drop_filter(user):
312     installer_filter = {}
313     for image in Image.objects.all():
314         installer_filter[image.id] = {}
315         for installer in image.os.sup_installers.all():
316             installer_filter[image.id][installer.id] = 1
317
318     scenario_filter = {}
319     for installer in Installer.objects.all():
320         scenario_filter[installer.id] = {}
321         for scenario in installer.sup_scenarios.all():
322             scenario_filter[installer.id][scenario.id] = 1
323
324     images = Image.objects.filter(Q(public=True) | Q(owner=user))
325     image_filter = {}
326     for image in images:
327         image_filter[image.id] = {}
328         image_filter[image.id]['lab'] = 'lab_' + str(image.from_lab.lab_user.id)
329         image_filter[image.id]['host_profile'] = 'host_' + str(image.host_type.id)
330         image_filter[image.id]['name'] = image.name
331
332     return {'installer_filter': json.dumps(installer_filter),
333             'scenario_filter': json.dumps(scenario_filter),
334             'image_filter': json.dumps(image_filter)}