aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/views.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/views.py')
-rw-r--r--src/api/views.py424
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)