Merge User Booking API Rev 1 (Try 3) 77/72977/2
authorSawyer Bergeron <sbergeron@iol.unh.edu>
Fri, 15 Oct 2021 18:14:46 +0000 (14:14 -0400)
committerSawyer Bergeron <sbergeron@iol.unh.edu>
Fri, 15 Oct 2021 18:20:02 +0000 (14:20 -0400)
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
Change-Id: Ie1eee0a59929f8da39f16bb6bc17ae3de4f1cba9
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
38 files changed:
config.env.sample
config/rabbitmq/rabbitmq.conf [new file with mode: 0644]
docker-compose.yml
laas_api_documentation.yaml [new file with mode: 0644]
requirements.txt
src/account/jira_util.py [deleted file]
src/account/models.py
src/account/tasks.py [deleted file]
src/account/urls.py
src/account/views.py
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
src/booking/stats.py
src/booking/urls.py
src/booking/views.py
src/dashboard/templatetags/jira_filters.py [deleted file]
src/dashboard/urls.py
src/laas_dashboard/settings.py
src/notifier/urls.py
src/resource_inventory/resource_manager.py
src/resource_inventory/urls.py
src/templates/base/account/configuration_list.html [deleted file]
src/templates/base/account/details.html
src/templates/base/base.html
src/templates/base/booking/booking_delete.html
src/templates/base/booking/booking_table.html
src/templates/lfedge/booking/booking_table.html
src/workflow/models.py
web/Dockerfile
worker/Dockerfile

index 5b34217..c47f2bf 100644 (file)
@@ -34,13 +34,6 @@ SECRET_KEY=http://www.miniwebtool.com/django-secret-key-generator/
 OAUTH_CONSUMER_KEY=sample_key
 OAUTH_CONSUMER_SECRET=sample_secret
 
-# access information for Jira
-# In addition to this, the rsa keys from your jira admin
-# need to go into src/account
-JIRA_URL=sample_url
-JIRA_USER_NAME=sample_jira_user
-JIRA_USER_PASSWORD=sample_jira_pass
-
 # LFID
 OIDC_CLIENT_ID=sample_id
 OIDC_CLIENT_SECRET=sample_secret
@@ -55,8 +48,8 @@ OIDC_RP_SIGN_ALGO=RS256
 OIDC_OP_JWKS_ENDPOINT=https://sso.linuxfoundation.org/.well-known/jwks.json
 
 # Rabbitmq
-RABBITMQ_DEFAULT_USER=opnfv
-RABBITMQ_DEFAULT_PASS=opnfvopnfv
+DEFAULT_USER=opnfv
+DEFAULT_PASS=opnfvopnfv
 
 # Jenkins Build Server
 JENKINS_URL=https://build.opnfv.org/ci
diff --git a/config/rabbitmq/rabbitmq.conf b/config/rabbitmq/rabbitmq.conf
new file mode 100644 (file)
index 0000000..39c222c
--- /dev/null
@@ -0,0 +1,2 @@
+default_user=opnfv
+default_pass=opnfvopnfv
index ee8de2c..f0de7b2 100644 (file)
@@ -50,7 +50,9 @@ services:
         restart: always
         image: rabbitmq
         container_name: rm01
-        env_file: config.env
+          #env_file: config.env
+        volumes:
+          - ./config/rabbitmq:/etc/rabbitmq
         ports:
             - "5672:5672"
 
diff --git a/laas_api_documentation.yaml b/laas_api_documentation.yaml
new file mode 100644 (file)
index 0000000..ee967b0
--- /dev/null
@@ -0,0 +1,401 @@
+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: successful operation
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/UserProfile'
+        '401':
+          description: Unauthorized API key
+  /labs:
+    get:
+      tags:
+        - Lab
+      summary: List all labs and some of their info
+      description: ''
+      operationId: listLabs
+      produces:
+        - application/json
+      responses:
+        '200':
+          description: successful operation
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/Lab'
+        '401':
+          description: Unauthorized API Key
+  /labs/{labID}/users:
+    get:
+      tags:
+        - Lab
+      summary: Get all users that are visible to a lab for operational purposes
+      description: ''
+      operationId: labUsers
+      consumes:
+        - application/json
+      produces:
+        - application/json
+      parameters:
+        - in: path
+          name: labID
+          required: true
+          type: string
+      responses:
+        '200':
+          description: successful
+          schema: array
+          items:
+            $ref: '#/definitions/UserProfile'
+        '400':
+          description: invalid lab id
+securityDefinitions:
+  AutomationAPI:
+    type: apiKey
+    in: header
+    name: auth-token
+definitions:
+  Lab:
+    type: object
+    required:
+      - id
+      - name
+    properties:
+      id:
+        type: integer
+        format: int64
+      name:
+        type: string
+  MakeBookingTemplate:
+    type: object
+    required:
+      - templateID
+      - purpose
+      - project
+      - collaborators
+      - hostname
+      - length
+      - imageLabID
+    properties:
+      templateID:
+        type: integer
+      purpose:
+        type: string
+      project:
+        type: string
+      collaborators:
+        type: array
+        items:
+          type: string
+          description: username of the referred user
+      hostname:
+        type: string
+      length:
+        type: integer
+        description: length of the booking in days (max 21, min 1)
+      imageLabID:
+        type: integer
+  Booking:
+    type: object
+    required:
+      - id
+      - owner
+      - collaborators
+      - start
+      - end
+      - lab
+      - purpose
+      - project
+      - resourceBundle
+    properties:
+      id:
+        type: integer
+        format: int64
+      owner:
+        type: string
+      collaborators:
+        type: array
+        items:
+          $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
+    required:
+      - labID
+      - resources
+    properties:
+      labID:
+        type: integer
+        format: int64
+      name:
+        type: string
+  ResourceBundle:
+    type: object
+    required:
+      - id
+      - resources
+    properties:
+      id:
+        type: integer
+        format: int64
+      resources:
+        type: array
+        items:
+          $ref: '#/definitions/Server'
+  ResourceProfile:
+    type: object
+    required:
+      - id
+      - name
+    properties:
+      id:
+        type: integer
+        format: int64
+      name:
+        type: string
+  UserProfile:
+    type: object
+    required:
+      - id
+      - name
+    properties:
+      id:
+        type: integer
+        format: int64
+      name:
+        type: string
+  ResourceTemplate:
+    type: object
+    required:
+      - id
+      - name
+      - resourceProfiles
+    properties:
+      id:
+        type: integer
+        format: int64
+      name:
+        type: string
+      resourceProfiles:
+        type: array
+        items:
+          $ref: '#/definitions/ResourceProfile'
+  Server:
+    type: object
+    required:
+      - id
+      - labid
+      - profile
+    properties:
+      id:
+        type: integer
+        format: int64
+      profile:
+        $ref: '#/definitions/ResourceProfile'
+      labid:
+        type: string
index b34dd1e..72afbfa 100644 (file)
@@ -1,18 +1,17 @@
-celery==3.1.23
-cryptography==2.6.1
+celery==5.1.2
+cryptography==3.4.7
 Django==2.2
 django-bootstrap4==0.0.8
 django-filter==2.0.0
 djangorestframework==3.8.2
