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