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