-gunicorn==19.6.0
-jira==1.0.7
+gunicorn==20.1.0
 oauth2==1.9.0.post1
-oauthlib==1.1.2
-pika==0.10.0
-psycopg2==2.8.4
-PyJWT==1.4.2
-requests==2.22.0
+oauthlib==3.1.1
+pika==1.2.0
+psycopg2==2.8.6
+PyJWT==2.1.0
+requests==2.26.0
 django-fernet-fields==0.6
-pyyaml==3.13
-pytz==2018.5
-mozilla-django-oidc==1.2.3
+pyyaml==5.4.1
+pytz==2021.1
+mozilla-django-oidc==2.0.0
\ No newline at end of file
diff --git a/src/account/jira_util.py b/src/account/jira_util.py
deleted file mode 100644 (file)
index a522594..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-##############################################################################
-# Copyright (c) 2016 Max Breitenfeldt and others.
-#
-# All rights reserved. This program and the accompanying materials
-# are made available under the terms of the Apache License, Version 2.0
-# which accompanies this distribution, and is available at
-# http://www.apache.org/licenses/LICENSE-2.0
-##############################################################################
-
-
-import base64
-import os
-
-import oauth2 as oauth
-from django.conf import settings
-from jira import JIRA
-from tlslite.utils import keyfactory
-
-
-class SignatureMethod_RSA_SHA1(oauth.SignatureMethod):
-    name = 'RSA-SHA1'
-
-    def signing_base(self, request, consumer, token):
-        if not hasattr(request, 'normalized_url') or request.normalized_url is None:
-            raise ValueError("Base URL for request is not set.")
-
-        sig = (
-            oauth.escape(request.method),
-            oauth.escape(request.normalized_url),
-            oauth.escape(request.get_normalized_parameters()),
-        )
-
-        key = '%s&' % oauth.escape(consumer.secret)
-        if token:
-            key += oauth.escape(token.secret)
-        raw = '&'.join(sig)
-        return key, raw
-
-    def sign(self, request, consumer, token):
-        """Build the base signature string."""
-        key, raw = self.signing_base(request, consumer, token)
-
-        module_dir = os.path.dirname(__file__)  # get current directory
-        with open(module_dir + '/rsa.pem', 'r') as f:
-            data = f.read()
-        privateKeyString = data.strip()
-        privatekey = keyfactory.parsePrivateKey(privateKeyString)
-        raw = str.encode(raw)
-        signature = privatekey.hashAndSign(raw)
-        return base64.b64encode(signature)
-
-
-def get_jira(user):
-    module_dir = os.path.dirname(__file__)  # get current directory
-    with open(module_dir + '/rsa.pem', 'r') as f:
-        key_cert = f.read()
-
-    oauth_dict = {
-        'access_token': user.userprofile.oauth_token,
-        'access_token_secret': user.userprofile.oauth_secret,
-        'consumer_key': settings.OAUTH_CONSUMER_KEY,
-        'key_cert': key_cert
-    }
-
-    return JIRA(server=settings.JIRA_URL, oauth=oauth_dict)
index b71f0ac..210025e 100644 (file)
@@ -51,6 +51,7 @@ class UserProfile(models.Model):
     oauth_secret = models.CharField(max_length=1024, blank=False)
 
     jira_url = models.CharField(max_length=100, null=True, blank=True, default='')
+
     full_name = models.CharField(max_length=100, null=True, blank=True, default='')
     booking_privledge = models.BooleanField(default=False)
 
