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