31865bef692d7f7b7e99324a0fbe435cf620cf89
[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         copy_of=old_template
86     )
87
88     for old_network in old_template.networks.all():
89         Network.objects.create(
90             name=old_network.name,
91             bundle=template,
92             is_public=old_network.is_public
93         )
94     # We are assuming there is only one opnfv config per public resource template
95     old_opnfv = template.opnfv_config.first()
96     if old_opnfv:
97         opnfv_config = OPNFVConfig.objects.create(
98             installer=old_opnfv.installer,
99             scenario=old_opnfv.installer,
100             template=template,
101             name=old_opnfv.installer,
102         )
103     # I am explicitly leaving opnfv_config.networks empty to avoid
104     # problems with duplicated / shared networks. In the quick deploy,
105     # there is never multiple networks anyway. This may have to change in the future
106
107     for old_config in old_template.getConfigs():
108         image_to_set = image
109         if not image:
110             image_to_set = old_config.image
111
112         config = ResourceConfiguration.objects.create(
113             profile=old_config.profile,
114             image=image_to_set,
115             template=template,
116             is_head_node=old_config.is_head_node,
117             name=hostname if len(old_template.getConfigs()) == 1 else old_config.name,
118             # cloud_init_files=old_config.cloud_init_files.set()
119         )
120
121         for file in old_config.cloud_init_files.all():
122             config.cloud_init_files.add(file)
123
124         if global_cloud_config:
125             config.cloud_init_files.add(global_cloud_config)
126             config.save()
127
128         for old_iface_config in old_config.interface_configs.all():
129             iface_config = InterfaceConfiguration.objects.create(
130                 profile=old_iface_config.profile,
131                 resource_config=config
132             )
133
134             for old_connection in old_iface_config.connections.all():
135                 iface_config.connections.add(NetworkConnection.objects.create(
136                     network=template.networks.get(name=old_connection.network.name),
137                     vlan_is_tagged=old_connection.vlan_is_tagged
138                 ))
139
140         for old_res_opnfv in old_config.resource_opnfv_config.all():
141             if old_opnfv:
142                 ResourceOPNFVConfig.objects.create(
143                     role=old_opnfv.role,
144                     resource_config=config,
145                     opnfv_config=opnfv_config
146                 )
147     return template
148
149
150 def generate_opnfvconfig(scenario, installer, template):
151     return OPNFVConfig.objects.create(
152         scenario=scenario,
153         installer=installer,
154         template=template
155     )
156
157
158 def generate_hostopnfv(hostconfig, opnfvconfig):
159     role = None
160     try:
161         role = OPNFVRole.objects.get(name="Jumphost")
162     except Exception:
163         role = OPNFVRole.objects.create(
164             name="Jumphost",
165             description="Single server jumphost role"
166         )
167     return ResourceOPNFVConfig.objects.create(
168         role=role,
169         host_config=hostconfig,
170         opnfv_config=opnfvconfig
171     )
172
173
174 def generate_resource_bundle(template):
175     resource_manager = ResourceManager.getInstance()
176     resource_bundle = resource_manager.instantiateTemplate(template)
177     return resource_bundle
178
179
180 def check_invariants(**kwargs):
181     # TODO: This should really happen in the BookingForm validation methods
182     image = kwargs['image']
183     lab = kwargs['lab']
184     length = kwargs['length']
185     # check that image os is compatible with installer
186     if image:
187         if image.from_lab != lab:
188             raise ValidationError("The chosen image is not available at the chosen hosting lab")
189         # TODO
190         # if image.host_type != host_profile:
191         #    raise ValidationError("The chosen image is not available for the chosen host type")
192         if not image.public and image.owner != kwargs['owner']:
193             raise ValidationError("You are not the owner of the chosen private image")
194     if length < 1 or length > 21:
195         raise BookingLengthException("Booking must be between 1 and 21 days long")
196
197
198 def create_from_form(form, request):
199     """
200     Parse data from QuickBookingForm to create booking
201     """
202     resource_field = form.cleaned_data['filter_field']
203     # users_field = form.cleaned_data['users']
204     hostname = 'opnfv_host' if not form.cleaned_data['hostname'] else form.cleaned_data['hostname']
205
206     global_cloud_config = None if not form.cleaned_data['global_cloud_config'] else form.cleaned_data['global_cloud_config']
207
208     if global_cloud_config:
209         form.cleaned_data['global_cloud_config'] = create_ci_file(global_cloud_config)
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['hostname'] = hostname
218     data['lab'] = lab
219     data['resource_template'] = resource_template
220     data['owner'] = request.user
221
222     return _create_booking(data)
223
224
225 def create_from_API(body, user):
226     """
227     Parse data from Automation API to create booking
228     """
229     booking_info = json.loads(body.decode('utf-8'))
230
231     data = {}
232     data['purpose'] = booking_info['purpose']
233     data['project'] = booking_info['project']
234     data['users'] = [UserProfile.objects.get(user__username=username)
235                      for username in booking_info['collaborators']]
236     data['hostname'] = booking_info['hostname']
237     data['length'] = booking_info['length']
238     data['installer'] = None
239     data['scenario'] = None
240
241     data['image'] = Image.objects.get(pk=booking_info['imageLabID'])
242
243     data['resource_template'] = ResourceTemplate.objects.get(pk=booking_info['templateID'])
244     data['lab'] = data['resource_template'].lab
245     data['owner'] = user
246
247     if 'global_cloud_config' in data.keys():
248         data['global_cloud_config'] = CloudInitFile.objects.get(id=data['global_cloud_config'])
249
250     return _create_booking(data)
251
252
253 def create_ci_file(data: str) -> CloudInitFile:
254     try:
255         d = yaml.load(data)
256         if not (type(d) is dict):
257             raise Exception("CI file was valid yaml but was not a dict")
258     except Exception:
259         raise ValidationError("The provided Cloud Config is not valid yaml, please refer to the Cloud Init documentation for expected structure")
260     print("about to create global cloud config")
261     config = CloudInitFile.create(text=data, priority=CloudInitFile.objects.count())
262     print("made global cloud config")
263
264     return config
265
266
267 @transaction.atomic
268 def _create_booking(data):
269     check_invariants(**data)
270
271     # check booking privileges
272     # TODO: use the canonical booking_allowed method because now template might have multiple
273     # machines
274     if Booking.objects.filter(owner=data['owner'], end__gt=timezone.now()).count() >= 3 and not data['owner'].userprofile.booking_privledge:
275         raise PermissionError("You do not have permission to have more than 3 bookings at a time.")
276
277     ResourceManager.getInstance().templateIsReservable(data['resource_template'])
278
279     resource_template = update_template(data['resource_template'], data['image'], data['hostname'], data['owner'], global_cloud_config=data['global_cloud_config'])
280
281     # generate resource bundle
282     resource_bundle = generate_resource_bundle(resource_template)
283
284     # generate booking
285     booking = Booking.objects.create(
286         purpose=data['purpose'],
287         project=data['project'],
288         lab=data['lab'],
289         owner=data['owner'],
290         start=timezone.now(),
291         end=timezone.now() + timedelta(days=int(data['length'])),
292         resource=resource_bundle,
293         opnfv_config=None
294     )
295
296     booking.pdf = PDFTemplater.makePDF(booking)
297
298     for collaborator in data['users']:   # list of Users (not UserProfile)
299         booking.collaborators.add(collaborator.user)
300
301     booking.save()
302
303     # generate job
304     JobFactory.makeCompleteJob(booking)
305     NotificationHandler.notify_new_booking(booking)
306
307     return booking
308
309
310 def drop_filter(user):
311     """
312     Return a dictionary that contains filters.
313
314     Only certain installlers are supported on certain images, etc
315     so the image filter indexed at [imageid][installerid] is truthy if
316     that installer is supported on that image
317     """
318     installer_filter = {}
319     scenario_filter = {}
320
321     images = Image.objects.filter(Q(public=True) | Q(owner=user))
322     image_filter = {}
323     for image in images:
324         image_filter[image.id] = {
325             'lab': 'lab_' + str(image.from_lab.lab_user.id),
326             'architecture': str(image.architecture),
327             'name': image.name
328         }
329
330     resource_filter = {}
331     templates = ResourceTemplate.objects.filter(Q(public=True) | Q(owner=user))
332     for rt in templates:
333         profiles = [conf.profile for conf in rt.getConfigs()]
334         resource_filter["resource_" + str(rt.id)] = [str(p.architecture) for p in profiles]
335
336     return {
337         'installer_filter': json.dumps(installer_filter),
338         'scenario_filter': json.dumps(scenario_filter),
339         'image_filter': json.dumps(image_filter),
340         'resource_profile_map': json.dumps(resource_filter),
341     }