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