1 ##############################################################################
2 # Copyright (c) 2016 Max Breitenfeldt and others.
3 # Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
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 ##############################################################################
15 from datetime import timedelta
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
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 (
49 from deepmerge import Merger
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
59 Most functions let you GET or POST to the same endpoint, and
60 the correct thing will happen
64 class BookingViewSet(viewsets.ModelViewSet):
65 queryset = Booking.objects.all()
66 serializer_class = BookingSerializer
67 filter_fields = ('resource', 'id')
70 class UserViewSet(viewsets.ModelViewSet):
71 queryset = UserProfile.objects.all()
72 serializer_class = UserSerializer
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)
82 Token.objects.create(user=user)
83 return redirect('account:settings')
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)
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)
101 # API extension for Cobbler integration
104 def all_images(request, lab_name=""):
106 for i in Image.objects.all():
107 a.append(i.serialize())
108 return JsonResponse(a, safe=False)
111 def all_opsyss(request, lab_name=""):
113 for opsys in Opsys.objects.all():
114 a.append(opsys.serialize())
116 return JsonResponse(a, safe=False)
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()
125 if request.method == "GET":
127 return HttpResponse(status=404)
128 return JsonResponse(img.serialize(), safe=False)
130 if request.method == "POST":
132 data = json.loads(request.body.decode('utf-8'))
136 # append lab name and the ID from the URL
137 data['from_lab_id'] = lab_name
138 data['lab_id'] = image_id
140 # create and save a new Image object
141 img = Image.new_from_data(data)
145 # indicate success in response
146 return HttpResponse(status=200)
147 return HttpResponse(status=405)
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()
156 if request.method == "GET":
158 return HttpResponse(status=404)
159 return JsonResponse(opsys.serialize(), safe=False)
161 if request.method == "POST":
162 data = json.loads(request.body.decode('utf-8'))
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)
173 return HttpResponse(status=200)
174 return HttpResponse(status=405)
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")
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")
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)
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")
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")
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
218 lab_manager.update_host_remote_info(request.POST, host_id),
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)
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
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')
243 NotificationHandler.task_updated(task)
245 d['task'] = task.config.get_delta()
247 m['status'] = task.status
248 m['job'] = str(task.job)
249 m['message'] = task.message
251 return JsonResponse(d, safe=False)
252 elif request.method == "GET":
253 return JsonResponse(get_task(task_id).config.get_delta())
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)
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)
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
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))
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
290 "instance-id": str(uuid.uuid4())
292 "userdata_raw": text,
295 "datasource_list": ["None"],
298 return HttpResponse(yaml.dump(cloud_dict, width=float("inf")), status=200)
302 def resource_ci_metadata(request, lab_name="", job_id="", resource_id="", file_id=0):
303 return HttpResponse("#cloud-config", status=200)
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()]
322 ["override"], # fallback
323 ["override"], # if types conflict (shouldn't happen in CI, but handle case)
326 for f in resource.config.cloud_init_files.order_by("priority").all():
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")
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:")
338 merge_failures.append({f.id: str(e)})
340 if len(merge_failures) > 0:
341 d['merge_failures'] = merge_failures
343 file = CloudInitFile.create(text=yaml.dump(d, width=float("inf")), priority=0)
345 return HttpResponse(json.dumps([{"id": file.id, "priority": file.priority}]), status=200)
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)
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)
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']
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)
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)
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)
395 return JsonResponse(lab_manager.create_downtime(form))
397 return JsonResponse(form.errors.get_json_data(), status=400)
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()
406 return JsonResponse(lab_manager.get_downtime_json(), safe=False)
408 return JsonResponse({"error": "Lab is not in downtime"}, status=422)
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)
417 def auth_and_log(request, endpoint):
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
423 user_token = request.META.get('HTTP_AUTH_TOKEN')
426 if user_token is None:
427 return HttpResponse('Unauthorized', status=401)
430 token = Token.objects.get(key=user_token)
431 except Token.DoesNotExist:
433 # Added logic to detect malformed token
434 if len(str(user_token)) != 40:
435 response = HttpResponse('Malformed Token', status=401)
437 response = HttpResponse('Unauthorized', status=401)
439 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
441 ip = x_forwarded_for.split(',')[0]
443 ip = request.META.get('REMOTE_ADDR')
447 if request.method in ['POST', 'PUT']:
449 body = json.loads(request.body.decode('utf-8')),
451 response = HttpResponse('Invalid Request Body', status=400)
453 APILog.objects.create(
455 call_time=timezone.now(),
456 method=request.method,
473 def user_bookings(request):
474 token = auth_and_log(request, 'booking')
476 if isinstance(token, HttpResponse):
479 bookings = Booking.objects.filter(owner=token.user, end__gte=timezone.now())
480 output = [AutomationAPIManager.serialize_booking(booking)
481 for booking in bookings]
482 return JsonResponse(output, safe=False)
486 def specific_booking(request, booking_id=""):
487 token = auth_and_log(request, 'booking/{}'.format(booking_id))
489 if isinstance(token, HttpResponse):
492 booking = get_object_or_404(Booking, pk=booking_id, owner=token.user)
493 if request.method == "GET":
494 sbooking = AutomationAPIManager.serialize_booking(booking)
495 return JsonResponse(sbooking, safe=False)
497 if request.method == "DELETE":
499 if booking.end < timezone.now():
500 return HttpResponse("Booking already over", status=400)
502 booking.end = timezone.now()
504 return HttpResponse("Booking successfully cancelled")
508 def extend_booking(request, booking_id="", days=""):
509 token = auth_and_log(request, 'booking/{}/extendBooking/{}'.format(booking_id, days))
511 if isinstance(token, HttpResponse):
514 booking = get_object_or_404(Booking, pk=booking_id, owner=token.user)
516 if booking.end < timezone.now():
517 return HttpResponse("This booking is already over, cannot extend")
520 return HttpResponse("Cannot extend a booking longer than 30 days")
522 if booking.ext_count == 0:
523 return HttpResponse("Booking has already been extended 2 times, cannot extend again")
525 booking.end += timedelta(days=days)
526 booking.ext_count -= 1
529 return HttpResponse("Booking successfully extended")
533 def make_booking(request):
534 token = auth_and_log(request, 'booking/makeBooking')
536 if isinstance(token, HttpResponse):
540 booking = create_from_API(request.body, token.user)
544 exc_type, exc_value, exc_traceback = sys.exc_info()
545 for i in traceback.format_exception(exc_type, exc_value, exc_traceback):
546 finalTrace += '<br>' + i.strip()
547 return HttpResponse(finalTrace, status=400)
549 sbooking = AutomationAPIManager.serialize_booking(booking)
550 return JsonResponse(sbooking, safe=False)
554 Resource Inventory API Views
558 def available_templates(request):
559 token = auth_and_log(request, 'resource_inventory/availableTemplates')
561 if isinstance(token, HttpResponse):
564 # get available templates
565 # mirrors MultipleSelectFilter Widget
567 for lab in Lab.objects.all():
568 for template in ResourceTemplate.objects.filter(Q(owner=token.user) | Q(public=True), lab=lab, temporary=False):
569 available_resources = lab.get_available_resources()
570 required_resources = template.get_required_resources()
571 least_available = 100
573 for resource, count_required in required_resources.items():
575 curr_count = math.floor(available_resources[str(resource)] / count_required)
576 if curr_count < least_available:
577 least_available = curr_count
581 if least_available > 0:
582 avt.append((template, least_available))
584 savt = [AutomationAPIManager.serialize_template(temp)
587 return JsonResponse(savt, safe=False)
590 def images_for_template(request, template_id=""):
591 _ = auth_and_log(request, 'resource_inventory/{}/images'.format(template_id))
593 template = get_object_or_404(ResourceTemplate, pk=template_id)
594 images = [AutomationAPIManager.serialize_image(config.image)
595 for config in template.getConfigs()]
596 return JsonResponse(images, safe=False)
604 def all_users(request):
605 token = auth_and_log(request, 'users')
608 return HttpResponse('Unauthorized', status=401)
610 users = [AutomationAPIManager.serialize_userprofile(up)
611 for up in UserProfile.objects.filter(public_user=True)]
613 return JsonResponse(users, safe=False)
616 def create_ci_file(request):
617 token = auth_and_log(request, 'booking/makeCloudConfig')
619 if isinstance(token, HttpResponse):
625 if not (type(d) is dict):
628 cconf = CloudInitFile.create(text=cconf, priority=CloudInitFile.objects.count())
630 return JsonResponse({"id": cconf.id})
632 return JsonResponse({"error": "Provided config file was not valid yaml or was not a dict at the top level"})
640 def list_labs(request):
642 for lab in Lab.objects.all():
645 'username': lab.lab_user.username,
646 'status': lab.status,
647 'project': lab.project,
648 'description': lab.description,
649 'location': lab.location,
650 'info': lab.lab_info_link,
651 'email': lab.contact_email,
652 'phone': lab.contact_phone
654 lab_list.append(lab_info)
656 return JsonResponse(lab_list, safe=False)