Minor Cleanup enhancements
[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     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('Requested host type is not available. Please try again later. Host availability can be viewed in the "Hosts" tab to the left.')
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)}