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