Fixing Network Models
[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 )
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("Could not book selected host due to changed availability. Try again later")
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)}