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