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