diff --git a/src/account/tasks.py b/src/account/tasks.py
deleted file mode 100644 (file)
index df98c73..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-##############################################################################
-# Copyright (c) 2016 Max Breitenfeldt and others.
-# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
-#
-# All rights reserved. This program and the accompanying materials
-# are made available under the terms of the Apache License, Version 2.0
-# which accompanies this distribution, and is available at
-# http://www.apache.org/licenses/LICENSE-2.0
-##############################################################################
-
-
-from celery import shared_task
-from django.contrib.auth.models import User
-from jira import JIRAError
-
-from account.jira_util import get_jira
-
-
-@shared_task
-def sync_jira_accounts():
-    users = User.objects.all()
-    for user in users:
-        jira = get_jira(user)
-        try:
-            user_dict = jira.myself()
-        except JIRAError:
-            # User can be anonymous (local django admin account)
-            continue
-        try:
-            user.email = user_dict['emailAddress']
-        except KeyError:
-            pass
-        user.userprofile.url = user_dict['self']
-        user.userprofile.full_name = user_dict['displayName']
-
-        user.userprofile.save()
-        user.save()
index 97d8c77..6d4ef2f 100644 (file)
@@ -30,46 +30,30 @@ from django.urls import path
 
 from account.views import (
     AccountSettingsView,
-    JiraAuthenticatedView,
-    JiraLoginView,
     OIDCLoginView,
-    JiraLogoutView,
+    LogoutView,
     UserListView,
     account_resource_view,
     account_booking_view,
     account_images_view,
-    account_configuration_view,
     account_detail_view,
-    resource_delete_view,
+    template_delete_view,
     booking_cancel_view,
     image_delete_view,
-    configuration_delete_view
 )
 
-from laas_dashboard import settings
+app_name = 'account'
 
-
-def get_login_view():
-    if (settings.AUTH_SETTING == 'LFID'):
-        return OIDCLoginView.as_view()
-    else:
-        return JiraLoginView.as_view()
-
-
-app_name = "account"
 urlpatterns = [
     url(r'^settings/', AccountSettingsView.as_view(), name='settings'),
-    url(r'^authenticated/$', JiraAuthenticatedView.as_view(), name='authenticated'),
-    url(r'^login/$', get_login_view(), name='login'),
-    url(r'^logout/$', JiraLogoutView.as_view(), name='logout'),
+    url(r'^login/$', OIDCLoginView.as_view(), name='login'),
+    url(r'^logout/$', LogoutView.as_view(), name='logout'),
     url(r'^users/$', UserListView.as_view(), name='users'),
-    url(r'^my/resources/$', account_resource_view, name="my-resources"),
-    path('my/resources/delete/<int:resource_id>', resource_delete_view),
-    url(r'^my/bookings/$', account_booking_view, name="my-bookings"),
+    url(r'^my/resources/$', account_resource_view, name='my-resources'),
+    path('my/resources/delete/<int:resource_id>', template_delete_view),
+    url(r'^my/bookings/$', account_booking_view, name='my-bookings'),
     path('my/bookings/cancel/<int:booking_id>', booking_cancel_view),
-    url(r'^my/images/$', account_images_view, name="my-images"),
+    url(r'^my/images/$', account_images_view, name='my-images'),
     path('my/images/delete/<int:image_id>', image_delete_view),
-    url(r'^my/configurations/$', account_configuration_view, name="my-configurations"),
-    path('my/configurations/delete/<int:config_id>', configuration_delete_view),
-    url(r'^my/$', account_detail_view, name="my-account"),
+    url(r'^my/$', account_detail_view, name='my-account'),
 ]
index b74126e..8976ff9 100644 (file)
 
 
 import os
-import urllib
 
-import oauth2 as oauth
-from django.conf import settings
 from django.utils import timezone
 from django.contrib import messages
-from django.contrib.auth import logout, authenticate, login
+from django.contrib.auth import logout
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.auth.models import User
@@ -26,13 +23,11 @@ from django.shortcuts import get_object_or_404
 from django.utils.decorators import method_decorator
 from django.views.generic import RedirectView, TemplateView, UpdateView
 from django.shortcuts import render
-from jira import JIRA
 from rest_framework.authtoken.models import Token
 from mozilla_django_oidc.auth import OIDCAuthenticationBackend
 
 
 from account.forms import AccountSettingsForm
-from account.jira_util import SignatureMethod_RSA_SHA1
 from account.models import UserProfile
 from booking.models import Booking
 from resource_inventory.models import ResourceTemplate, Image
