1516374e2f5c634f6430f1f6e7b0b0016e7ddac7
[laas.git] / src / api / views.py
1 ##############################################################################
2 # Copyright (c) 2016 Max Breitenfeldt and others.
3 # Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
4 #
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9 ##############################################################################
10
11 import json
12 import math
13 import traceback
14 import sys
15 from datetime import timedelta
16
17 from django.contrib.auth.decorators import login_required
18 from django.shortcuts import redirect, get_object_or_404
19 from django.utils.decorators import method_decorator
20 from django.utils import timezone
21 from django.views import View
22 from django.http import HttpResponseNotFound
23 from django.http.response import JsonResponse, HttpResponse
24 from rest_framework import viewsets
25 from rest_framework.authtoken.models import Token
26 from django.views.decorators.csrf import csrf_exempt
27 from django.core.exceptions import ObjectDoesNotExist
28 from django.db.models import Q
29
30 from api.serializers.booking_serializer import BookingSerializer
31 from api.serializers.old_serializers import UserSerializer
32 from api.forms import DowntimeForm
33 from account.models import UserProfile, Lab
34 from booking.models import Booking
35 from booking.quick_deployer import create_from_API
36 from api.models import LabManagerTracker, get_task, Job, AutomationAPIManager, APILog
37 from notifier.manager import NotificationHandler
38 from analytics.models import ActiveVPNUser
39 from resource_inventory.models import (
40     Image,
41     Opsys,
42     CloudInitFile,
43     ResourceQuery,
44     ResourceTemplate,
45 )
46
47 import yaml
48 import uuid
49 from deepmerge import Merger
50
51 """
52 API views.
53
54 All functions return a Json blob
55 Most functions that deal with info from a specific lab (tasks, host info)
56 requires the Lab auth token.
57     for example, curl -H auth-token:mylabsauthtoken url
58
59 Most functions let you GET or POST to the same endpoint, and
60 the correct thing will happen
61 """
62
63
64 class BookingViewSet(viewsets.ModelViewSet):
65     queryset = Booking.objects.all()
66     serializer_class = BookingSerializer
67     filter_fields = ('resource', 'id')
68
69
70 class UserViewSet(viewsets.ModelViewSet):
71     queryset = UserProfile.objects.all()
72     serializer_class = UserSerializer
73
74
75 @method_decorator(login_required, name='dispatch')
76 class GenerateTokenView(View):
77     def get(self, request, *args, **kwargs):
78         user = self.request.user
79         token, created = Token.objects.get_or_create(user=user)
80         if not created:
81             token.delete()
82             Token.objects.create(user=user)
83         return redirect('account:settings')
84
85
86 def lab_inventory(request, lab_name=""):
87     lab_token = request.META.get('HTTP_AUTH_TOKEN')
88     lab_manager = LabManagerTracker.get(lab_name, lab_token)
89     return JsonResponse(lab_manager.get_inventory(), safe=False)
90
91
92 @csrf_exempt
93 def lab_host(request, lab_name="", host_id=""):
94     lab_token = request.META.get('HTTP_AUTH_TOKEN')
95     lab_manager = LabManagerTracker.get(lab_name, lab_token)
96     if request.method == "GET":
97         return JsonResponse(lab_manager.get_host(host_id), safe=False)
98     if request.method == "POST":
99         return JsonResponse(lab_manager.update_host(host_id, request.POST), safe=False)
100
101 # API extension for Cobbler integration
102
103
104 def all_images(request, lab_name=""):
105     a = []
106     for i in Image.objects.all():
107         a.append(i.serialize())
108     return JsonResponse(a, safe=False)
109
110
111 def all_opsyss(request, lab_name=""):
112     a = []
113     for opsys in Opsys.objects.all():
114         a.append(opsys.serialize())
115
116     return JsonResponse(a, safe=False)
117
118
119 @csrf_exempt
120 def single_image(request, lab_name="", image_id=""):
121     lab_token = request.META.get('HTTP_AUTH_TOKEN')
122     lab_manager = LabManagerTracker.get(lab_name, lab_token)
123     img = lab_manager.get_image(image_id).first()
124
125     if request.method == "GET":
126         if not img:
127             return HttpResponse(status=404)
128         return JsonResponse(img.serialize(), safe=False)
129
130     if request.method == "POST":
131         # get POST data
132         data = json.loads(request.body.decode('utf-8'))
133         if img:
134             img.update(data)
135         else:
136             # append lab name and the ID from the URL
137             data['from_lab_id'] = lab_name
138             data['lab_id'] = image_id
139
140             # create and save a new Image object
141             img = Image.new_from_data(data)
142
143         img.save()
144
145         # indicate success in response
146         return HttpResponse(status=200)
147     return HttpResponse(status=405)
148
149
150 @csrf_exempt
151 def single_opsys(request, lab_name="", opsys_id=""):
152     lab_token = request.META.get('HTTP_AUTH_TOKEN')
153     lab_manager = LabManagerTracker.get(lab_name, lab_token)
154     opsys = lab_manager.get_opsys(opsys_id).first()
155
156     if request.method == "GET":
157         if not opsys:
158             return HttpResponse(status=404)
159         return JsonResponse(opsys.serialize(), safe=False)
160
161     if request.method == "POST":
162         data = json.loads(request.body.decode('utf-8'))
163         if opsys:
164             opsys.update(data)
165         else:
166             # only name, available, and obsolete are needed to create an Opsys
167             # other fields are derived from the URL parameters
168             data['from_lab_id'] = lab_name
169             data['lab_id'] = opsys_id
170             opsys = Opsys.new_from_data(data)
171
172         opsys.save()
173         return HttpResponse(status=200)
174     return HttpResponse(status=405)
175
176 # end API extension
177
178
179 def get_pdf(request, lab_name="", booking_id=""):
180     lab_token = request.META.get('HTTP_AUTH_TOKEN')
181     lab_manager = LabManagerTracker.get(lab_name, lab_token)
182     return HttpResponse(lab_manager.get_pdf(booking_id), content_type="text/plain")
183
184
185 def get_idf(request, lab_name="", booking_id=""):
186     lab_token = request.META.get('HTTP_AUTH_TOKEN')
187     lab_manager = LabManagerTracker.get(lab_name, lab_token)
188     return HttpResponse(lab_manager.get_idf(booking_id), content_type="text/plain")
189
190
191 def lab_status(request, lab_name=""):
192     lab_token = request.META.get('HTTP_AUTH_TOKEN')
193     lab_manager = LabManagerTracker.get(lab_name, lab_token)
194     if request.method == "POST":
195         return JsonResponse(lab_manager.set_status(request.POST), safe=False)
196     return JsonResponse(lab_manager.get_status(), safe=False)
197
198
199 def lab_users(request, lab_name=""):
200     lab_token = request.META.get('HTTP_AUTH_TOKEN')
201     lab_manager = LabManagerTracker.get(lab_name, lab_token)
202     return HttpResponse(lab_manager.get_users(), content_type="text/plain")
203
204
205 def lab_user(request, lab_name="", user_id=-1):
206     lab_token = request.META.get('HTTP_AUTH_TOKEN')
207     lab_manager = LabManagerTracker.get(lab_name, lab_token)
208     return HttpResponse(lab_manager.get_user(user_id), content_type="text/plain")
209
210
211 @csrf_exempt
212 def update_host_bmc(request, lab_name="", host_id=""):
213     lab_token = request.META.get('HTTP_AUTH_TOKEN')
214     lab_manager = LabManagerTracker.get(lab_name, lab_token)
215     if request.method == "POST":
216         # update / create RemoteInfo for host
217         return JsonResponse(
218             lab_manager.update_host_remote_info(request.POST, host_id),
219             safe=False
220         )
221
222
223 def lab_profile(request, lab_name=""):
224     lab_token = request.META.get('HTTP_AUTH_TOKEN')
225     lab_manager = LabManagerTracker.get(lab_name, lab_token)
226     return JsonResponse(lab_manager.get_profile(), safe=False)
227
228
229 @csrf_exempt
230 def specific_task(request, lab_name="", job_id="", task_id=""):
231     lab_token = request.META.get('HTTP_AUTH_TOKEN')
232     LabManagerTracker.get(lab_name, lab_token)  # Authorize caller, but we dont need the result
233
234     if request.method == "POST":
235         task = get_task(task_id)
236         if 'status' in request.POST:
237             task.status = request.POST.get('status')
238         if 'message' in request.POST:
239             task.message = request.POST.get('message')
240         if 'lab_token' in request.POST:
241             task.lab_token = request.POST.get('lab_token')
242         task.save()
243         NotificationHandler.task_updated(task)
244         d = {}
245         d['task'] = task.config.get_delta()
246         m = {}
247         m['status'] = task.status
248         m['job'] = str(task.job)
249         m['message'] = task.message
250         d['meta'] = m
251         return JsonResponse(d, safe=False)
252     elif request.method == "GET":
253         return JsonResponse(get_task(task_id).config.get_delta())
254
255
256 @csrf_exempt
257 def specific_job(request, lab_name="", job_id=""):
258     lab_token = request.META.get('HTTP_AUTH_TOKEN')
259     lab_manager = LabManagerTracker.get(lab_name, lab_token)
260     if request.method == "POST":
261         return JsonResponse(lab_manager.update_job(job_id, request.POST), safe=False)
262     return JsonResponse(lab_manager.get_job(job_id), safe=False)
263
264
265 @csrf_exempt
266 def resource_ci_userdata(request, lab_name="", job_id="", resource_id="", file_id=0):
267     # lab_token = request.META.get('HTTP_AUTH_TOKEN')
268     # lab_manager = LabManagerTracker.get(lab_name, lab_token)
269
270     # job = lab_manager.get_job(job_id)
271     Job.objects.get(id=job_id)  # verify a valid job was given, even if we don't use it
272
273     cifile = None
274     try:
275         cifile = CloudInitFile.objects.get(id=file_id)
276     except ObjectDoesNotExist:
277         return HttpResponseNotFound("Could not find a matching resource by id " + str(resource_id))
278
279     text = cifile.text
280
281     prepended_text = "#cloud-config\n"
282     # mstrat = CloudInitFile.merge_strategy()
283     # prepended_text = prepended_text + yaml.dump({"merge_strategy": mstrat}) + "\n"
284     # print("in cloudinitfile create")
285     text = prepended_text + text
286     cloud_dict = {
287         "datasource": {
288             "None": {
289                 "metadata": {
290                     "instance-id": str(uuid.uuid4())
291                 },
292                 "userdata_raw": text,
293             },
294         },
295         "datasource_list": ["None"],
296     }
297
298     return HttpResponse(yaml.dump(cloud_dict, width=float("inf")), status=200)
299
300
301 @csrf_exempt
302 def resource_ci_metadata(request, lab_name="", job_id="", resource_id="", file_id=0):
303     return HttpResponse("#cloud-config", status=200)
304
305
306 @csrf_exempt
307 def resource_ci_userdata_directory(request, lab_name="", job_id="", resource_id=""):
308     # files = [{"id": file.file_id, "priority": file.priority} for file in CloudInitFile.objects.filter(job__id=job_id, resource_id=resource_id).order_by("priority").all()]
309     resource = ResourceQuery.get(labid=resource_id, lab=Lab.objects.get(name=lab_name))
310     files = resource.config.cloud_init_files
311     files = [{"id": file.id, "priority": file.priority} for file in files.order_by("priority").all()]
312
313     d = {}
314
315     merge_failures = []
316
317     merger = Merger(
318         [
319             (list, ["append"]),
320             (dict, ["merge"]),
321         ],
322         ["override"],  # fallback
323         ["override"],  # if types conflict (shouldn't happen in CI, but handle case)
324     )
325
326     for f in resource.config.cloud_init_files.order_by("priority").all():
327         try:
328             other_dict = yaml.safe_load(f.text)
329             if not (type(d) is dict):
330                 raise Exception("CI file was valid yaml but was not a dict")
331
332             merger.merge(d, other_dict)
333         except Exception as e:
334             # if fail to merge, then just skip
335             print("Failed to merge file in, as it had invalid content:", f.id)
336             print("File text was:")
337             print(f.text)
338             merge_failures.append({f.id: str(e)})
339
340     if len(merge_failures) > 0:
341         d['merge_failures'] = merge_failures
342
343     file = CloudInitFile.create(text=yaml.dump(d, width=float("inf")), priority=0)
344
345     return HttpResponse(json.dumps([{"id": file.id, "priority": file.priority}]), status=200)
346
347
348 def new_jobs(request, lab_name=""):
349     lab_token = request.META.get('HTTP_AUTH_TOKEN')
350     lab_manager = LabManagerTracker.get(lab_name, lab_token)
351     return JsonResponse(lab_manager.get_new_jobs(), safe=False)
352
353
354 def current_jobs(request, lab_name=""):
355     lab_token = request.META.get('HTTP_AUTH_TOKEN')
356     lab_manager = LabManagerTracker.get(lab_name, lab_token)
357     return JsonResponse(lab_manager.get_current_jobs(), safe=False)
358
359
360 @csrf_exempt
361 def analytics_job(request, lab_name=""):
362     """ returns all jobs with type booking"""
363     lab_token = request.META.get('HTTP_AUTH_TOKEN')
364     lab_manager = LabManagerTracker.get(lab_name, lab_token)
365     if request.method == "GET":
366         return JsonResponse(lab_manager.get_analytics_job(), safe=False)
367     if request.method == "POST":
368         users = json.loads(request.body.decode('utf-8'))['active_users']
369         try:
370             ActiveVPNUser.create(lab_name, users)
371         except ObjectDoesNotExist:
372             return JsonResponse('Lab does not exist!', safe=False)
373         return HttpResponse(status=200)
374     return HttpResponse(status=405)
375
376
377 def lab_downtime(request, lab_name=""):
378     lab_token = request.META.get('HTTP_AUTH_TOKEN')
379     lab_manager = LabManagerTracker.get(lab_name, lab_token)
380     if request.method == "GET":
381         return JsonResponse(lab_manager.get_downtime_json())
382     if request.method == "POST":
383         return post_lab_downtime(request, lab_manager)
384     if request.method == "DELETE":
385         return delete_lab_downtime(lab_manager)
386     return HttpResponse(status=405)
387
388
389 def post_lab_downtime(request, lab_manager):
390     current_downtime = lab_manager.get_downtime()
391     if current_downtime.exists():
392         return JsonResponse({"error": "Lab is already in downtime"}, status=422)
393     form = DowntimeForm(request.POST)
394     if form.is_valid():
395         return JsonResponse(lab_manager.create_downtime(form))
396     else:
397         return JsonResponse(form.errors.get_json_data(), status=400)
398
399
400 def delete_lab_downtime(lab_manager):
401     current_downtime = lab_manager.get_downtime()
402     if current_downtime.exists():
403         dt = current_downtime.first()
404         dt.end = timezone.now()
405         dt.save()
406         return JsonResponse(lab_manager.get_downtime_json(), safe=False)
407     else:
408         return JsonResponse({"error": "Lab is not in downtime"}, status=422)
409
410
411 def done_jobs(request, lab_name=""):
412     lab_token = request.META.get('HTTP_AUTH_TOKEN')
413     lab_manager = LabManagerTracker.get(lab_name, lab_token)
414     return JsonResponse(lab_manager.get_done_jobs(), safe=False)
415
416
417 def auth_and_log(request, endpoint):
418     """
419     Function to authenticate an API user and log info
420     in the API log model. This is to keep record of
421     all calls to the dashboard
422     """
423     user_token = request.META.get('HTTP_AUTH_TOKEN')
424     response = None
425
426     if user_token is None:
427         return HttpResponse('Unauthorized', status=401)
428
429     try:
430         token = Token.objects.get(key=user_token)
431     except Token.DoesNotExist:
432         token = None
433         response = HttpResponse('Unauthorized', status=401)
434
435     x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
436     if x_forwarded_for:
437         ip = x_forwarded_for.split(',')[0]
438     else:
439         ip = request.META.get('REMOTE_ADDR')
440
441     body = None
442
443     if request.method in ['POST', 'PUT']:
444         try:
445             body = json.loads(request.body.decode('utf-8')),
446         except Exception:
447             response = HttpResponse('Invalid Request Body', status=400)
448
449     APILog.objects.create(
450         user=token.user,
451         call_time=timezone.now(),
452         method=request.method,
453         endpoint=endpoint,
454         body=body,
455         ip_addr=ip
456     )
457
458     if response:
459         return response
460     else:
461         return token
462
463
464 """
465 Booking API Views
466 """
467
468
469 def user_bookings(request):
470     token = auth_and_log(request, 'booking')
471
472     if isinstance(token, HttpResponse):
473         return token
474
475     bookings = Booking.objects.filter(owner=token.user, end__gte=timezone.now())
476     output = [AutomationAPIManager.serialize_booking(booking)
477               for booking in bookings]
478     return JsonResponse(output, safe=False)
479
480
481 @csrf_exempt
482 def specific_booking(request, booking_id=""):
483     token = auth_and_log(request, 'booking/{}'.format(booking_id))
484
485     if isinstance(token, HttpResponse):
486         return token
487
488     booking = get_object_or_404(Booking, pk=booking_id, owner=token.user)
489     if request.method == "GET":
490         sbooking = AutomationAPIManager.serialize_booking(booking)
491         return JsonResponse(sbooking, safe=False)
492
493     if request.method == "DELETE":
494
495         if booking.end < timezone.now():
496             return HttpResponse("Booking already over", status=400)
497
498         booking.end = timezone.now()
499         booking.save()
500         return HttpResponse("Booking successfully cancelled")
501
502
503 @csrf_exempt
504 def extend_booking(request, booking_id="", days=""):
505     token = auth_and_log(request, 'booking/{}/extendBooking/{}'.format(booking_id, days))
506
507     if isinstance(token, HttpResponse):
508         return token
509
510     booking = get_object_or_404(Booking, pk=booking_id, owner=token.user)
511
512     if booking.end < timezone.now():
513         return HttpResponse("This booking is already over, cannot extend")
514
515     if days > 30:
516         return HttpResponse("Cannot extend a booking longer than 30 days")
517
518     if booking.ext_count == 0:
519         return HttpResponse("Booking has already been extended 2 times, cannot extend again")
520
521     booking.end += timedelta(days=days)
522     booking.ext_count -= 1
523     booking.save()
524
525     return HttpResponse("Booking successfully extended")
526
527
528 @csrf_exempt
529 def make_booking(request):
530     token = auth_and_log(request, 'booking/makeBooking')
531
532     if isinstance(token, HttpResponse):
533         return token
534
535     try:
536         booking = create_from_API(request.body, token.user)
537
538     except Exception:
539         finalTrace = ''
540         exc_type, exc_value, exc_traceback = sys.exc_info()
541         for i in traceback.format_exception(exc_type, exc_value, exc_traceback):
542             finalTrace += '<br>' + i.strip()
543         return HttpResponse(finalTrace, status=400)
544
545     sbooking = AutomationAPIManager.serialize_booking(booking)
546     return JsonResponse(sbooking, safe=False)
547
548
549 """
550 Resource Inventory API Views
551 """
552
553
554 def available_templates(request):
555     token = auth_and_log(request, 'resource_inventory/availableTemplates')
556
557     if isinstance(token, HttpResponse):
558         return token
559
560     # get available templates
561     # mirrors MultipleSelectFilter Widget
562     avt = []
563     for lab in Lab.objects.all():
564         for template in ResourceTemplate.objects.filter(Q(owner=token.user) | Q(public=True), lab=lab, temporary=False):
565             available_resources = lab.get_available_resources()
566             required_resources = template.get_required_resources()
567             least_available = 100
568
569             for resource, count_required in required_resources.items():
570                 try:
571                     curr_count = math.floor(available_resources[str(resource)] / count_required)
572                     if curr_count < least_available:
573                         least_available = curr_count
574                 except KeyError:
575                     least_available = 0
576
577             if least_available > 0:
578                 avt.append((template, least_available))
579
580     savt = [AutomationAPIManager.serialize_template(temp)
581             for temp in avt]
582
583     return JsonResponse(savt, safe=False)
584
585
586 def images_for_template(request, template_id=""):
587     _ = auth_and_log(request, 'resource_inventory/{}/images'.format(template_id))
588
589     template = get_object_or_404(ResourceTemplate, pk=template_id)
590     images = [AutomationAPIManager.serialize_image(config.image)
591               for config in template.getConfigs()]
592     return JsonResponse(images, safe=False)
593
594
595 """
596 User API Views
597 """
598
599
600 def all_users(request):
601     token = auth_and_log(request, 'users')
602
603     if token is None:
604         return HttpResponse('Unauthorized', status=401)
605
606     users = [AutomationAPIManager.serialize_userprofile(up)
607              for up in UserProfile.objects.filter(public_user=True)]
608
609     return JsonResponse(users, safe=False)
610
611
612 def create_ci_file(request):
613     token = auth_and_log(request, 'booking/makeCloudConfig')
614
615     if isinstance(token, HttpResponse):
616         return token
617
618     try:
619         cconf = request.body
620         d = yaml.load(cconf)
621         if not (type(d) is dict):
622             raise Exception()
623
624         cconf = CloudInitFile.create(text=cconf, priority=CloudInitFile.objects.count())
625
626         return JsonResponse({"id": cconf.id})
627     except Exception:
628         return JsonResponse({"error": "Provided config file was not valid yaml or was not a dict at the top level"})
629
630
631 """
632 Lab API Views
633 """
634
635
636 def list_labs(request):
637     lab_list = []
638     for lab in Lab.objects.all():
639         lab_info = {
640             'name': lab.name,
641             'username': lab.lab_user.username,
642             'status': lab.status,
643             'project': lab.project,
644             'description': lab.description,
645             'location': lab.location,
646             'info': lab.lab_info_link,
647             'email': lab.contact_email,
648             'phone': lab.contact_phone
649         }
650         lab_list.append(lab_info)
651
652     return JsonResponse(lab_list, safe=False)