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