951ff47f22a9e7fee1f79574c5aa86f1a4011041
[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 from django.db.models import Q
13 from datetime import timedelta
14 from django.utils import timezone
15 from django.core.exceptions import ValidationError
16 from account.models import Lab
17
18 from resource_inventory.models import (
19     ResourceTemplate,
20     Installer,
21     Image,
22     OPNFVRole,
23     OPNFVConfig,
24     ResourceOPNFVConfig,
25     ResourceConfiguration,
26     NetworkConnection,
27     InterfaceConfiguration,
28     Network,
29 )
30 from resource_inventory.resource_manager import ResourceManager
31 from resource_inventory.pdf_templater import PDFTemplater
32 from notifier.manager import NotificationHandler
33 from booking.models import Booking
34 from dashboard.exceptions import BookingLengthException
35 from api.models import JobFactory
36
37
38 def parse_resource_field(resource_json):
39     """
40     Parse the json from the frontend.
41
42     returns a reference to the selected Lab and ResourceTemplate objects
43     """
44     lab, template = (None, None)
45     lab_dict = resource_json['lab']
46     for lab_info in lab_dict.values():
47         if lab_info['selected']:
48             lab = Lab.objects.get(lab_user__id=lab_info['id'])
49
50     resource_dict = resource_json['resource']
51     for resource_info in resource_dict.values():
52         if resource_info['selected']:
53             template = ResourceTemplate.objects.get(pk=resource_info['id'])
54
55     if lab is None:
56         raise ValidationError("No lab was selected")
57     if template is None:
58         raise ValidationError("No Host was selected")
59
60     return lab, template
61
62
63 def update_template(old_template, image, hostname, user):
64     """
65     Duplicate a template to the users account and update configured fields.
66
67     The dashboard presents users with preconfigured resource templates,
68     but the user might want to make small modifications, e.g hostname and
69     linux distro. So we copy the public template and create a private version
70     to the user's profile, and mark it temporary. When the booking ends the
71     new template is deleted
72     """
73     name = user.username + "'s Copy of '" + old_template.name + "'"
74     num_copies = ResourceTemplate.objects.filter(name__startswith=name).count()
75     template = ResourceTemplate.objects.create(
76         name=name if num_copies == 0 else name + " (" + str(num_copies) + ")",
77         xml=old_template.xml,
78         owner=user,
79         lab=old_template.lab,
80         description=old_template.description,
81         public=False,
82         temporary=True
83     )
84
85     for old_network in old_template.networks.all():
86         Network.objects.create(
87             name=old_network.name,
88             bundle=old_template,
89             is_public=False
90         )
91     # We are assuming there is only one opnfv config per public resource template
92     old_opnfv = template.opnfv_config.first()
93     if old_opnfv:
94         opnfv_config = OPNFVConfig.objects.create(
95             installer=old_opnfv.installer,
96             scenario=old_opnfv.installer,
97             template=template,
98             name=old_opnfv.installer,
99         )
100     # I am explicitly leaving opnfv_config.networks empty to avoid
101     # problems with duplicated / shared networks. In the quick deploy,
102     # there is never multiple networks anyway. This may have to change in the future
103
104     for old_config in old_template.getConfigs():
105         config = ResourceConfiguration.objects.create(
106             profile=old_config.profile,
107             image=image,
108             template=template
109         )
110
111         for old_iface_config in old_config.interface_configs.all():
112             iface_config = InterfaceConfiguration.objects.create(
113                 profile=old_iface_config.profile,
114                 resource_config=config
115             )
116
117             for old_connection in old_iface_config.connections.all():
118                 iface_config.connections.add(NetworkConnection.objects.create(
119                     network=template.networks.get(name=old_connection.network.name),
120                     vlan_is_tagged=old_connection.vlan_is_tagged
121                 ))
122
123         for old_res_opnfv in old_config.resource_opnfv_config.all():
124             if old_opnfv:
125                 ResourceOPNFVConfig.objects.create(
126                     role=old_opnfv.role,
127                     resource_config=config,
128                     opnfv_config=opnfv_config
129                 )
130
131
132 def generate_opnfvconfig(scenario, installer, template):
133     return OPNFVConfig.objects.create(
134         scenario=scenario,
135         installer=installer,
136         template=template
137     )
138
139
140 def generate_hostopnfv(hostconfig, opnfvconfig):
141     role = None
142     try:
143         role = OPNFVRole.objects.get(name="Jumphost")
144     except Exception:
145         role = OPNFVRole.objects.create(
146             name="Jumphost",
147             description="Single server jumphost role"
148         )
149     return ResourceOPNFVConfig.objects.create(
150         role=role,
151         host_config=hostconfig,
152         opnfv_config=opnfvconfig
153     )
154
155
156 def generate_resource_bundle(template):
157     resource_manager = ResourceManager.getInstance()
158     resource_bundle = resource_manager.instantiateTemplate(template)
159     return resource_bundle
160
161
162 def check_invariants(request, **kwargs):
163     # TODO: This should really happen in the BookingForm validation methods
164     installer = kwargs['installer']
165     image = kwargs['image']
166     scenario = kwargs['scenario']
167     lab = kwargs['lab']
168     resource_template = kwargs['resource_template']
169     length = kwargs['length']
170     # check that image os is compatible with installer
171     if installer in image.os.sup_installers.all():
172         # if installer not here, we can omit that and not check for scenario
173         if not scenario:
174             raise ValidationError("An OPNFV Installer needs a scenario to be chosen to work properly")
175         if scenario not in installer.sup_scenarios.all():
176             raise ValidationError("The chosen installer does not support the chosen scenario")
177     if image.from_lab != lab:
178         raise ValidationError("The chosen image is not available at the chosen hosting lab")
179     #TODO
180     #if image.host_type != host_profile:
181     #    raise ValidationError("The chosen image is not available for the chosen host type")
182     if not image.public and image.owner != request.user:
183         raise ValidationError("You are not the owner of the chosen private image")
184     if length < 1 or length > 21:
185         raise BookingLengthException("Booking must be between 1 and 21 days long")
186
187
188 def create_from_form(form, request):
189     """
190     Create a Booking from the user's form.
191
192     Large, nasty method to create a booking or return a useful error
193     based on the form from the frontend
194     """
195     resource_field = form.cleaned_data['filter_field']
196     purpose_field = form.cleaned_data['purpose']
197     project_field = form.cleaned_data['project']
198     users_field = form.cleaned_data['users']
199     hostname = form.cleaned_data['hostname']
200     length = form.cleaned_data['length']
201
202     image = form.cleaned_data['image']
203     scenario = form.cleaned_data['scenario']
204     installer = form.cleaned_data['installer']
205
206     lab, resource_template = parse_resource_field(resource_field)
207     data = form.cleaned_data
208     data['lab'] = lab
209     data['resource_template'] = resource_template
210     check_invariants(request, **data)
211
212     # check booking privileges
213     # TODO: use the canonical booking_allowed method because now template might have multiple
214     # machines
215     if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
216         raise PermissionError("You do not have permission to have more than 3 bookings at a time.")
217
218     ResourceManager.getInstance().templateIsReservable(resource_template)
219
220     hconf = update_template(resource_template, image, hostname, request.user)
221
222     # if no installer provided, just create blank host
223     opnfv_config = None
224     if installer:
225         opnfv_config = generate_opnfvconfig(scenario, installer, resource_template)
226         generate_hostopnfv(hconf, opnfv_config)
227
228     # generate resource bundle
229     resource_bundle = generate_resource_bundle(resource_template)
230
231     # generate booking
232     booking = Booking.objects.create(
233         purpose=purpose_field,
234         project=project_field,
235         lab=lab,
236         owner=request.user,
237         start=timezone.now(),
238         end=timezone.now() + timedelta(days=int(length)),
239         resource=resource_bundle,
240         opnfv_config=opnfv_config
241     )
242     booking.pdf = PDFTemplater.makePDF(booking)
243
244     for collaborator in users_field:  # list of UserProfiles
245         booking.collaborators.add(collaborator.user)
246
247     booking.save()
248
249     # generate job
250     JobFactory.makeCompleteJob(booking)
251     NotificationHandler.notify_new_booking(booking)
252
253     return booking
254
255
256 def drop_filter(user):
257     """
258     Return a dictionary that contains filters.
259
260     Only certain installlers are supported on certain images, etc
261     so the image filter indexed at [imageid][installerid] is truthy if
262     that installer is supported on that image
263     """
264     installer_filter = {}
265     for image in Image.objects.all():
266         installer_filter[image.id] = {}
267         for installer in image.os.sup_installers.all():
268             installer_filter[image.id][installer.id] = 1
269
270     scenario_filter = {}
271     for installer in Installer.objects.all():
272         scenario_filter[installer.id] = {}
273         for scenario in installer.sup_scenarios.all():
274             scenario_filter[installer.id][scenario.id] = 1
275
276     images = Image.objects.filter(Q(public=True) | Q(owner=user))
277     image_filter = {}
278     for image in images:
279         image_filter[image.id] = {
280             'lab': 'lab_' + str(image.from_lab.lab_user.id),
281             'host_profile': str(image.host_type.id),
282             'name': image.name
283         }
284
285     resource_filter = {}
286     templates = ResourceTemplate.objects.filter(Q(public=True) | Q(owner=user))
287     for rt in templates:
288         profiles = [conf.profile for conf in rt.getConfigs()]
289         resource_filter["resource_" + str(rt.id)] = [str(p.id) for p in profiles]
290
291     return {
292         'installer_filter': json.dumps(installer_filter),
293         'scenario_filter': json.dumps(scenario_filter),
294         'image_filter': json.dumps(image_filter),
295         'resource_profile_map': json.dumps(resource_filter),
296     }