@@ -69,7 +64,7 @@ class MyOIDCAB(OIDCAuthenticationBackend):
         If this changes we will need to match users based on some
         other criterea.
         """
-        username = claims.get(os.environ['CLAIMS_ENDPOINT'] + 'username')
+        username = claims.get(os.environ.get('CLAIMS_ENDPOINT') + 'username')
 
         if not username:
             return HttpResponse('No username provided, contact support.')
@@ -101,109 +96,17 @@ class MyOIDCAB(OIDCAuthenticationBackend):
         return user
 
 
-class JiraLoginView(RedirectView):
-    def get_redirect_url(self, *args, **kwargs):
-        consumer = oauth.Consumer(settings.OAUTH_CONSUMER_KEY, settings.OAUTH_CONSUMER_SECRET)
-        client = oauth.Client(consumer)
-        client.set_signature_method(SignatureMethod_RSA_SHA1())
-
-        # Step 1. Get a request token from Jira.
-        try:
-            resp, content = client.request(settings.OAUTH_REQUEST_TOKEN_URL, "POST")
-        except Exception:
-            messages.add_message(self.request, messages.ERROR,
-                                 'Error: Connection to Jira failed. Please contact an Administrator')
-            return '/'
-        if resp['status'] != '200':
-            messages.add_message(self.request, messages.ERROR,
-                                 'Error: Connection to Jira failed. Please contact an Administrator')
-            return '/'
-
-        # Step 2. Store the request token in a session for later use.
-        self.request.session['request_token'] = dict(urllib.parse.parse_qsl(content.decode()))
-        # Step 3. Redirect the user to the authentication URL.
-        url = settings.OAUTH_AUTHORIZE_URL + '?oauth_token=' + \
-            self.request.session['request_token']['oauth_token'] + \
-            '&oauth_callback=' + settings.OAUTH_CALLBACK_URL
-        return url
-
-
 class OIDCLoginView(RedirectView):
     def get_redirect_url(self, *args, **kwargs):
         return reverse('oidc_authentication_init')
 
 
-class JiraLogoutView(LoginRequiredMixin, RedirectView):
+class LogoutView(LoginRequiredMixin, RedirectView):
     def get_redirect_url(self, *args, **kwargs):
         logout(self.request)
         return '/'
 
 
-class JiraAuthenticatedView(RedirectView):
-    def get_redirect_url(self, *args, **kwargs):
-        # Step 1. Use the request token in the session to build a new client.
-        consumer = oauth.Consumer(settings.OAUTH_CONSUMER_KEY, settings.OAUTH_CONSUMER_SECRET)
-        token = oauth.Token(self.request.session['request_token']['oauth_token'],
-                            self.request.session['request_token']['oauth_token_secret'])
-        client = oauth.Client(consumer, token)
-        client.set_signature_method(SignatureMethod_RSA_SHA1())
-
-        # Step 2. Request the authorized access token from Jira.
-        try:
-            resp, content = client.request(settings.OAUTH_ACCESS_TOKEN_URL, "POST")
-        except Exception:
-            messages.add_message(self.request, messages.ERROR,
-                                 'Error: Connection to Jira failed. Please contact an Administrator')
-            return '/'
-        if resp['status'] != '200':
-            messages.add_message(self.request, messages.ERROR,
-                                 'Error: Connection to Jira failed. Please contact an Administrator')
-            return '/'
-
-        access_token = dict(urllib.parse.parse_qsl(content.decode()))
-
-        module_dir = os.path.dirname(__file__)  # get current directory
-        with open(module_dir + '/rsa.pem', 'r') as f:
-            key_cert = f.read()
-
-        oauth_dict = {
-            'access_token': access_token['oauth_token'],
-            'access_token_secret': access_token['oauth_token_secret'],
-            'consumer_key': settings.OAUTH_CONSUMER_KEY,
-            'key_cert': key_cert
-        }
-
-        jira = JIRA(server=settings.JIRA_URL, oauth=oauth_dict)
-        username = jira.current_user()
-        email = ""
-        try:
-            email = jira.user(username).emailAddress
-        except AttributeError:
-            email = ""
-        url = '/'
-        # Step 3. Lookup the user or create them if they don't exist.
-        try:
-            user = User.objects.get(username=username)
-        except User.DoesNotExist:
-            # Save our permanent token and secret for later.
-            user = User.objects.create_user(username=username,
-                                            password=access_token['oauth_token_secret'])
-            profile = UserProfile()
-            profile.user = user
-            profile.save()
-            user.userprofile.email_addr = email
-            url = reverse('account:settings')
-        user.userprofile.oauth_token = access_token['oauth_token']
-        user.userprofile.oauth_secret = access_token['oauth_token_secret']
-        user.userprofile.save()
-        user.set_password(access_token['oauth_token_secret'])
-        user.save()
-        user = authenticate(username=username, password=access_token['oauth_token_secret'])
-        login(self.request, user)
-        # redirect user to settings page to complete profile
-        return url
-
-
 @method_decorator(login_required, name='dispatch')
 class UserListView(TemplateView):
     template_name = "account/user_list.html"
@@ -232,9 +135,9 @@ def account_resource_view(request):
     template = "account/resource_list.html"
 
     active_bundles = [book.resource for book in Booking.objects.filter(
-        owner=request.user, end__gte=timezone.now())]
+        owner=request.user, end__gte=timezone.now(), resource__template__temporary=False)]
     active_resources = [bundle.template.id for bundle in active_bundles]
-    resource_list = list(ResourceTemplate.objects.filter(owner=request.user))
+    resource_list = list(ResourceTemplate.objects.filter(owner=request.user, temporary=False))
 
     context = {
         "resources": resource_list,
@@ -262,15 +165,6 @@ def account_booking_view(request):
     return render(request, template, context=context)
 
 
-def account_configuration_view(request):
-    if not request.user.is_authenticated:
-        return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
-    template = "account/configuration_list.html"
-    configs = list(ResourceTemplate.objects.filter(owner=request.user))
-    context = {"title": "Configuration List", "configurations": configs}
-    return render(request, template, context=context)
-
-
 def account_images_view(request):
     if not request.user.is_authenticated:
         return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
@@ -290,28 +184,18 @@ def account_images_view(request):
     return render(request, template, context=context)
 
 
-def resource_delete_view(request, resource_id=None):
-    if not request.user.is_authenticated:
-        return HttpResponse('no')  # 403?
-    grb = get_object_or_404(ResourceTemplate, pk=resource_id)
-    if not request.user.id == grb.owner.id:
-        return HttpResponse('no')  # 403?
-    if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists():
-        return HttpResponse('no')  # 403?
-    grb.delete()
-    return HttpResponse('')
-
-
-def configuration_delete_view(request, config_id=None):
+def template_delete_view(request, resource_id=None):
     if not request.user.is_authenticated:
-        return HttpResponse('no')  # 403?
-    config = get_object_or_404(ResourceTemplate, pk=config_id)
-    if not request.user.id == config.owner.id:
-        return HttpResponse('no')  # 403?
-    if Booking.objects.filter(resource__template=config, end__gt=timezone.now()).exists():
-        return HttpResponse('no')
-    config.delete()
-    return HttpResponse('')
+        return HttpResponse(status=403)
+    template = get_object_or_404(ResourceTemplate, pk=resource_id)
+    if not request.user.id == template.owner.id:
+        return HttpResponse(status=403)
+    if Booking.objects.filter(resource__template=template, end__gt=timezone.now()).exists():
+        return HttpResponse(status=403)
+    template.public = False
+    template.temporary = True
+    template.save()
+    return HttpResponse(status=200)
 
 
 def booking_cancel_view(request, booking_id=None):
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..52a6fc7 100644 (file)
@@ -45,7 +45,15 @@ 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,
+    list_labs
 )
 
 urlpatterns = [
@@ -65,5 +73,17 @@ 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),
+    path('labs', list_labs),
+
     url(r'^token$', GenerateTokenView.as_view(), name='generate_token'),
 ]
index 2e5f33f..c0da1bc 100644 (file)
@@ -8,9 +8,14 @@
 # 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
@@ -23,12 +28,15 @@ 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
+from django.db.models import Q
+
 
 """
 API views.
