ac69c8c4e589a34cd1730e52ea2f159e4f52221e
[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_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     hconf.bundle = config_bundle
189     hconf.is_head_node = True
190     hconf.save()
191
192     return hconf
193
194
195 def generate_hostopnfv(hostconfig, opnfvconfig):
196     config = HostOPNFVConfig()
197     role = None
198     try:
199         role = OPNFVRole.objects.get(name="Jumphost")
200     except Exception:
201         role = OPNFVRole.objects.create(
202             name="Jumphost",
203             description="Single server jumphost role"
204         )
205     config.role = role
206     config.host_config = hostconfig
207     config.opnfv_config = opnfvconfig
208     config.save()
209     return config
210
211
212 def generate_resource_bundle(generic_resource_bundle, config_bundle):  # warning: requires cleanup
213     try:
214         resource_manager = ResourceManager.getInstance()
215         resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
216         return resource_bundle
217     except ResourceAvailabilityException:
218         raise ResourceAvailabilityException("Requested resources not available")
219     except ModelValidationException:
220         raise ModelValidationException("Encountered error while saving grbundle")
221
222
223 def check_invariants(request, **kwargs):
224     installer = kwargs['installer']
225     image = kwargs['image']
226     scenario = kwargs['scenario']
227     lab = kwargs['lab']
228     host_profile = kwargs['host_profile']
229     length = kwargs['length']
230     # check that image os is compatible with installer
231     if installer in image.os.sup_installers.all():
232         # if installer not here, we can omit that and not check for scenario
233         if not scenario:
234             raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
235         if scenario not in installer.sup_scenarios.all():
236             raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
237     if image.from_lab != lab:
238         raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
239     if image.host_type != host_profile:
240         raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
241     if not image.public and image.owner != request.user:
242         raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
243     if length < 1 or length > 21:
244         raise BookingLengthException("Booking must be between 1 and 21 days long")
245
246
247 def configure_networking(grb, config):
248     # create network
249     net = Network.objects.create(name="public", bundle=grb, is_public=True)
250     # connect network to generic host
251     grb.getHosts()[0].generic_interfaces.first().connections.add(
252         NetworkConnection.objects.create(network=net, vlan_is_tagged=False)
253     )
254     # asign network role
255     role = NetworkRole.objects.create(name="public", network=net)
256     opnfv_config = config.opnfv_config.first()
257     if opnfv_config:
258         opnfv_config.networks.add(role)
259
260
261 def create_from_form(form, request):
262     quick_booking_id = str(uuid.uuid4())
263
264     host_field = form.cleaned_data['filter_field']
265     purpose_field = form.cleaned_data['purpose']
266     project_field = form.cleaned_data['project']
267     users_field = form.cleaned_data['users']
268     hostname = form.cleaned_data['hostname']
269     length = form.cleaned_data['length']
270
271     image = form.cleaned_data['image']
272     scenario = form.cleaned_data['scenario']
273     installer = form.cleaned_data['installer']
274
275     lab, host_profile = parse_host_field(host_field)
276     data = form.cleaned_data
277     data['lab'] = lab
278     data['host_profile'] = host_profile
279     check_invariants(request, **data)
280
281     # check booking privileges
282     if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
283         raise BookingPermissionException("You do not have permission to have more than 3 bookings at a time.")
284
285     check_available_matching_host(lab, host_profile)  # requires cleanup if failure after this point
286
287     grbundle = generate_grb(request.user, lab, quick_booking_id)
288     gresource = generate_gresource(grbundle, hostname)
289     ghost = generate_ghost(gresource, host_profile)
290     cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
291     hconf = generate_hostconfig(ghost, image, cbundle)
292
293     # if no installer provided, just create blank host
294     opnfv_config = None
295     if installer:
296         opnfv_config = generate_opnfvconfig(scenario, installer, cbundle)
297         generate_hostopnfv(hconf, opnfv_config)
298
299     # construct generic interfaces
300     for interface_profile in host_profile.interfaceprofile.all():
301         generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
302         generic_interface.save()
303
304     configure_networking(grbundle, cbundle)
305
306     # generate resource bundle
307     resource_bundle = generate_resource_bundle(grbundle, cbundle)
308
309     # generate booking
310     booking = Booking.objects.create(
311         purpose=purpose_field,
312         project=project_field,
313         lab=lab,
314         owner=request.user,
315         start=timezone.now(),
316         end=timezone.now() + timedelta(days=int(length)),
317         resource=resource_bundle,
318         config_bundle=cbundle,
319         opnfv_config=opnfv_config
320     )
321     booking.pdf = PDFTemplater.makePDF(booking)
322
323     for collaborator in users_field:  # list of UserProfiles
324         booking.collaborators.add(collaborator.user)
325
326     booking.save()
327
328     # generate job
329     JobFactory.makeCompleteJob(booking)
330     NotificationHandler.notify_new_booking(booking)
331
332
333 def drop_filter(user):
334     installer_filter = {}
335     for image in Image.objects.all():
336         installer_filter[image.id] = {}
337         for installer in image.os.sup_installers.all():
338             installer_filter[image.id][installer.id] = 1
339
340     scenario_filter = {}
341     for installer in Installer.objects.all():
342         scenario_filter[installer.id] = {}
343         for scenario in installer.sup_scenarios.all():
344             scenario_filter[installer.id][scenario.id] = 1
345
346     images = Image.objects.filter(Q(public=True) | Q(owner=user))
347     image_filter = {}
348     for image in images:
349         image_filter[image.id] = {}
350         image_filter[image.id]['lab'] = 'lab_' + str(image.from_lab.lab_user.id)
351         image_filter[image.id]['host_profile'] = 'host_' + str(image.host_type.id)
352         image_filter[image.id]['name'] = image.name
353
354     return {'installer_filter': json.dumps(installer_filter),
355             'scenario_filter': json.dumps(scenario_filter),
356             'image_filter': json.dumps(image_filter)}