Merge master for RC
[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         copy_of=old_template
84     )
85
86     for old_network in old_template.networks.all():
87         Network.objects.create(
88             name=old_network.name,
89             bundle=template,
90             is_public=old_network.is_public
91         )
92     # We are assuming there is only one opnfv config per public resource template
93     old_opnfv = template.opnfv_config.first()
94     if old_opnfv:
95         opnfv_config = OPNFVConfig.objects.create(
96             installer=old_opnfv.installer,
97             scenario=old_opnfv.installer,
98             template=template,
99             name=old_opnfv.installer,
100         )
101     # I am explicitly leaving opnfv_config.networks empty to avoid
102     # problems with duplicated / shared networks. In the quick deploy,
103     # there is never multiple networks anyway. This may have to change in the future
104
105     for old_config in old_template.getConfigs():
106         image_to_set = image
107         if not image:
108             image_to_set = old_config.image
109
110         config = ResourceConfiguration.objects.create(
111             profile=old_config.profile,
112             image=image_to_set,
113             template=template,
114             is_head_node=old_config.is_head_node,
115             name=hostname if len(old_template.getConfigs()) == 1 else old_config.name
116         )
117
118         for old_iface_config in old_config.interface_configs.all():
119             iface_config = InterfaceConfiguration.objects.create(
120                 profile=old_iface_config.profile,
121                 resource_config=config
122             )
123
124             for old_connection in old_iface_config.connections.all():
125                 iface_config.connections.add(NetworkConnection.objects.create(
126                     network=template.networks.get(name=old_connection.network.name),
127                     vlan_is_tagged=old_connection.vlan_is_tagged
128                 ))
129
130         for old_res_opnfv in old_config.resource_opnfv_config.all():
131             if old_opnfv:
132                 ResourceOPNFVConfig.objects.create(
133                     role=old_opnfv.role,
134                     resource_config=config,
135                     opnfv_config=opnfv_config
136                 )
137     return template
138
139
140 def generate_opnfvconfig(scenario, installer, template):
141     return OPNFVConfig.objects.create(
142         scenario=scenario,
143         installer=installer,
144         template=template
145     )
146
147
148 def generate_hostopnfv(hostconfig, opnfvconfig):
149     role = None
150     try:
151         role = OPNFVRole.objects.get(name="Jumphost")
152     except Exception:
153         role = OPNFVRole.objects.create(
154             name="Jumphost",
155             description="Single server jumphost role"
156         )
157     return ResourceOPNFVConfig.objects.create(
158         role=role,
159         host_config=hostconfig,
160         opnfv_config=opnfvconfig
161     )
162
163
164 def generate_resource_bundle(template):
165     resource_manager = ResourceManager.getInstance()
166     resource_bundle = resource_manager.instantiateTemplate(template)
167     return resource_bundle
168
169
170 def check_invariants(request, **kwargs):
171     # TODO: This should really happen in the BookingForm validation methods
172     installer = kwargs['installer']
173     image = kwargs['image']
174     scenario = kwargs['scenario']
175     lab = kwargs['lab']
176     length = kwargs['length']
177     # check that image os is compatible with installer
178     if image:
179         if installer or scenario:
180             if installer in image.os.sup_installers.all():
181                 # if installer not here, we can omit that and not check for scenario
182                 if not scenario:
183                     raise ValidationError("An OPNFV Installer needs a scenario to be chosen to work properly")
184                 if scenario not in installer.sup_scenarios.all():
185                     raise ValidationError("The chosen installer does not support the chosen scenario")
186         if image.from_lab != lab:
187             raise ValidationError("The chosen image is not available at the chosen hosting lab")
188         # TODO
189         # if image.host_type != host_profile:
190         #    raise ValidationError("The chosen image is not available for the chosen host type")
191         if not image.public and image.owner != request.user:
192             raise ValidationError("You are not the owner of the chosen private image")
193     if length < 1 or length > 21:
194         raise BookingLengthException("Booking must be between 1 and 21 days long")
195
196
197 def create_from_form(form, request):
198     """
199     Create a Booking from the user's form.
200
201     Large, nasty method to create a booking or return a useful error
202     based on the form from the frontend
203     """
204     resource_field = form.cleaned_data['filter_field']
205     purpose_field = form.cleaned_data['purpose']
206     project_field = form.cleaned_data['project']
207     users_field = form.cleaned_data['users']
208     hostname = 'opnfv_host' if not form.cleaned_data['hostname'] else form.cleaned_data['hostname']
209     length = form.cleaned_data['length']
210
211     image = form.cleaned_data['image']
212     scenario = form.cleaned_data['scenario']
213     installer = form.cleaned_data['installer']
214
215     lab, resource_template = parse_resource_field(resource_field)
216     data = form.cleaned_data
217     data['lab'] = lab
218     data['resource_template'] = resource_template
219     check_invariants(request, **data)
220
221     # check booking privileges
222     # TODO: use the canonical booking_allowed method because now template might have multiple
223     # machines
224     if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
225         raise PermissionError("You do not have permission to have more than 3 bookings at a time.")
226
227     ResourceManager.getInstance().templateIsReservable(resource_template)
228
229     resource_template = update_template(resource_template, image, hostname, request.user)
230
231     # if no installer provided, just create blank host
232     opnfv_config = None
233     if installer:
234         hconf = resource_template.getConfigs()[0]
235         opnfv_config = generate_opnfvconfig(scenario, installer, resource_template)
236         generate_hostopnfv(hconf, opnfv_config)
237
238     # generate resource bundle
239     resource_bundle = generate_resource_bundle(resource_template)
240
241     # generate booking
242     booking = Booking.objects.create(
243         purpose=purpose_field,
244         project=project_field,
245         lab=lab,
246         owner=request.user,
247         start=timezone.now(),
248         end=timezone.now() + timedelta(days=int(length)),
249         resource=resource_bundle,
250         opnfv_config=opnfv_config
251     )
252     booking.pdf = PDFTemplater.makePDF(booking)
253
254     for collaborator in users_field:  # list of UserProfiles
255         booking.collaborators.add(collaborator.user)
256
257     booking.save()
258
259     # generate job
260     JobFactory.makeCompleteJob(booking)
261     NotificationHandler.notify_new_booking(booking)
262
263     return booking
264
265
266 def drop_filter(user):
267     """
268     Return a dictionary that contains filters.
269
270     Only certain installlers are supported on certain images, etc
271     so the image filter indexed at [imageid][installerid] is truthy if
272     that installer is supported on that image
273     """
274     installer_filter = {}
275     for image in Image.objects.all():
276         installer_filter[image.id] = {}
277         for installer in image.os.sup_installers.all():
278             installer_filter[image.id][installer.id] = 1
279
280     scenario_filter = {}
281     for installer in Installer.objects.all():
282         scenario_filter[installer.id] = {}
283         for scenario in installer.sup_scenarios.all():
284             scenario_filter[installer.id][scenario.id] = 1
285
286     images = Image.objects.filter(Q(public=True) | Q(owner=user))
287     image_filter = {}
288     for image in images:
289         image_filter[image.id] = {
290             'lab': 'lab_' + str(image.from_lab.lab_user.id),
291             'host_profile': str(image.host_type.id),
292             'name': image.name
293         }
294
295     resource_filter = {}
296     templates = ResourceTemplate.objects.filter(Q(public=True) | Q(owner=user))
297     for rt in templates:
298         profiles = [conf.profile for conf in rt.getConfigs()]
299         resource_filter["resource_" + str(rt.id)] = [str(p.id) for p in profiles]
300
301     return {
302         'installer_filter': json.dumps(installer_filter),
303         'scenario_filter': json.dumps(scenario_filter),
304         'image_filter': json.dumps(image_filter),
305         'resource_profile_map': json.dumps(resource_filter),
306     }