@@ -234,3 +242,222 @@ 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)
+
+
+"""
+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)
index 0a3bfc6..5e5bc8b 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(user__username=username)
+                     for username 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()
index 626ed79..70f91fa 100644 (file)
@@ -104,5 +104,5 @@ class StatisticsManager(object):
             "user": [x, users],
             "utils": [in_use, not_in_use, maintenance],
             "projects": [project_keys, project_counts],
-            "colors": anuket_colors if os.environ['TEMPLATE_OVERRIDE_DIR'] == 'laas' else lfedge_colors
+            "colors": anuket_colors if os.environ.get('TEMPLATE_OVERRIDE_DIR') == 'laas' else lfedge_colors
         }
index cdf18ae..0b60351 100644 (file)
@@ -38,7 +38,7 @@ from booking.views import (
     booking_modify_image
 )
 
-app_name = "booking"
+app_name = 'booking'
 urlpatterns = [
     url(r'^detail/(?P<booking_id>[0-9]+)/$', booking_detail_view, name='detail'),
     url(r'^(?P<booking_id>[0-9]+)/$', booking_detail_view, name='booking_detail'),
index 2b910e7..a418c82 100644 (file)
@@ -127,7 +127,6 @@ class ResourceBookingsJSON(View):
             'start',
             'end',
             'purpose',
-            'jira_issue_status',
             'config_bundle__name'
         )
         return JsonResponse({'bookings': list(bookings)})
diff --git a/src/dashboard/templatetags/jira_filters.py b/src/dashboard/templatetags/jira_filters.py
deleted file mode 100644 (file)
index 9a97c1d..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-##############################################################################
-# Copyright (c) 2016 Max Breitenfeldt and others.
-#
-# All rights reserved. This program and the accompanying materials
-# are made available under the terms of the Apache License, Version 2.0
-# which accompanies this distribution, and is available at
-# http://www.apache.org/licenses/LICENSE-2.0
-##############################################################################
-
-
-from django.conf import settings
-from django.template.defaultfilters import register
-
-
-@register.filter
-def jira_issue_url(issue):
-    return settings.JIRA_URL + '/browse/' + str(issue)
index d5dad57..c87dacc 100644 (file)
@@ -33,7 +33,7 @@ from dashboard.views import (
     host_profile_detail_view
 )
 
-app_name = "dashboard"
+app_name = 'dashboard'
 urlpatterns = [
     url(r'^$', landing_view, name='index'),
     url(r'^lab/$', lab_list_view, name='all_labs'),
index 6b3ed09..f253fa0 100644 (file)
@@ -15,8 +15,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 # SECURITY WARNING: don't run with debug turned on in production!
 # NOTE: os.environ only returns strings, so making a comparison to
 # 'True' here will convert it to the correct Boolean value.
-DEBUG = os.environ['DEBUG'] == 'True'
-TESTING = os.environ['TEST'] == 'True'
+DEBUG = os.environ.get('DEBUG') == 'True'
+TESTING = os.environ.get('TEST') == 'True'
 
 # Application definition
 
@@ -53,29 +53,34 @@ MIDDLEWARE = [
     'account.middleware.TimezoneMiddleware',
 ]
 
-AUTH_SETTING = os.environ.get('AUTH_SETTING', 'JIRA')
 
-if AUTH_SETTING == 'LFID':
-    AUTHENTICATION_BACKENDS = ['account.views.MyOIDCAB']
+# AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend', 'account.views.MyOIDCAB']
+AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']
+
+AUTH_SETTING = os.environ.get('AUTH_SETTING')
 
+if AUTH_SETTING == 'LFID':
     # OpenID Authentications
-    OIDC_RP_CLIENT_ID = os.environ['OIDC_CLIENT_ID']
-    OIDC_RP_CLIENT_SECRET = os.environ['OIDC_CLIENT_SECRET']
+    AUTHENTICATION_BACKENDS.append('account.views.MyOIDCAB')
+    OIDC_RP_CLIENT_ID = os.environ.get('OIDC_CLIENT_ID')
+    OIDC_RP_CLIENT_SECRET = os.environ.get('OIDC_CLIENT_SECRET')
 
-    OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ['OIDC_AUTHORIZATION_ENDPOINT']
-    OIDC_OP_TOKEN_ENDPOINT = os.environ['OIDC_TOKEN_ENDPOINT']
-    OIDC_OP_USER_ENDPOINT = os.environ['OIDC_USER_ENDPOINT']
+    OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get('OIDC_AUTHORIZATION_ENDPOINT')
+    OIDC_OP_TOKEN_ENDPOINT = os.environ.get('OIDC_TOKEN_ENDPOINT')
+    OIDC_OP_USER_ENDPOINT = os.environ.get('OIDC_USER_ENDPOINT')
 
-    LOGIN_REDIRECT_URL = os.environ['DASHBOARD_URL']
-    LOGOUT_REDIRECT_URL = os.environ['DASHBOARD_URL']
+    LOGIN_REDIRECT_URL = os.environ.get('DASHBOARD_URL')
+    LOGOUT_REDIRECT_URL = os.environ.get('DASHBOARD_URL')
 
-    OIDC_RP_SIGN_ALGO = os.environ["OIDC_RP_SIGN_ALGO"]
+    OIDC_RP_SIGN_ALGO = os.environ.get("OIDC_RP_SIGN_ALGO")
 
     if OIDC_RP_SIGN_ALGO == "RS256":
-        OIDC_OP_JWKS_ENDPOINT = os.environ["OIDC_OP_JWKS_ENDPOINT"]
+        OIDC_OP_JWKS_ENDPOINT = os.environ.get("OIDC_OP_JWKS_ENDPOINT")
+else:
+    raise Exception('AUTH_SETTING set to invalid value')
 
 # This is for LFID auth setups w/ an HTTPS proxy
-if os.environ['EXPECT_HOST_FORWARDING'] == 'True':
+if os.environ.get('EXPECT_HOST_FORWARDING') == 'True':
     SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', "https")
     USE_X_FORWARDED_HOST = True
 
@@ -162,7 +167,7 @@ STATICFILES_DIRS = [
 LOGIN_REDIRECT_URL = '/'
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = os.environ['SECRET_KEY']
+SECRET_KEY = os.environ.get('SECRET_KEY')
 
 BOOTSTRAP3 = {
     'set_placeholder': False,
@@ -175,11 +180,11 @@ ALLOWED_HOSTS = ['*']
 DATABASES = {
     'default': {
         'ENGINE': 'django.db.backends.postgresql',
-        'NAME': os.environ['DB_NAME'],
-        'USER': os.environ['DB_USER'],
-        'PASSWORD': os.environ['DB_PASS'],
-        'HOST': os.environ['DB_SERVICE'],
-        'PORT': os.environ['DB_PORT']
+        'NAME': os.environ.get('DB_NAME'),
+        'USER': os.environ.get('DB_USER'),
+        'PASSWORD': os.environ.get('DB_PASS'),
+        'HOST': os.environ.get('DB_SERVICE'),
+        'PORT': os.environ.get('DB_PORT')
     }
 }
 
@@ -198,27 +203,17 @@ REST_FRAMEWORK = {
 MEDIA_ROOT = '/media'
 STATIC_ROOT = '/static'
 
-# Jira Settings
-CREATE_JIRA_TICKET = False
-
-JIRA_URL = os.environ['JIRA_URL']
-
-JIRA_USER_NAME = os.environ['JIRA_USER_NAME']
-JIRA_USER_PASSWORD = os.environ['JIRA_USER_PASSWORD']
-
-OAUTH_CONSUMER_KEY = os.environ['OAUTH_CONSUMER_KEY']
-OAUTH_CONSUMER_SECRET = os.environ['OAUTH_CONSUMER_SECRET']
-
-OAUTH_REQUEST_TOKEN_URL = JIRA_URL + '/plugins/servlet/oauth/request-token'
-OAUTH_ACCESS_TOKEN_URL = JIRA_URL + '/plugins/servlet/oauth/access-token'
-OAUTH_AUTHORIZE_URL = JIRA_URL + '/plugins/servlet/oauth/authorize'
+OAUTH_CONSUMER_KEY = os.environ.get('OAUTH_CONSUMER_KEY')
+OAUTH_CONSUMER_SECRET = os.environ.get('OAUTH_CONSUMER_SECRET')
 
-OAUTH_CALLBACK_URL = os.environ['DASHBOARD_URL'] + '/accounts/authenticated'
+OAUTH_CALLBACK_URL = os.environ.get('DASHBOARD_URL') + '/accounts/authenticated'
 
 # Celery Settings
 CELERY_TIMEZONE = 'UTC'
 
 RABBITMQ_URL = 'rabbitmq'
+# RABBITMQ_DEFAULT_USER = os.environ['DEFAULT_USER']
+# RABBITMQ_DEFAULT_PASS = os.environ['DEFAULT_PASS']
 RABBITMQ_DEFAULT_USER = os.environ['RABBITMQ_DEFAULT_USER']
 RABBITMQ_DEFAULT_PASS = os.environ['RABBITMQ_DEFAULT_PASS']
 
@@ -248,10 +243,10 @@ CELERYBEAT_SCHEDULE = {
 }
 
 # Notifier Settings
-EMAIL_HOST = os.environ['EMAIL_HOST']
-EMAIL_PORT = os.environ['EMAIL_PORT']
-EMAIL_HOST_USER = os.environ['EMAIL_HOST_USER']
-EMAIL_HOST_PASSWORD = os.environ['EMAIL_HOST_PASSWORD']
+EMAIL_HOST = os.environ.get('EMAIL_HOST')
+EMAIL_PORT = os.environ.get('EMAIL_PORT')
+EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
+EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
 EMAIL_USE_TLS = True
 DEFAULT_EMAIL_FROM = os.environ.get('DEFAULT_EMAIL_FROM', 'webmaster@localhost')
 SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
index fedb9e8..923cc33 100644 (file)
@@ -12,7 +12,7 @@ from django.conf.urls import url
 
 from notifier.views import InboxView, NotificationView
 
-app_name = "notifier"
+app_name = 'notifier'
 urlpatterns = [
     url(r'^$', InboxView, name='messages'),
     url(r'^notification/(?P<notification_id>[0-9]+)/$', NotificationView, name='notifier_single')
index 9406977..37bf33c 100644 (file)
@@ -6,20 +6,28 @@
 # which accompanies this distribution, and is available at
 # http://www.apache.org/licenses/LICENSE-2.0
 ##############################################################################
+from __future__ import annotations
+
 import re
+from typing import Optional
 from django.db.models import Q
 
 from dashboard.exceptions import ResourceAvailabilityException
 
 from resource_inventory.models import (
+    Resource,
     ResourceBundle,
     ResourceTemplate,
+    ResourceConfiguration,
     Network,
     Vlan,
     PhysicalNetwork,
     InterfaceConfiguration,
 )
 
+from account.models import Lab
+from django.contrib.auth.models import User
+
 
 class ResourceManager:
 
@@ -29,19 +37,19 @@ class ResourceManager:
         pass
 
     @staticmethod
-    def getInstance():
+    def getInstance() -> ResourceManager:
         if ResourceManager.instance is None:
             ResourceManager.instance = ResourceManager()
         return ResourceManager.instance
 
-    def getAvailableResourceTemplates(self, lab, user=None):
+    def getAvailableResourceTemplates(self, lab: Lab, user: Optional[User] = None) -> list[ResourceTemplate]:
         filter = Q(public=True)
         if user:
             filter = filter | Q(owner=user)
         filter = filter & Q(temporary=False) & Q(lab=lab)
         return ResourceTemplate.objects.filter(filter)
 
-    def templateIsReservable(self, resource_template):
+    def templateIsReservable(self, resource_template: ResourceTemplate):
         """
         Check if the required resources to reserve this template is available.
 
