API V1 99/72599/2
authorSean Smith <ssmith@iol.unh.edu>
Fri, 19 Mar 2021 15:48:00 +0000 (11:48 -0400)
committerSean Smith <ssmith@iol.unh.edu>
Fri, 28 May 2021 15:06:08 +0000 (11:06 -0400)
Change-Id: I38bc35eeecc0cc7d2334d1fb4b9f215a291e31f6
Signed-off-by: Sean Smith <ssmith@iol.unh.edu>
laas_api_documentation.yaml [new file with mode: 0644]
src/api/admin.py
src/api/migrations/0017_apilog.py [new file with mode: 0644]
src/api/migrations/0018_apilog_ip_addr.py [new file with mode: 0644]
src/api/migrations/0019_auto_20210322_1823.py [new file with mode: 0644]
src/api/migrations/0020_auto_20210322_2218.py [new file with mode: 0644]
src/api/migrations/0021_auto_20210405_1943.py [new file with mode: 0644]
src/api/models.py
src/api/urls.py
src/api/views.py
src/booking/quick_deployer.py

diff --git a/laas_api_documentation.yaml b/laas_api_documentation.yaml
new file mode 100644 (file)
index 0000000..5528e5a
--- /dev/null
@@ -0,0 +1,333 @@
+swagger: '2.0'
+info:
+  description: |-
+    Details for all endpoints for LaaS automation API. This serves to allow users
+    to create bookings outside of the web UI hosted at labs.lfnetworking.org. 
+    All included setup is referencing the development server hosted while in 
+    beta testing for the API. 
+  version: 1.0.0
+  title: LaaS Automation API
+  termsOfService: 'http://labs.lfnetworking.org'
+  contact:
+    email: opnfv@iol.unh.edu
+  license:
+    name: MIT License
+host: 10.10.30.55
+basePath: /api
+tags:
+  - name: Bookings
+    description: View and edit existing bookings
+  - name: Resource Inventory
+    description: Examine and manage resources in a lab
+  - name: Users
+    description: All actions for referencing 
+schemes:
+  - http
+security:
+  - AutomationAPI: []
+paths:
+  /booking:
+    get:
+      tags:
+        - Bookings
+      summary: Get all bookings belonging to user
+      description: Get all bookings belonging to the user authenticated by API key.
+      operationId: retrieveBookings
+      produces:
+        - application/json
+      responses:
+        '200':
+          description: successful operation
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/Booking'
+        '401':
+          description: Unauthorized API key
+  /booking/makeBooking:
+    put:
+      tags:
+        - Bookings
+      summary: Make booking by specifying information
+      description: Exposes same functionality as quick booking form from dashboard
+      operationId: makeBooking
+      consumes:
+        - application/json
+      produces:
+        - application/json
+      parameters:
+        - in: body
+          name: booking
+          description: the booking to create
+          schema:
+            $ref: '#/definitions/MakeBookingTemplate'
+      responses:
+        '200':
+          description: successful operation
+          schema:
+            $ref: '#/definitions/Booking'
+        '400':
+          description: Error in booking info
+        '401':
+          description: Unauthorized API key
+  '/booking/{bookingID}':
+    get:
+      tags:
+        - Bookings
+      summary: See all info for specific booking
+      description: ''
+      operationId: specificBooking
+      parameters:
+        - in: path
+          name: bookingID
+          required: true
+          type: integer
+      produces:
+        - application/json
+      responses:
+        '200':
+          description: successful operation
+          schema:
+            $ref: '#/definitions/Booking'
+        '404':
+          description: Booking does not exist
+        '401':
+          description: Unauthorized API key
+    delete:
+      tags:
+        - Bookings
+      summary: Cancel booking
+      description: ''
+      operationId: cancelBooking
+      parameters:
+        - in: path
+          name: bookingID
+          required: true
+          type: integer
+      produces:
+        - application/json
+      responses:
+        '200':
+          description: successfully canceled booking
+        '404':
+          description: Booking does not exist
+        '400':
+          description: Cannnot cancel booking
+        '401':
+          description: Unauthorized API key
+  '/booking/{bookingID}/extendBooking/{days}':
+    post:
+      tags:
+        - Bookings
+      summary: Extend end date of booking
+      description: ''
+      operationId: extendBooking
+      parameters:
+        - in: path
+          name: bookingID
+          required: true
+          type: integer
+        - in: path
+          name: days
+          required: true
+          type: integer
+      responses:
+        '200':
+          description: successful operation
+          schema:
+            $ref: '#/definitions/Booking'
+        '404':
+          description: Booking to extend does not exist
+        '400':
+          description: Cannot extend Booking
+        '401':
+          description: Unauthorized API key
+  '/resource_inventory/{templateLabID}/images':
+    get:
+      tags:
+        - Resource Inventory
+      summary: See valid images for a resource template
+      description: ''
+      operationId: viewImages
+      parameters:
+        - in: path
+          name: templateLabID
+          required: true
+          type: integer
+      produces:
+        - application/json
+      responses:
+        '200':
+          description: successful operation
+          schema:
+            $ref: '#/definitions/Image'
+        '404':
+          description: Resource Template does not exist
+        '401':
+          description: Unauthorized API key
+  /resource_inventory/availableTemplates:
+    get:
+      tags:
+        - Resource Inventory
+      summary: All Resource Templates currently available
+      description: ''
+      operationId: listTemplates
+      produces:
+        - application/json
+      responses:
+        '200':
+          description: successful operation
+          schema:
+            $ref: '#/definitions/ResourceTemplate'
+        '401':
+          description: Unauthorized API key
+  /users:
+    get:
+      tags:
+        - Users
+      summary: See all public users that can be added to a booking
+      description: ''
+      operationId: getUsers
+      produces:
+        - application/json
+      responses:
+        '200':
+          description: successfel operation
+          schema:
+            $ref: '#/definitions/UserProfile'
+        '401':
+          description: Unauthorized API key
+  /labs/{labID}/users:
+    get:
+      tags:
+        - Lab
+      summary: Get all users at a lab
+      description: ''
+      operationId: labUsers
+      consumes:
+        - application/json
+      produces:
+        - application/json
+      parameters:
+        - in: path
+          name: labID
+          required: true
+          type: string
+      responses:
+        '200':
+          description: successful
+        '400':
+          description: invalid lab id
+securityDefinitions:
+  AutomationAPI:
+    type: apiKey
+    in: header
+    name: auth-token
+definitions:
+  Lab:
+    type: object
+    properties:
+      id:
+        type: integer
+        format: int64
+      name:
+        type: string
+  MakeBookingTemplate:
+    type: object
+    properties:
+      templateID:
+        type: integer
+      purpose:
+        type: string
+      project:
+        type: string
+      collaborators:
+        type: array
+        items:
+          type: integer
+      hostname:
+        type: string
+      length:
+        type: integer
+      imageLabID:
+        type: integer
+  Booking:
+    type: object
+    properties:
+      id:
+        type: integer
+        format: int64
+      owner:
+        type: string
+      collaborators:
+        $ref: '#/definitions/UserProfile'
+      start:
+        type: string
+        format: date-time
+      end:
+        type: string
+        format: date-time
+      lab:
+        $ref: '#/definitions/Lab'
+      purpose:
+        type: string
+      resourceBundle:
+        $ref: '#/definitions/ResourceBundle'
+      project:
+        type: string
+  Image:
+    type: object
+    properties:
+      labID:
+        type: integer
+        format: int64
+      name:
+        type: string
+  ResourceBundle:
+    type: object
+    properties:
+      id:
+        type: integer
+        format: int64
+      resources:
+        type: array
+        items:
+          $ref: '#/definitions/Server'
+  ResourceProfile:
+    type: object
+    properties:
+      id:
+        type: integer
+        format: int64
+      name:
+        type: string
+  UserProfile:
+    type: object
+    properties:
+      id:
+        type: integer
+        format: int64
+      name:
+        type: string
+  ResourceTemplate:
+    type: object
+    properties:
+      id:
+        type: integer
+        format: int64
+      name:
+        type: string
+      resourceProfiles:
+        type: array
+        items:
+          $ref: '#/definitions/ResourceProfile'
+  Server:
+    type: object
+    properties:
+      id:
+        type: integer
+        format: int64
+      profile:
+        $ref: '#/definitions/ResourceProfile'
+      labid:
+        type: string
index 8b2fcb3..1e243a0 100644 (file)
@@ -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 (file)
index 0000000..d209aef
--- /dev/null
@@ -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 (file)
index 0000000..4b7ce39
--- /dev/null
@@ -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 (file)
index 0000000..b3c4cdf
--- /dev/null
@@ -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 (file)
index 0000000..0252c79
--- /dev/null
@@ -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 (file)
index 0000000..ca6e741
--- /dev/null
@@ -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),
+        ),
+    ]
index d1bb692..d85f3e9 100644 (file)
@@ -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.
index bae86ea..3d78ed6 100644 (file)
@@ -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'),
 ]
index 2e5f33f..3c8445d 100644 (file)
@@ -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)
index 0a3bfc6..65dd9b2 100644 (file)
 
 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()