diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/api/admin.py | 2 | ||||
-rw-r--r-- | src/api/migrations/0017_apilog.py | 27 | ||||
-rw-r--r-- | src/api/migrations/0018_apilog_ip_addr.py | 18 | ||||
-rw-r--r-- | src/api/migrations/0019_auto_20210322_1823.py | 19 | ||||
-rw-r--r-- | src/api/migrations/0020_auto_20210322_2218.py | 23 | ||||
-rw-r--r-- | src/api/migrations/0021_auto_20210405_1943.py | 18 | ||||
-rw-r--r-- | src/api/models.py | 97 | ||||
-rw-r--r-- | src/api/urls.py | 20 | ||||
-rw-r--r-- | src/api/views.py | 203 | ||||
-rw-r--r-- | src/booking/quick_deployer.py | 88 |
10 files changed, 469 insertions, 46 deletions
diff --git a/src/api/admin.py b/src/api/admin.py index 8b2fcb3..1e243a0 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -22,6 +22,7 @@ from api.models import ( SoftwareRelation, HostHardwareRelation, HostNetworkRelation, + APILog ) @@ -39,3 +40,4 @@ admin.site.register(AccessRelation) admin.site.register(SoftwareRelation) admin.site.register(HostHardwareRelation) admin.site.register(HostNetworkRelation) +admin.site.register(APILog) diff --git a/src/api/migrations/0017_apilog.py b/src/api/migrations/0017_apilog.py new file mode 100644 index 0000000..d209aef --- /dev/null +++ b/src/api/migrations/0017_apilog.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2 on 2021-03-19 20:45 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('api', '0016_auto_20201109_2149'), + ] + + operations = [ + migrations.CreateModel( + name='APILog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('call_time', models.DateTimeField(auto_now=True)), + ('endpoint', models.CharField(max_length=300)), + ('body', django.contrib.postgres.fields.jsonb.JSONField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/api/migrations/0018_apilog_ip_addr.py b/src/api/migrations/0018_apilog_ip_addr.py new file mode 100644 index 0000000..4b7ce39 --- /dev/null +++ b/src/api/migrations/0018_apilog_ip_addr.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2021-03-22 18:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0017_apilog'), + ] + + operations = [ + migrations.AddField( + model_name='apilog', + name='ip_addr', + field=models.GenericIPAddressField(null=True), + ), + ] diff --git a/src/api/migrations/0019_auto_20210322_1823.py b/src/api/migrations/0019_auto_20210322_1823.py new file mode 100644 index 0000000..b3c4cdf --- /dev/null +++ b/src/api/migrations/0019_auto_20210322_1823.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2021-03-22 18:23 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0018_apilog_ip_addr'), + ] + + operations = [ + migrations.AlterField( + model_name='apilog', + name='body', + field=django.contrib.postgres.fields.jsonb.JSONField(null=True), + ), + ] diff --git a/src/api/migrations/0020_auto_20210322_2218.py b/src/api/migrations/0020_auto_20210322_2218.py new file mode 100644 index 0000000..0252c79 --- /dev/null +++ b/src/api/migrations/0020_auto_20210322_2218.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2 on 2021-03-22 22:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0019_auto_20210322_1823'), + ] + + operations = [ + migrations.AddField( + model_name='apilog', + name='method', + field=models.CharField(max_length=4, null=True), + ), + migrations.AlterField( + model_name='apilog', + name='endpoint', + field=models.CharField(max_length=300, null=True), + ), + ] diff --git a/src/api/migrations/0021_auto_20210405_1943.py b/src/api/migrations/0021_auto_20210405_1943.py new file mode 100644 index 0000000..ca6e741 --- /dev/null +++ b/src/api/migrations/0021_auto_20210405_1943.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2021-04-05 19:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0020_auto_20210322_2218'), + ] + + operations = [ + migrations.AlterField( + model_name='apilog', + name='method', + field=models.CharField(max_length=6, null=True), + ), + ] diff --git a/src/api/models.py b/src/api/models.py index d1bb692..d85f3e9 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -12,6 +12,7 @@ from django.contrib.auth.models import User from django.db import models from django.core.exceptions import PermissionDenied, ValidationError from django.shortcuts import get_object_or_404 +from django.contrib.postgres.fields import JSONField from django.http import HttpResponseNotFound from django.urls import reverse from django.utils import timezone @@ -37,7 +38,7 @@ from account.models import Downtime, UserProfile from dashboard.utils import AbstractModelQuery -class JobStatus(object): +class JobStatus: """ A poor man's enum for a job's status. @@ -52,7 +53,7 @@ class JobStatus(object): ERROR = 300 -class LabManagerTracker(object): +class LabManagerTracker: @classmethod def get(cls, lab_name, token): @@ -72,7 +73,7 @@ class LabManagerTracker(object): raise PermissionDenied("Lab not authorized") -class LabManager(object): +class LabManager: """ Handles all lab REST calls. @@ -337,6 +338,96 @@ class LabManager(object): return profile_ser +class APILog(models.Model): + user = models.ForeignKey(User, on_delete=models.PROTECT) + call_time = models.DateTimeField(auto_now=True) + method = models.CharField(null=True, max_length=6) + endpoint = models.CharField(null=True, max_length=300) + ip_addr = models.GenericIPAddressField(protocol="both", null=True, unpack_ipv4=False) + body = JSONField(null=True) + + def __str__(self): + return "Call to {} at {} by {}".format( + self.endpoint, + self.call_time, + self.user.username + ) + + +class AutomationAPIManager: + @staticmethod + def serialize_booking(booking): + sbook = {} + sbook['id'] = booking.pk + sbook['owner'] = booking.owner.username + sbook['collaborators'] = [user.username for user in booking.collaborators.all()] + sbook['start'] = booking.start + sbook['end'] = booking.end + sbook['lab'] = AutomationAPIManager.serialize_lab(booking.lab) + sbook['purpose'] = booking.purpose + sbook['resourceBundle'] = AutomationAPIManager.serialize_bundle(booking.resource) + return sbook + + @staticmethod + def serialize_lab(lab): + slab = {} + slab['id'] = lab.pk + slab['name'] = lab.name + return slab + + @staticmethod + def serialize_bundle(bundle): + sbundle = {} + sbundle['id'] = bundle.pk + sbundle['resources'] = [ + AutomationAPIManager.serialize_server(server) + for server in bundle.get_resources()] + return sbundle + + @staticmethod + def serialize_server(server): + sserver = {} + sserver['id'] = server.pk + sserver['name'] = server.name + return sserver + + @staticmethod + def serialize_resource_profile(profile): + sprofile = {} + sprofile['id'] = profile.pk + sprofile['name'] = profile.name + return sprofile + + @staticmethod + def serialize_template(rec_temp_and_count): + template = rec_temp_and_count[0] + count = rec_temp_and_count[1] + + stemplate = {} + stemplate['id'] = template.pk + stemplate['name'] = template.name + stemplate['count_available'] = count + stemplate['resourceProfiles'] = [ + AutomationAPIManager.serialize_resource_profile(config.profile) + for config in template.getConfigs() + ] + return stemplate + + @staticmethod + def serialize_image(image): + simage = {} + simage['id'] = image.pk + simage['name'] = image.name + return simage + + @staticmethod + def serialize_userprofile(up): + sup = {} + sup['id'] = up.pk + sup['username'] = up.user.username + return sup + + class Job(models.Model): """ A Job to be performed by the Lab. diff --git a/src/api/urls.py b/src/api/urls.py index bae86ea..3d78ed6 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -45,7 +45,14 @@ from api.views import ( lab_users, lab_user, GenerateTokenView, - analytics_job + analytics_job, + user_bookings, + make_booking, + available_templates, + images_for_template, + specific_booking, + extend_booking, + all_users ) urlpatterns = [ @@ -65,5 +72,16 @@ urlpatterns = [ path('labs/<slug:lab_name>/jobs/getByType/DATA', analytics_job), path('labs/<slug:lab_name>/users', lab_users), path('labs/<slug:lab_name>/users/<int:user_id>', lab_user), + + path('booking', user_bookings), + path('booking/<int:booking_id>', specific_booking), + path('booking/<int:booking_id>/extendBooking/<int:days>', extend_booking), + path('booking/makeBooking', make_booking), + + path('resource_inventory/availableTemplates', available_templates), + path('resource_inventory/<int:template_id>/images', images_for_template), + + path('users', all_users), + url(r'^token$', GenerateTokenView.as_view(), name='generate_token'), ] diff --git a/src/api/views.py b/src/api/views.py index 2e5f33f..3c8445d 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -8,9 +8,12 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## +import json +import math +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 @@ -23,12 +26,14 @@ from django.core.exceptions import ObjectDoesNotExist 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 api.models import LabManagerTracker, AutomationAPIManager, get_task, APILog from notifier.manager import NotificationHandler from analytics.models import ActiveVPNUser -import json +from booking.quick_deployer import create_from_API +from resource_inventory.models import ResourceTemplate + """ API views. @@ -234,3 +239,193 @@ 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 as e: + return HttpResponse(str(e), 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(lab=lab, owner=token.user, public=True): + 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.exclude(user=token.user)] + + return JsonResponse(users, safe=False) diff --git a/src/booking/quick_deployer.py b/src/booking/quick_deployer.py index 0a3bfc6..65dd9b2 100644 --- a/src/booking/quick_deployer.py +++ b/src/booking/quick_deployer.py @@ -10,10 +10,11 @@ import json from django.db.models import Q +from django.db import transaction from datetime import timedelta from django.utils import timezone from django.core.exceptions import ValidationError -from account.models import Lab +from account.models import Lab, UserProfile from resource_inventory.models import ( ResourceTemplate, @@ -167,7 +168,7 @@ def generate_resource_bundle(template): return resource_bundle -def check_invariants(request, **kwargs): +def check_invariants(**kwargs): # TODO: This should really happen in the BookingForm validation methods installer = kwargs['installer'] image = kwargs['image'] @@ -188,7 +189,7 @@ def check_invariants(request, **kwargs): # TODO # if image.host_type != host_profile: # raise ValidationError("The chosen image is not available for the chosen host type") - if not image.public and image.owner != request.user: + if not image.public and image.owner != kwargs['owner']: raise ValidationError("You are not the owner of the chosen private image") if length < 1 or length > 21: raise BookingLengthException("Booking must be between 1 and 21 days long") @@ -196,62 +197,73 @@ def check_invariants(request, **kwargs): def create_from_form(form, request): """ - Create a Booking from the user's form. - - Large, nasty method to create a booking or return a useful error - based on the form from the frontend + Parse data from QuickBookingForm to create booking """ resource_field = form.cleaned_data['filter_field'] - purpose_field = form.cleaned_data['purpose'] - project_field = form.cleaned_data['project'] - users_field = form.cleaned_data['users'] - hostname = 'opnfv_host' if not form.cleaned_data['hostname'] else form.cleaned_data['hostname'] - length = form.cleaned_data['length'] - - image = form.cleaned_data['image'] - scenario = form.cleaned_data['scenario'] - installer = form.cleaned_data['installer'] lab, resource_template = parse_resource_field(resource_field) data = form.cleaned_data data['lab'] = lab data['resource_template'] = resource_template - check_invariants(request, **data) + data['owner'] = request.user + + return _create_booking(data) + + +def create_from_API(body, user): + """ + Parse data from Automation API to create booking + """ + booking_info = json.loads(body.decode('utf-8')) + + data = {} + data['purpose'] = booking_info['purpose'] + data['project'] = booking_info['project'] + data['users'] = [UserProfile.objects.get(pk=user_id) + for user_id in booking_info['collaborators']] + data['hostname'] = booking_info['hostname'] + data['length'] = booking_info['length'] + data['installer'] = None + data['scenario'] = None + + data['image'] = Image.objects.get(pk=booking_info['imageLabID']) + + data['resource_template'] = ResourceTemplate.objects.get(pk=booking_info['templateID']) + data['lab'] = data['resource_template'].lab + data['owner'] = user + + return _create_booking(data) + + +@transaction.atomic +def _create_booking(data): + check_invariants(**data) # check booking privileges # TODO: use the canonical booking_allowed method because now template might have multiple # machines - if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge: + if Booking.objects.filter(owner=data['owner'], end__gt=timezone.now()).count() >= 3 and not data['owner'].userprofile.booking_privledge: raise PermissionError("You do not have permission to have more than 3 bookings at a time.") - ResourceManager.getInstance().templateIsReservable(resource_template) - - resource_template = update_template(resource_template, image, hostname, request.user) - - # if no installer provided, just create blank host - opnfv_config = None - if installer: - hconf = resource_template.getConfigs()[0] - opnfv_config = generate_opnfvconfig(scenario, installer, resource_template) - generate_hostopnfv(hconf, opnfv_config) - - # generate resource bundle - resource_bundle = generate_resource_bundle(resource_template) + ResourceManager.getInstance().templateIsReservable(data['resource_template']) + data['resource_template'] = update_template(data['resource_template'], data['image'], 'opnfv_host' if not data['hostname'] else data['hostname'], data['owner']) + resource_bundle = generate_resource_bundle(data['resource_template']) # generate booking booking = Booking.objects.create( - purpose=purpose_field, - project=project_field, - lab=lab, - owner=request.user, + purpose=data['purpose'], + project=data['project'], + lab=data['lab'], + owner=data['owner'], start=timezone.now(), - end=timezone.now() + timedelta(days=int(length)), + end=timezone.now() + timedelta(days=int(data['length'])), resource=resource_bundle, - opnfv_config=opnfv_config + opnfv_config=None ) + booking.pdf = PDFTemplater.makePDF(booking) - for collaborator in users_field: # list of UserProfiles + for collaborator in data['users']: # list of UserProfiles booking.collaborators.add(collaborator.user) booking.save() |