diff options
Diffstat (limited to 'src/api/views.py')
-rw-r--r-- | src/api/views.py | 424 |
1 files changed, 420 insertions, 4 deletions
diff --git a/src/api/views.py b/src/api/views.py index 2e5f33f..1516374 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -8,27 +8,45 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## +import json +import math +import traceback +import sys +from datetime import timedelta from django.contrib.auth.decorators import login_required -from django.shortcuts import redirect +from django.shortcuts import redirect, get_object_or_404 from django.utils.decorators import method_decorator from django.utils import timezone from django.views import View +from django.http import HttpResponseNotFound from django.http.response import JsonResponse, HttpResponse from rest_framework import viewsets from rest_framework.authtoken.models import Token from django.views.decorators.csrf import csrf_exempt from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Q from api.serializers.booking_serializer import BookingSerializer from api.serializers.old_serializers import UserSerializer from api.forms import DowntimeForm -from account.models import UserProfile +from account.models import UserProfile, Lab from booking.models import Booking -from api.models import LabManagerTracker, get_task +from booking.quick_deployer import create_from_API +from api.models import LabManagerTracker, get_task, Job, AutomationAPIManager, APILog from notifier.manager import NotificationHandler from analytics.models import ActiveVPNUser -import json +from resource_inventory.models import ( + Image, + Opsys, + CloudInitFile, + ResourceQuery, + ResourceTemplate, +) + +import yaml +import uuid +from deepmerge import Merger """ API views. @@ -80,6 +98,83 @@ def lab_host(request, lab_name="", host_id=""): if request.method == "POST": return JsonResponse(lab_manager.update_host(host_id, request.POST), safe=False) +# API extension for Cobbler integration + + +def all_images(request, lab_name=""): + a = [] + for i in Image.objects.all(): + a.append(i.serialize()) + return JsonResponse(a, safe=False) + + +def all_opsyss(request, lab_name=""): + a = [] + for opsys in Opsys.objects.all(): + a.append(opsys.serialize()) + + return JsonResponse(a, safe=False) + + +@csrf_exempt +def single_image(request, lab_name="", image_id=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + img = lab_manager.get_image(image_id).first() + + if request.method == "GET": + if not img: + return HttpResponse(status=404) + return JsonResponse(img.serialize(), safe=False) + + if request.method == "POST": + # get POST data + data = json.loads(request.body.decode('utf-8')) + if img: + img.update(data) + else: + # append lab name and the ID from the URL + data['from_lab_id'] = lab_name + data['lab_id'] = image_id + + # create and save a new Image object + img = Image.new_from_data(data) + + img.save() + + # indicate success in response + return HttpResponse(status=200) + return HttpResponse(status=405) + + +@csrf_exempt +def single_opsys(request, lab_name="", opsys_id=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + opsys = lab_manager.get_opsys(opsys_id).first() + + if request.method == "GET": + if not opsys: + return HttpResponse(status=404) + return JsonResponse(opsys.serialize(), safe=False) + + if request.method == "POST": + data = json.loads(request.body.decode('utf-8')) + if opsys: + opsys.update(data) + else: + # only name, available, and obsolete are needed to create an Opsys + # other fields are derived from the URL parameters + data['from_lab_id'] = lab_name + data['lab_id'] = opsys_id + opsys = Opsys.new_from_data(data) + + opsys.save() + return HttpResponse(status=200) + return HttpResponse(status=405) + +# end API extension + def get_pdf(request, lab_name="", booking_id=""): lab_token = request.META.get('HTTP_AUTH_TOKEN') @@ -167,6 +262,89 @@ def specific_job(request, lab_name="", job_id=""): return JsonResponse(lab_manager.get_job(job_id), safe=False) +@csrf_exempt +def resource_ci_userdata(request, lab_name="", job_id="", resource_id="", file_id=0): + # lab_token = request.META.get('HTTP_AUTH_TOKEN') + # lab_manager = LabManagerTracker.get(lab_name, lab_token) + + # job = lab_manager.get_job(job_id) + Job.objects.get(id=job_id) # verify a valid job was given, even if we don't use it + + cifile = None + try: + cifile = CloudInitFile.objects.get(id=file_id) + except ObjectDoesNotExist: + return HttpResponseNotFound("Could not find a matching resource by id " + str(resource_id)) + + text = cifile.text + + prepended_text = "#cloud-config\n" + # mstrat = CloudInitFile.merge_strategy() + # prepended_text = prepended_text + yaml.dump({"merge_strategy": mstrat}) + "\n" + # print("in cloudinitfile create") + text = prepended_text + text + cloud_dict = { + "datasource": { + "None": { + "metadata": { + "instance-id": str(uuid.uuid4()) + }, + "userdata_raw": text, + }, + }, + "datasource_list": ["None"], + } + + return HttpResponse(yaml.dump(cloud_dict, width=float("inf")), status=200) + + +@csrf_exempt +def resource_ci_metadata(request, lab_name="", job_id="", resource_id="", file_id=0): + return HttpResponse("#cloud-config", status=200) + + +@csrf_exempt +def resource_ci_userdata_directory(request, lab_name="", job_id="", resource_id=""): + # 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()] + resource = ResourceQuery.get(labid=resource_id, lab=Lab.objects.get(name=lab_name)) + files = resource.config.cloud_init_files + files = [{"id": file.id, "priority": file.priority} for file in files.order_by("priority").all()] + + d = {} + + merge_failures = [] + + merger = Merger( + [ + (list, ["append"]), + (dict, ["merge"]), + ], + ["override"], # fallback + ["override"], # if types conflict (shouldn't happen in CI, but handle case) + ) + + for f in resource.config.cloud_init_files.order_by("priority").all(): + try: + other_dict = yaml.safe_load(f.text) + if not (type(d) is dict): + raise Exception("CI file was valid yaml but was not a dict") + + merger.merge(d, other_dict) + except Exception as e: + # if fail to merge, then just skip + print("Failed to merge file in, as it had invalid content:", f.id) + print("File text was:") + print(f.text) + merge_failures.append({f.id: str(e)}) + + if len(merge_failures) > 0: + d['merge_failures'] = merge_failures + + file = CloudInitFile.create(text=yaml.dump(d, width=float("inf")), priority=0) + + return HttpResponse(json.dumps([{"id": file.id, "priority": file.priority}]), status=200) + + def new_jobs(request, lab_name=""): lab_token = request.META.get('HTTP_AUTH_TOKEN') lab_manager = LabManagerTracker.get(lab_name, lab_token) @@ -234,3 +412,241 @@ def done_jobs(request, lab_name=""): lab_token = request.META.get('HTTP_AUTH_TOKEN') lab_manager = LabManagerTracker.get(lab_name, lab_token) return JsonResponse(lab_manager.get_done_jobs(), safe=False) + + +def auth_and_log(request, endpoint): + """ + Function to authenticate an API user and log info + in the API log model. This is to keep record of + all calls to the dashboard + """ + user_token = request.META.get('HTTP_AUTH_TOKEN') + response = None + + if user_token is None: + return HttpResponse('Unauthorized', status=401) + + try: + token = Token.objects.get(key=user_token) + except Token.DoesNotExist: + token = None + response = HttpResponse('Unauthorized', status=401) + + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + + body = None + + if request.method in ['POST', 'PUT']: + try: + body = json.loads(request.body.decode('utf-8')), + except Exception: + response = HttpResponse('Invalid Request Body', status=400) + + APILog.objects.create( + user=token.user, + call_time=timezone.now(), + method=request.method, + endpoint=endpoint, + body=body, + ip_addr=ip + ) + + if response: + return response + else: + return token + + +""" +Booking API Views +""" + + +def user_bookings(request): + token = auth_and_log(request, 'booking') + + if isinstance(token, HttpResponse): + return token + + bookings = Booking.objects.filter(owner=token.user, end__gte=timezone.now()) + output = [AutomationAPIManager.serialize_booking(booking) + for booking in bookings] + return JsonResponse(output, safe=False) + + +@csrf_exempt +def specific_booking(request, booking_id=""): + token = auth_and_log(request, 'booking/{}'.format(booking_id)) + + if isinstance(token, HttpResponse): + return token + + booking = get_object_or_404(Booking, pk=booking_id, owner=token.user) + if request.method == "GET": + sbooking = AutomationAPIManager.serialize_booking(booking) + return JsonResponse(sbooking, safe=False) + + if request.method == "DELETE": + + if booking.end < timezone.now(): + return HttpResponse("Booking already over", status=400) + + booking.end = timezone.now() + booking.save() + return HttpResponse("Booking successfully cancelled") + + +@csrf_exempt +def extend_booking(request, booking_id="", days=""): + token = auth_and_log(request, 'booking/{}/extendBooking/{}'.format(booking_id, days)) + + if isinstance(token, HttpResponse): + return token + + booking = get_object_or_404(Booking, pk=booking_id, owner=token.user) + + if booking.end < timezone.now(): + return HttpResponse("This booking is already over, cannot extend") + + if days > 30: + return HttpResponse("Cannot extend a booking longer than 30 days") + + if booking.ext_count == 0: + return HttpResponse("Booking has already been extended 2 times, cannot extend again") + + booking.end += timedelta(days=days) + booking.ext_count -= 1 + booking.save() + + return HttpResponse("Booking successfully extended") + + +@csrf_exempt +def make_booking(request): + token = auth_and_log(request, 'booking/makeBooking') + + if isinstance(token, HttpResponse): + return token + + try: + booking = create_from_API(request.body, token.user) + + except Exception: + finalTrace = '' + exc_type, exc_value, exc_traceback = sys.exc_info() + for i in traceback.format_exception(exc_type, exc_value, exc_traceback): + finalTrace += '<br>' + i.strip() + return HttpResponse(finalTrace, status=400) + + sbooking = AutomationAPIManager.serialize_booking(booking) + return JsonResponse(sbooking, safe=False) + + +""" +Resource Inventory API Views +""" + + +def available_templates(request): + token = auth_and_log(request, 'resource_inventory/availableTemplates') + + if isinstance(token, HttpResponse): + return token + + # get available templates + # mirrors MultipleSelectFilter Widget + avt = [] + for lab in Lab.objects.all(): + for template in ResourceTemplate.objects.filter(Q(owner=token.user) | Q(public=True), lab=lab, temporary=False): + available_resources = lab.get_available_resources() + required_resources = template.get_required_resources() + least_available = 100 + + for resource, count_required in required_resources.items(): + try: + curr_count = math.floor(available_resources[str(resource)] / count_required) + if curr_count < least_available: + least_available = curr_count + except KeyError: + least_available = 0 + + if least_available > 0: + avt.append((template, least_available)) + + savt = [AutomationAPIManager.serialize_template(temp) + for temp in avt] + + return JsonResponse(savt, safe=False) + + +def images_for_template(request, template_id=""): + _ = auth_and_log(request, 'resource_inventory/{}/images'.format(template_id)) + + template = get_object_or_404(ResourceTemplate, pk=template_id) + images = [AutomationAPIManager.serialize_image(config.image) + for config in template.getConfigs()] + return JsonResponse(images, safe=False) + + +""" +User API Views +""" + + +def all_users(request): + token = auth_and_log(request, 'users') + + if token is None: + return HttpResponse('Unauthorized', status=401) + + users = [AutomationAPIManager.serialize_userprofile(up) + for up in UserProfile.objects.filter(public_user=True)] + + return JsonResponse(users, safe=False) + + +def create_ci_file(request): + token = auth_and_log(request, 'booking/makeCloudConfig') + + if isinstance(token, HttpResponse): + return token + + try: + cconf = request.body + d = yaml.load(cconf) + if not (type(d) is dict): + raise Exception() + + cconf = CloudInitFile.create(text=cconf, priority=CloudInitFile.objects.count()) + + return JsonResponse({"id": cconf.id}) + except Exception: + return JsonResponse({"error": "Provided config file was not valid yaml or was not a dict at the top level"}) + + +""" +Lab API Views +""" + + +def list_labs(request): + lab_list = [] + for lab in Lab.objects.all(): + lab_info = { + 'name': lab.name, + 'username': lab.lab_user.username, + 'status': lab.status, + 'project': lab.project, + 'description': lab.description, + 'location': lab.location, + 'info': lab.lab_info_link, + 'email': lab.contact_email, + 'phone': lab.contact_phone + } + lab_list.append(lab_info) + + return JsonResponse(lab_list, safe=False) |