aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/admin.py2
-rw-r--r--src/api/migrations/0017_apilog.py27
-rw-r--r--src/api/migrations/0018_apilog_ip_addr.py18
-rw-r--r--src/api/migrations/0019_auto_20210322_1823.py19
-rw-r--r--src/api/migrations/0020_auto_20210322_2218.py23
-rw-r--r--src/api/migrations/0021_auto_20210405_1943.py18
-rw-r--r--src/api/models.py97
-rw-r--r--src/api/urls.py20
-rw-r--r--src/api/views.py203
-rw-r--r--src/booking/quick_deployer.py88
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()