@@ -63,13 +71,16 @@ class ResourceManager:
         return True
 
     # public interface
-    def deleteResourceBundle(self, resourceBundle):
+    def deleteResourceBundle(self, resourceBundle: ResourceBundle):
         raise NotImplementedError("Resource Bundle Deletion Not Implemented")
 
-    def releaseResourceBundle(self, resourceBundle):
+    def releaseResourceBundle(self, resourceBundle: ResourceBundle):
         resourceBundle.release()
 
-    def get_vlans(self, resourceTemplate):
+    def get_vlans(self, resourceTemplate: ResourceTemplate) -> dict[str, int]:
+        """
+        returns: dict from network name to the associated vlan number (backend vlan id)
+        """
         networks = {}
         vlan_manager = resourceTemplate.lab.vlan_manager
         for network in resourceTemplate.networks.all():
@@ -84,7 +95,7 @@ class ResourceManager:
                 networks[network.name] = vlans[0]
         return networks
 
-    def instantiateTemplate(self, resource_template):
+    def instantiateTemplate(self, resource_template: ResourceTemplate):
         """
         Convert a ResourceTemplate into a ResourceBundle.
 
@@ -113,16 +124,18 @@ class ResourceManager:
 
         return resource_bundle
 
-    def configureNetworking(self, resource_bundle, resource, vlan_map):
+    def configureNetworking(self, resource_bundle: ResourceBundle, resource: Resource, vlan_map: dict[str, int]):
+        """
+        @vlan_map: dict from network name to the associated vlan number (backend vlan id)
+        """
         for physical_interface in resource.interfaces.all():
-            # assign interface configs
 
-            iface_configs = InterfaceConfiguration.objects.filter(
+            # assign interface configs
+            iface_config = InterfaceConfiguration.objects.get(
                 profile=physical_interface.profile,
                 resource_config=resource.config
             )
 
-            iface_config = iface_configs.first()
             physical_interface.acts_as = iface_config
             physical_interface.acts_as.save()
 
@@ -143,7 +156,7 @@ class ResourceManager:
                 )
 
     # private interface
-    def acquireHost(self, resource_config):
+    def acquireHost(self, resource_config: ResourceConfiguration) -> Resource:
         resources = resource_config.profile.get_resources(
             lab=resource_config.template.lab,
             unreserved=True
index a008176..a9a4d43 100644 (file)
@@ -29,7 +29,7 @@ from django.conf.urls import url
 from resource_inventory.views import HostView, hostprofile_detail_view
 
 
-app_name = "resource"
+app_name = 'resource'
 urlpatterns = [
     url(r'^hosts$', HostView.as_view(), name='hosts'),
     url(r'^profiles/(?P<hostprofile_id>.+)/$', hostprofile_detail_view, name='host_detail'),
diff --git a/src/templates/base/account/configuration_list.html b/src/templates/base/account/configuration_list.html
deleted file mode 100644 (file)
index fee6e83..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-{% extends "base.html" %}
-{% block content %}
-<div class="row">
-{% for config in configurations %}
-    <div class="col-12 col-md-6 col-lg-4 col-xl-3 mb-3">
-        <div class="card h-100">
-            <div class="card-header">
-                <h3>Configuration {{config.id}}</h3>
-            </div>
-            <ul class="list-group list-group-flush h-100">
-                <li class="list-group-item">id: {{config.id}}</li>
-                <li class="list-group-item">name: {{config.name}}</li>
-                <li class="list-group-item">description: {{config.description}}</li>
-                <li class="list-group-item">resource: {{config.bundle}}</li>
-            </ul>
-            <div class="card-footer">
-                <button
-                    class="btn btn-danger w-100"
-                    onclick='delete_config({{config.id}});'
-                    data-toggle="modal"
-                    data-target="#configModal"
-                >Delete</button>
-            </div>
-        </div>
-    </div>
-{% empty %}
-    <div class="col">
-        <p>You don't have any configurations. You can create a configuration by configuring a pod.</p>
-    </div>
-{% endfor %}
-</div>
-
-<script>
-    var current_config_id = -1;
-    function delete_config(config_id) {
-        current_config_id = config_id;
-    }
-
-    function submit_delete_form() {
-        var ajaxForm = $("#config_delete_form");
-        var formData = ajaxForm.serialize();
-        req = new XMLHttpRequest();
-        var url = "delete/" + current_config_id;
-        req.onreadystatechange = function() {
-            if (this.readyState == 4 && this.status == 200) {
-                location.reload();
-            }
-        };
-        req.open("POST", url, true);
-        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
-        req.onerror = function() { alert("problem submitting form"); }
-        req.send(formData);
-    }
-</script>
-
-<div class="modal fade" id="configModal" tabindex="-1" role="dialog" aria-hidden="true">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h4 class="modal-title d-inline float-left">Delete Configuration?</h4>
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-                    <span aria-hidden="true">&times;</span>
-                </button>
-            </div>
-            <form id="config_delete_form">
-                {% csrf_token %}
-            </form>
-            <div class="modal-footer d-flex flex-column">
-                <div class="mb-2">
-                    <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
-                    <button type="button" class="btn btn-danger" data-toggle="collapse" data-target="#warning">Delete</button>
-                </div>
-                <div class="collapse w-100 text-center border-top" id="warning">
-                    <div class="p-4">
-                        <h3>Are You Sure?</h3>
-                        <p>This cannot be undone</p>
-                        <button class="btn btn-outline-secondary" data-dismiss="modal">Nevermind</button>
-                        <button class="btn btn-danger" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>
-{% endblock %}
index 3092ad0..ad59c9a 100644 (file)
@@ -4,6 +4,5 @@
 <h1>Account Details</h1>
 <a class="btn btn-primary" href="{% url 'account:my-resources' %}">My Resources</a>
 <a class="btn btn-primary" href="{% url 'account:my-bookings' %}">My Bookings</a>
-<a class="btn btn-primary" href="{% url 'account:my-configurations' %}">My Configurations</a>
 <a class="btn btn-primary" href="{% url 'account:my-images' %}">My Snapshots</a>
 {% endblock content %}
index a628ab4..704bc3b 100644 (file)
@@ -87,7 +87,7 @@
                                 {% else %}
                                     <a href="{% url 'account:login' %}" class="dropdown-item Anuket-Text">
                                         <i class="fas fa-sign-in-alt"></i>
-                                        Login with Jira
+                                        Login
                                     </a>
                                 {% endif %}
                             {% endif %}
                                 <a href="{% url 'account:my-bookings' %}" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg">
                                     My Bookings
                                 </a>
-                                <a href="{% url 'account:my-configurations' %}" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg">
-                                    My Configurations
-                                </a>
-                                <a href="{% url 'account:my-images' %}" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg">
-                                    My Snapshots
-                                </a>
                             </div>
                             <a href="{% url 'dashboard:all_labs' %}" class="list-group-item list-group-item-action nav-bg">
                                 Lab Info
index f550a38..91a216c 100644 (file)
@@ -160,7 +160,7 @@ class BookingAuthManager():
             return True  # admin override for this user
         if Booking.objects.filter(owner=booking.owner, end__gt=timezone.now()).count() >= 3:
             return False
-        if len(booking.resource.template.getResources()) < 2:
+        if len(booking.resource.template.get_required_resources()) < 2:
             return True  # if they only have one server, we dont care
         if repo.BOOKING_INFO_FILE not in repo.el:
             return False  # INFO file not provided
index 40ab8f0..c05193a 100644 (file)
@@ -6,7 +6,7 @@
 # which accompanies this distribution, and is available at
 # http://www.apache.org/licenses/LICENSE-2.0
 ##############################################################################
-FROM python:3.5
+FROM python:3.9
 ENV PYTHONUNBUFFERED 1
 
 RUN apt-get update && apt-get install -y npm
index 5e24bed..edf86d1 100644 (file)
@@ -6,7 +6,7 @@
 # which accompanies this distribution, and is available at
 # http://www.apache.org/licenses/LICENSE-2.0
 ##############################################################################
-FROM python:3.5
+FROM python:3.9
 ENV PYTHONUNBUFFERED 1
 
 ADD requirements.txt /requirements.txt