summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--dashboard/__init__.py8
-rw-r--r--dashboard/docker-compose.yml1
-rw-r--r--dashboard/open-api-spec.yaml523
-rw-r--r--dashboard/requirements.txt2
-rw-r--r--dashboard/src/__init__.py8
-rw-r--r--dashboard/src/account/models.py4
-rw-r--r--dashboard/src/account/tests/test_general.py2
-rw-r--r--dashboard/src/account/views.py18
-rw-r--r--dashboard/src/api/migrations/0006_auto_20190313_1729.py23
-rw-r--r--dashboard/src/api/migrations/0007_auto_20190417_1511.py25
-rw-r--r--dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py20
-rw-r--r--dashboard/src/api/migrations/0008_auto_20190419_1414.py28
-rw-r--r--dashboard/src/api/migrations/0009_merge_20190508_1317.py14
-rw-r--r--dashboard/src/api/models.py209
-rw-r--r--dashboard/src/api/tests/test_models_unittest.py269
-rw-r--r--dashboard/src/api/tests/test_serializers.py229
-rw-r--r--dashboard/src/api/urls.py8
-rw-r--r--dashboard/src/api/views.py39
-rw-r--r--dashboard/src/booking/forms.py34
-rw-r--r--dashboard/src/booking/lib.py36
-rw-r--r--dashboard/src/booking/migrations/0005_booking_idf.py18
-rw-r--r--dashboard/src/booking/migrations/0006_booking_opnfv_config.py20
-rw-r--r--dashboard/src/booking/models.py4
-rw-r--r--dashboard/src/booking/quick_deployer.py322
-rw-r--r--dashboard/src/booking/stats.py42
-rw-r--r--dashboard/src/booking/tests/test_models.py7
-rw-r--r--dashboard/src/booking/tests/test_quick_booking.py150
-rw-r--r--dashboard/src/booking/views.py17
-rw-r--r--dashboard/src/dashboard/actions.py47
-rw-r--r--dashboard/src/dashboard/exceptions.py4
-rw-r--r--dashboard/src/dashboard/populate_db_iol.py2
-rw-r--r--dashboard/src/dashboard/tasks.py3
-rw-r--r--dashboard/src/dashboard/testing_utils.py396
-rw-r--r--dashboard/src/dashboard/views.py2
-rw-r--r--dashboard/src/notifier/manager.py4
-rw-r--r--dashboard/src/notifier/migrations/0003_auto_20190123_1741.py23
-rw-r--r--dashboard/src/notifier/migrations/0004_auto_20190124_2115.py23
-rw-r--r--dashboard/src/notifier/migrations/0005_auto_20190306_1616.py18
-rw-r--r--dashboard/src/notifier/models.py4
-rw-r--r--dashboard/src/notifier/views.py39
-rw-r--r--dashboard/src/pharos_dashboard/settings.py2
-rw-r--r--dashboard/src/resource_inventory/admin.py5
-rw-r--r--dashboard/src/resource_inventory/idf_templater.py151
-rw-r--r--dashboard/src/resource_inventory/migrations/0007_auto_20190306_1616.py31
-rw-r--r--dashboard/src/resource_inventory/migrations/0008_host_remote_management.py19
-rw-r--r--dashboard/src/resource_inventory/migrations/0009_auto_20190315_1757.py73
-rw-r--r--dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py54
-rw-r--r--dashboard/src/resource_inventory/models.py99
-rw-r--r--dashboard/src/resource_inventory/pdf_templater.py193
-rw-r--r--dashboard/src/resource_inventory/resource_manager.py194
-rw-r--r--dashboard/src/resource_inventory/urls.py5
-rw-r--r--dashboard/src/resource_inventory/views.py17
-rw-r--r--dashboard/src/static/bower.json12
-rw-r--r--dashboard/src/static/css/base.css8
-rw-r--r--dashboard/src/static/css/detail_view.css7
-rw-r--r--dashboard/src/static/js/dashboard.js1134
-rw-r--r--dashboard/src/templates/account/booking_list.html62
-rw-r--r--dashboard/src/templates/account/configuration_list.html11
-rw-r--r--dashboard/src/templates/account/image_list.html17
-rw-r--r--dashboard/src/templates/account/resource_list.html10
-rw-r--r--dashboard/src/templates/account/userprofile_update_form.html17
-rw-r--r--dashboard/src/templates/base.html301
-rw-r--r--dashboard/src/templates/booking/booking_calendar.html2
-rw-r--r--dashboard/src/templates/booking/booking_delete.html2
-rw-r--r--dashboard/src/templates/booking/booking_detail.html42
-rw-r--r--dashboard/src/templates/booking/booking_list.html36
-rw-r--r--dashboard/src/templates/booking/booking_table.html14
-rw-r--r--dashboard/src/templates/booking/quick_deploy.html157
-rw-r--r--dashboard/src/templates/booking/stats.html31
-rw-r--r--dashboard/src/templates/booking/steps/booking_confirm.html2
-rw-r--r--dashboard/src/templates/booking/steps/booking_meta.html25
-rw-r--r--dashboard/src/templates/booking/steps/resource_select.html2
-rw-r--r--dashboard/src/templates/booking/steps/swconfig_select.html2
-rw-r--r--dashboard/src/templates/config_bundle/steps/assign_host_roles.html22
-rw-r--r--dashboard/src/templates/config_bundle/steps/assign_network_roles.html22
-rw-r--r--dashboard/src/templates/config_bundle/steps/config_software.html50
-rw-r--r--dashboard/src/templates/config_bundle/steps/define_software.html135
-rw-r--r--dashboard/src/templates/config_bundle/steps/pick_installer.html32
-rw-r--r--dashboard/src/templates/config_bundle/steps/table_formset.html64
-rw-r--r--dashboard/src/templates/dashboard/genericselect.html104
-rw-r--r--dashboard/src/templates/dashboard/idf.yaml46
-rw-r--r--dashboard/src/templates/dashboard/lab_detail.html71
-rw-r--r--dashboard/src/templates/dashboard/lab_list.html107
-rw-r--r--dashboard/src/templates/dashboard/landing.html159
-rw-r--r--dashboard/src/templates/dashboard/multiple_select_filter_widget.html436
-rw-r--r--dashboard/src/templates/dashboard/pdf.yaml175
-rw-r--r--dashboard/src/templates/dashboard/resource.html10
-rw-r--r--dashboard/src/templates/dashboard/resource_all.html10
-rw-r--r--dashboard/src/templates/dashboard/searchable_select_multiple.html454
-rw-r--r--dashboard/src/templates/dashboard/table.html9
-rw-r--r--dashboard/src/templates/layout.html11
-rw-r--r--dashboard/src/templates/notifier/inbox.html185
-rw-r--r--dashboard/src/templates/notifier/notification.html65
-rw-r--r--dashboard/src/templates/resource/hostprofile_detail.html102
-rw-r--r--dashboard/src/templates/resource/hosts.html7
-rw-r--r--dashboard/src/templates/resource/steps/define_hardware.html17
-rw-r--r--dashboard/src/templates/resource/steps/host_info.html2
-rw-r--r--dashboard/src/templates/resource/steps/meta_info.html24
-rw-r--r--dashboard/src/templates/resource/steps/pod_definition.html667
-rw-r--r--dashboard/src/templates/snapshot_workflow/steps/meta.html2
-rw-r--r--dashboard/src/templates/snapshot_workflow/steps/select_host.html2
-rw-r--r--dashboard/src/templates/workflow/confirm.html15
-rw-r--r--dashboard/src/templates/workflow/exit_redirect.html6
-rw-r--r--dashboard/src/templates/workflow/resource_select.html2
-rw-r--r--dashboard/src/templates/workflow/viewport-base.html522
-rw-r--r--dashboard/src/templates/workflow/viewport-element.html2
-rw-r--r--dashboard/src/workflow/booking_workflow.py292
-rw-r--r--dashboard/src/workflow/forms.py535
-rw-r--r--dashboard/src/workflow/models.py302
-rw-r--r--dashboard/src/workflow/opnfv_workflow.py299
-rw-r--r--dashboard/src/workflow/resource_bundle_workflow.py394
-rw-r--r--dashboard/src/workflow/snapshot_workflow.py18
-rw-r--r--dashboard/src/workflow/sw_bundle_workflow.py218
-rw-r--r--dashboard/src/workflow/urls.py4
-rw-r--r--dashboard/src/workflow/views.py39
-rw-r--r--dashboard/src/workflow/workflow_factory.py57
-rw-r--r--dashboard/src/workflow/workflow_manager.py77
-rwxr-xr-xdashboard/test.sh2
118 files changed, 7146 insertions, 4010 deletions
diff --git a/dashboard/__init__.py b/dashboard/__init__.py
deleted file mode 100644
index b6fef6c..0000000
--- a/dashboard/__init__.py
+++ /dev/null
@@ -1,8 +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
-##############################################################################
diff --git a/dashboard/docker-compose.yml b/dashboard/docker-compose.yml
index f9cf0bb..eac84e6 100644
--- a/dashboard/docker-compose.yml
+++ b/dashboard/docker-compose.yml
@@ -47,6 +47,7 @@ services:
- pharos-data:/var/lib/postgresql/data
rabbitmq:
+ restart: always
image: rabbitmq
container_name: rm01
env_file: config.env
diff --git a/dashboard/open-api-spec.yaml b/dashboard/open-api-spec.yaml
new file mode 100644
index 0000000..2e8dfd6
--- /dev/null
+++ b/dashboard/open-api-spec.yaml
@@ -0,0 +1,523 @@
+---
+swagger: "2.0"
+info:
+ description: This is the Lab as a Service API
+ version: 2.0.1
+ title: LaaS API
+ contact:
+ email: nfvlab@iol.unh.edu
+ license:
+ name: Apache 2.0
+ url: http://www.apache.org/licenses/LICENSE-2.0.html
+host: virtserver.swaggerhub.com
+basePath: /IOL-OPNFV-LaaS/Labs/1.0.0
+tags:
+- name: admin
+ description: Secured Admin-only calls
+- name: developers
+ description: Operations available to regular developers
+schemes:
+- https
+paths:
+ /api/labs/{lab-name}/jobs/new:
+ get:
+ summary: list of new, unstarted jobs for the lab
+ description: |
+ List of jobs for <lab-name> to start. These jobs all must have a status of `new`,
+ meaning they are unstarted.
+ produces:
+ - application/json
+ parameters:
+ - name: lab-name
+ in: path
+ required: true
+ type: string
+ responses:
+ 200:
+ description: search results matching criteria
+ schema:
+ type: array
+ items:
+ $ref: '#/definitions/Job'
+ /api/labs/{lab-name}/jobs/current:
+ get:
+ summary: list of unfinished jobs
+ description: |
+ List of jobs for <lab-name> that are still in progress. A job is in progress if
+ it has been started but has not finished.
+ produces:
+ - application/json
+ parameters:
+ - name: lab-name
+ in: path
+ required: true
+ type: string
+ responses:
+ 200:
+ description: search results matching criteria
+ schema:
+ type: array
+ items:
+ $ref: '#/definitions/Job'
+ /api/labs/{lab-name}/jobs/done:
+ get:
+ summary: list of done jobs
+ description: |
+ List of jobs for <lab-name> that were started and are no longer in progress.
+ A job can be marked 'done' with a succesful or error status.
+ produces:
+ - application/json
+ parameters:
+ - name: lab-name
+ in: path
+ required: true
+ type: string
+ responses:
+ 200:
+ description: search results matching criteria
+ schema:
+ type: array
+ items:
+ $ref: '#/definitions/Job'
+ /api/labs/{lab-name}/jobs/{job_id}/{task_id}>:
+ post:
+ summary: update job information
+ produces:
+ - application/json
+ parameters:
+ - name: lab-name
+ in: path
+ required: true
+ type: string
+ - name: job_id
+ in: path
+ required: true
+ type: integer
+ - name: task_id
+ in: path
+ required: true
+ type: string
+ - in: body
+ name: payload
+ description: payload, schema based on job type
+ required: true
+ schema:
+ $ref: '#/definitions/JobUpdate'
+ responses:
+ 200:
+ description: success
+ /api/labs/{lab-name}/inventory:
+ get:
+ summary: lab inventory
+ produces:
+ - application/json
+ parameters:
+ - name: lab-name
+ in: path
+ required: true
+ type: string
+ responses:
+ 200:
+ description: lab inventory
+ schema:
+ $ref: '#/definitions/Inventory'
+ post:
+ summary: updates lab inventory
+ parameters:
+ - name: lab-name
+ in: path
+ required: true
+ type: string
+ - in: body
+ name: inventory
+ required: true
+ schema:
+ $ref: '#/definitions/Inventory'
+ responses:
+ 200:
+ description: success
+ /api/labs/{lab-name}/profile:
+ get:
+ summary: lab profile
+ produces:
+ - application/json
+ parameters:
+ - name: lab-name
+ in: path
+ required: true
+ type: string
+ responses:
+ 200:
+ description: lab profile
+ schema:
+ $ref: '#/definitions/Profile'
+ post:
+ summary: updates lab profile
+ parameters:
+ - name: lab-name
+ in: path
+ required: true
+ type: string
+ - in: body
+ name: profile
+ required: true
+ schema:
+ $ref: '#/definitions/Profile'
+ responses:
+ 200:
+ description: success
+definitions:
+ Host_Interface:
+ properties:
+ mac:
+ type: string
+ example: 00:11:22:33:44:55
+ description: mac address
+ busaddr:
+ type: string
+ example: 0000:02:00.1
+ description: bus address reported by `ethtool -i <ifname>`
+ switchport:
+ $ref: '#/definitions/Switchport'
+ Generic_Interface:
+ properties:
+ speed:
+ type: string
+ example: 10G
+ description: speed in M or G
+ name:
+ type: string
+ example: eno3
+ description: interface name
+ Generic_Disk:
+ properties:
+ size:
+ type: string
+ example: 500G
+ description: size in M, G, or T
+ type:
+ type: string
+ example: SSD
+ description: must be SSD or HDD
+ name:
+ type: string
+ example: sda
+ description: name of root block device
+ CPU:
+ properties:
+ cores:
+ type: integer
+ format: int32
+ example: 64
+ description: how many CPU cores the host has (across all physical cpus)
+ minimum: 1
+ arch:
+ type: string
+ example: x86_64
+ description: must be x86_64 or aarch64
+ cpus:
+ type: integer
+ example: 2
+ description: Number of different physical CPU chips
+ minimum: 1
+ Image:
+ properties:
+ name:
+ type: string
+ description:
+ type: string
+ lab_id:
+ type: string
+ description: identifier provided by lab
+ dashboard_id:
+ type: string
+ description: identifier provided by dashboard
+ Inventory_Host:
+ properties:
+ interfaces:
+ type: array
+ items:
+ $ref: '#/definitions/Host_Interface'
+ hostname:
+ type: string
+ example: hpe3.opnfv.iol.unh.edu
+ description: globally unique fqdn
+ host_type:
+ type: string
+ description: name of host type this host belongs to
+ Inventory_Network:
+ properties:
+ cidr:
+ type: string
+ example: 174.0.5.0/24
+ description: subnet description
+ gateway:
+ type: string
+ example: 174.0.5.1
+ description: ip of gateway
+ vlan:
+ type: integer
+ example: 100
+ description: vlan tag of this network
+ Inventory:
+ properties:
+ hosts:
+ type: array
+ description: all hosts
+ items:
+ $ref: '#/definitions/Inventory_Host'
+ networks:
+ type: array
+ description: all networks
+ items:
+ $ref: '#/definitions/Inventory_Network'
+ images:
+ type: array
+ description: available images
+ items:
+ $ref: '#/definitions/Image'
+ host_types:
+ type: array
+ description: all host types hosted by a lab
+ items:
+ $ref: '#/definitions/Host_Type'
+ Host_Type:
+ properties:
+ cpu:
+ $ref: '#/definitions/CPU'
+ disks:
+ type: array
+ items:
+ $ref: '#/definitions/Generic_Disk'
+ description:
+ type: string
+ description: human readable description of host type
+ interface:
+ type: array
+ items:
+ $ref: '#/definitions/Generic_Interface'
+ ram:
+ $ref: '#/definitions/Ram'
+ name:
+ type: string
+ description: lab-unique name
+ Ram:
+ properties:
+ amount:
+ type: integer
+ example: 16
+ description: amount of ram in Gibibytes (GiB)
+ Switchport:
+ properties:
+ switch_name:
+ type: string
+ example: Cisco-9
+ description: name of switch owning this switchport
+ port_name:
+ type: string
+ example: Ethernet1/34
+ description: name of port on switch
+ invariant_config:
+ type: array
+ description: list of vlans that may not be modified on this port
+ items:
+ $ref: '#/definitions/Vlan'
+ current_config:
+ type: array
+ description: list of current vlan configuration
+ items:
+ $ref: '#/definitions/Vlan'
+ Vlan:
+ properties:
+ vlan_id:
+ type: integer
+ example: 100
+ description: vlan id
+ minimum: 1
+ maximum: 4098
+ tagged:
+ type: boolean
+ example: true
+ description: whether this vlan is tagged or untagged
+ Job:
+ properties:
+ id:
+ type: integer
+ description: globally unique job identifier
+ payload:
+ $ref: '#/definitions/JobPayload'
+ JobPayload:
+ properties:
+ hardware:
+ $ref: '#/definitions/HardwareTask'
+ software:
+ $ref: '#/definitions/SoftwareTask'
+ network:
+ $ref: '#/definitions/NetworkTask'
+ access:
+ $ref: '#/definitions/AccessTask'
+ snapshot:
+ $ref: '#/definitions/SnapshotTask'
+ HardwareTask:
+ properties:
+ taskId:
+ $ref: '#/definitions/HardwareConfig'
+ SoftwareTask:
+ properties:
+ taskId:
+ $ref: '#/definitions/SoftwarePayload'
+ NetworkTask:
+ properties:
+ taskId:
+ $ref: '#/definitions/NetworkPayload'
+ AccessTask:
+ properties:
+ taskId:
+ $ref: '#/definitions/AccessPayload'
+ SnapshotTask:
+ properties:
+ taskId:
+ $ref: '#/definitions/SnapshotPayload'
+ SnapshotPayload:
+ properties:
+ host:
+ type: string
+ example: hpe3
+ description: how the lab identifies the host
+ image:
+ type: string
+ example: "4"
+ description: lab id of existing image, if updating an existing image. if this key does not exist, the lab must create a new image
+ dashboard_id:
+ type: string
+ description: how the dashboard identifies this image / snapshot
+ AccessPayload:
+ properties:
+ revoke:
+ type: boolean
+ description: whether to revoke key during completion of job
+ user:
+ type: string
+ description: PK/ID of user access is being given to
+ access_type:
+ type: string
+ example: ssh
+ description: type of access key to be generated. Options include "vpn and ssh"
+ hosts:
+ type: array
+ description: hosts to grant access to if applicable
+ items:
+ type: string
+ description: id of host
+ lab_token:
+ type: string
+ description: identifier provided by lab to this task
+ HardwareConfig:
+ properties:
+ id:
+ type: string
+ description: ID of host
+ image:
+ type: integer
+ example: 42
+ description: lab provided ID of the request image
+ power:
+ type: string
+ example: on
+ description: desired power state, either on or off
+ hostname:
+ type: string
+ example: my_new_machine
+ description: user-defined hostname
+ ipmi_create:
+ type: boolean
+ description: whether or not to create an ipmi account
+ lab_token:
+ type: string
+ description: identifier provided by lab to this task
+ SoftwarePayload:
+ properties:
+ opnfv:
+ $ref: '#/definitions/OpnfvConfiguration'
+ lab_token:
+ type: string
+ description: identifier provided by lab to this task
+ OpnfvHost:
+ properties:
+ hostname:
+ type: string
+ example: Jumphost
+ description: maps hostname to OPNFV role
+ OpnfvConfiguration:
+ properties:
+ installer:
+ type: string
+ description: Installer user wants
+ scenario:
+ type: string
+ description: scenario of OPNFV to deploy
+ pdf:
+ type: string
+ example: LaaS.com/api/my_job/pdf
+ description: URL to find the Pod Descriptor File contents
+ idf:
+ type: string
+ example: LaaS.com/api/my_job/idf
+ description: URL to find the Installer Descriptor File contents
+ roles:
+ type: array
+ description: role the host will play in OPNFV
+ items:
+ $ref: '#/definitions/OpnfvHost'
+ NetworkPayload:
+ properties:
+ hostId:
+ $ref: '#/definitions/NetworkConfig'
+ lab_token:
+ type: string
+ description: identifier provided by lab to this task
+ NetworkConfig:
+ properties:
+ interface_name:
+ type: array
+ description: list of vlans on this interface
+ items:
+ $ref: '#/definitions/Vlan'
+ JobUpdate:
+ properties:
+ status:
+ type: integer
+ description: status type, see status enum
+ message:
+ type: string
+ description: message from lab for user
+ lab_token:
+ type: string
+ description: identifier provided by lab to this task
+ Profile:
+ properties:
+ name:
+ type: string
+ description: proper expanded lab name
+ contact:
+ $ref: '#/definitions/Contact'
+ description:
+ type: string
+ host_count:
+ type: array
+ items:
+ $ref: '#/definitions/Host_Number'
+ Host_Number:
+ properties:
+ type:
+ type: string
+ count:
+ type: integer
+ Contact:
+ properties:
+ phone:
+ type: string
+ description: phone number at which a lab can be reached
+ email:
+ type: string
+ description: email at which a lab can be reached
diff --git a/dashboard/requirements.txt b/dashboard/requirements.txt
index 9ea10a4..55e5fc9 100644
--- a/dashboard/requirements.txt
+++ b/dashboard/requirements.txt
@@ -1,7 +1,7 @@
celery==3.1.23
cryptography==2.3.1
Django==2.1
-django-bootstrap3==10.0.1
+django-bootstrap4==0.0.8
django-crispy-forms==1.7.2
django-filter==2.0.0
django-registration==2.1.2
diff --git a/dashboard/src/__init__.py b/dashboard/src/__init__.py
deleted file mode 100644
index b6fef6c..0000000
--- a/dashboard/src/__init__.py
+++ /dev/null
@@ -1,8 +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
-##############################################################################
diff --git a/dashboard/src/account/models.py b/dashboard/src/account/models.py
index 0f8154e..4fc7c40 100644
--- a/dashboard/src/account/models.py
+++ b/dashboard/src/account/models.py
@@ -61,7 +61,7 @@ class VlanManager(models.Model):
new_vlan = vlans.index(1) # will throw if none available
vlans[new_vlan] = 0
allocated.append(new_vlan)
- if count is 1:
+ if count == 1:
return allocated[0]
return allocated
@@ -131,7 +131,7 @@ class VlanManager(models.Model):
vlans = set(vlans)
for vlan in vlans:
- if my_vlans[vlan] is 0:
+ if my_vlans[vlan] == 0:
raise ValueError("vlan " + str(vlan) + " is not available")
my_vlans[vlan] = 0
diff --git a/dashboard/src/account/tests/test_general.py b/dashboard/src/account/tests/test_general.py
index 57ad291..3fb52b0 100644
--- a/dashboard/src/account/tests/test_general.py
+++ b/dashboard/src/account/tests/test_general.py
@@ -47,7 +47,7 @@ class AccountMiddlewareTestCase(TestCase):
self.user1profile.timezone = 'Etc/Greenwich'
self.user1profile.save()
self.client.get(url)
- self.assertEqual(timezone.get_current_timezone_name(), 'Etc/Greenwich')
+ self.assertEqual(timezone.get_current_timezone_name(), 'GMT')
# if there is no profile for a user, it should be created
user2 = User.objects.create(username='user2')
diff --git a/dashboard/src/account/views.py b/dashboard/src/account/views.py
index 11689a1..2b4eccb 100644
--- a/dashboard/src/account/views.py
+++ b/dashboard/src/account/views.py
@@ -14,6 +14,7 @@ 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.decorators import login_required
@@ -21,7 +22,6 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.urls import reverse
from django.http import HttpResponse
-from django.utils import timezone
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.generic import RedirectView, TemplateView, UpdateView
@@ -198,9 +198,17 @@ def account_booking_view(request):
if not request.user.is_authenticated:
return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
template = "account/booking_list.html"
- bookings = list(Booking.objects.filter(owner=request.user).order_by("-start"))
- collab_bookings = list(request.user.collaborators.all().order_by("-start"))
- context = {"title": "My Bookings", "bookings": bookings, "collab_bookings": collab_bookings}
+ bookings = list(Booking.objects.filter(owner=request.user, end__gt=timezone.now()).order_by("-start"))
+ my_old_bookings = Booking.objects.filter(owner=request.user, end__lt=timezone.now()).order_by("-start")
+ collab_old_bookings = request.user.collaborators.filter(end__lt=timezone.now()).order_by("-start")
+ expired_bookings = list(my_old_bookings.union(collab_old_bookings))
+ collab_bookings = list(request.user.collaborators.filter(end__gt=timezone.now()).order_by("-start"))
+ context = {
+ "title": "My Bookings",
+ "bookings": bookings,
+ "collab_bookings": collab_bookings,
+ "expired_bookings": expired_bookings
+ }
return render(request, template, context=context)
@@ -228,7 +236,7 @@ def account_images_view(request):
"images": my_images,
"public_images": public_images,
"used_images": used_images
- }
+ }
return render(request, template, context=context)
diff --git a/dashboard/src/api/migrations/0006_auto_20190313_1729.py b/dashboard/src/api/migrations/0006_auto_20190313_1729.py
new file mode 100644
index 0000000..ec148bd
--- /dev/null
+++ b/dashboard/src/api/migrations/0006_auto_20190313_1729.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.1 on 2019-03-13 17:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0005_snapshotconfig_delta'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='opnfvapiconfig',
+ name='installer',
+ field=models.CharField(max_length=200),
+ ),
+ migrations.AlterField(
+ model_name='opnfvapiconfig',
+ name='scenario',
+ field=models.CharField(max_length=300),
+ ),
+ ]
diff --git a/dashboard/src/api/migrations/0007_auto_20190417_1511.py b/dashboard/src/api/migrations/0007_auto_20190417_1511.py
new file mode 100644
index 0000000..e7d2c59
--- /dev/null
+++ b/dashboard/src/api/migrations/0007_auto_20190417_1511.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.1 on 2019-04-17 15:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0006_auto_20190313_1729'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='opnfvapiconfig',
+ name='idf',
+ field=models.CharField(default='', max_length=100),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='opnfvapiconfig',
+ name='pdf',
+ field=models.CharField(default='', max_length=100),
+ preserve_default=False,
+ ),
+ ]
diff --git a/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py b/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py
new file mode 100644
index 0000000..46f3631
--- /dev/null
+++ b/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1 on 2019-05-01 18:53
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0010_auto_20190430_1405'),
+ ('api', '0006_auto_20190313_1729'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='opnfvapiconfig',
+ name='opnfv_config',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.OPNFVConfig'),
+ ),
+ ]
diff --git a/dashboard/src/api/migrations/0008_auto_20190419_1414.py b/dashboard/src/api/migrations/0008_auto_20190419_1414.py
new file mode 100644
index 0000000..03c3865
--- /dev/null
+++ b/dashboard/src/api/migrations/0008_auto_20190419_1414.py
@@ -0,0 +1,28 @@
+# Generated by Django 2.1 on 2019-04-19 14:14
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0009_auto_20190315_1757'),
+ ('api', '0007_auto_20190417_1511'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BridgeConfig',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('interfaces', models.ManyToManyField(to='resource_inventory.Interface')),
+ ('opnfv_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.OPNFVConfig')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='opnfvapiconfig',
+ name='bridge_config',
+ field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.BridgeConfig'),
+ ),
+ ]
diff --git a/dashboard/src/api/migrations/0009_merge_20190508_1317.py b/dashboard/src/api/migrations/0009_merge_20190508_1317.py
new file mode 100644
index 0000000..1a34380
--- /dev/null
+++ b/dashboard/src/api/migrations/0009_merge_20190508_1317.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.1 on 2019-05-08 13:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0008_auto_20190419_1414'),
+ ('api', '0007_opnfvapiconfig_opnfv_config'),
+ ]
+
+ operations = [
+ ]
diff --git a/dashboard/src/api/models.py b/dashboard/src/api/models.py
index b14ea2f..1f708ae 100644
--- a/dashboard/src/api/models.py
+++ b/dashboard/src/api/models.py
@@ -11,6 +11,8 @@
from django.contrib.auth.models import User
from django.db import models
from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404
+from django.urls import reverse
import json
import uuid
@@ -21,8 +23,13 @@ from resource_inventory.models import (
HostProfile,
Host,
Image,
- Interface
+ Interface,
+ HostOPNFVConfig,
+ RemoteInfo,
+ OPNFVConfig
)
+from resource_inventory.idf_templater import IDFTemplater
+from resource_inventory.pdf_templater import PDFTemplater
class JobStatus(object):
@@ -42,7 +49,7 @@ class LabManagerTracker(object):
"""
try:
lab = Lab.objects.get(name=lab_name)
- except:
+ except Exception:
raise PermissionDenied("Lab not found")
if lab.api_token == token:
return LabManager(lab)
@@ -60,6 +67,47 @@ class LabManager(object):
def __init__(self, lab):
self.lab = lab
+ def update_host_remote_info(self, data, host_id):
+ host = get_object_or_404(Host, labid=host_id, lab=self.lab)
+ info = {}
+ try:
+ info['address'] = data['address']
+ info['mac_address'] = data['mac_address']
+ info['password'] = data['password']
+ info['user'] = data['user']
+ info['type'] = data['type']
+ info['versions'] = json.dumps(data['versions'])
+ except Exception as e:
+ return {"error": "invalid arguement: " + str(e)}
+ remote_info = host.remote_management
+ if "default" in remote_info.mac_address:
+ remote_info = RemoteInfo()
+ remote_info.address = info['address']
+ remote_info.mac_address = info['mac_address']
+ remote_info.password = info['password']
+ remote_info.user = info['user']
+ remote_info.type = info['type']
+ remote_info.versions = info['versions']
+ remote_info.save()
+ host.remote_management = remote_info
+ host.save()
+ booking = Booking.objects.get(resource=host.bundle)
+ self.update_xdf(booking)
+ return {"status": "success"}
+
+ def update_xdf(self, booking):
+ booking.pdf = PDFTemplater.makePDF(booking)
+ booking.idf = IDFTemplater().makeIDF(booking)
+ booking.save()
+
+ def get_pdf(self, booking_id):
+ booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
+ return booking.pdf
+
+ def get_idf(self, booking_id):
+ booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab)
+ return booking.idf
+
def get_profile(self):
prof = {}
prof['name'] = self.lab.name
@@ -88,6 +136,22 @@ class LabManager(object):
inventory['host_types'] = self.serialize_host_profiles(profiles)
return inventory
+ def get_host(self, hostname):
+ host = get_object_or_404(Host, labid=hostname, lab=self.lab)
+ return {
+ "booked": host.booked,
+ "working": host.working,
+ "type": host.profile.name
+ }
+
+ def update_host(self, hostname, data):
+ host = get_object_or_404(Host, labid=hostname, lab=self.lab)
+ if "working" in data:
+ working = data['working'] == "true"
+ host.working = working
+ host.save()
+ return self.get_host(hostname)
+
def get_status(self):
return {"status": self.lab.status}
@@ -297,25 +361,71 @@ class TaskConfig(models.Model):
self.delta = '{}'
+class BridgeConfig(models.Model):
+ """
+ Displays mapping between jumphost interfaces and
+ bridges
+ """
+ interfaces = models.ManyToManyField(Interface)
+ opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
+
+ def to_dict(self):
+ d = {}
+ hid = self.interfaces.first().host.labid
+ d[hid] = {}
+ for interface in self.interfaces.all():
+ d[hid][interface.mac_address] = []
+ for vlan in interface.config.all():
+ network_role = self.opnfv_model.networks().filter(network=vlan.network)
+ bridge = IDFTemplater.bridge_names[network_role.name]
+ br_config = {
+ "vlan_id": vlan.vlan_id,
+ "tagged": vlan.tagged,
+ "bridge": bridge
+ }
+ d[hid][interface.mac_address].append(br_config)
+ return d
+
+ def to_json(self):
+ return json.dumps(self.to_dict())
+
+
class OpnfvApiConfig(models.Model):
- installer = models.CharField(max_length=100)
- scenario = models.CharField(max_length=100)
+ installer = models.CharField(max_length=200)
+ scenario = models.CharField(max_length=300)
roles = models.ManyToManyField(Host)
+ # pdf and idf are url endpoints, not the actual file
+ pdf = models.CharField(max_length=100)
+ idf = models.CharField(max_length=100)
+ bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True)
delta = models.TextField()
+ opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL)
def to_dict(self):
d = {}
+ if not self.opnfv_config:
+ return d
if self.installer:
d['installer'] = self.installer
if self.scenario:
d['scenario'] = self.scenario
+ if self.pdf:
+ d['pdf'] = self.pdf
+ if self.idf:
+ d['idf'] = self.idf
+ if self.bridge_config:
+ d['bridged_interfaces'] = self.bridge_config.to_dict()
hosts = self.roles.all()
if hosts.exists():
d['roles'] = []
- for host in self.roles.all():
- d['roles'].append({host.labid: host.config.opnfvRole.name})
+ for host in hosts:
+ d['roles'].append({
+ host.labid: self.opnfv_config.host_opnfv_config.get(
+ host_config__pk=host.config.pk
+ ).role.name
+ })
return d
@@ -334,6 +444,16 @@ class OpnfvApiConfig(models.Model):
d['scenario'] = scenario
self.delta = json.dumps(d)
+ def set_xdf(self, booking, update_delta=True):
+ kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id}
+ self.pdf = reverse('get-pdf', kwargs=kwargs)
+ self.idf = reverse('get-idf', kwargs=kwargs)
+ if update_delta:
+ d = json.loads(self.delta)
+ d['pdf'] = self.pdf
+ d['idf'] = self.idf
+ self.delta = json.dumps(d)
+
def add_role(self, host):
self.roles.add(host)
d = json.loads(self.delta)
@@ -366,7 +486,7 @@ class AccessConfig(TaskConfig):
d['revoke'] = self.revoke
try:
d['context'] = json.loads(self.context)
- except:
+ except Exception:
pass
return d
@@ -571,6 +691,7 @@ class SnapshotConfig(TaskConfig):
if not self.delta:
self.delta = self.to_json()
self.save()
+
d = json.loads(self.delta)
return d
@@ -721,14 +842,12 @@ class JobFactory(object):
net_relation.status = JobStatus.NEW
# re-apply ssh access after host is reset
- ssh_relation = AccessRelation.objects.get(job=job, config__access_type="ssh")
- ssh_relation.status = JobStatus.NEW
+ for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
+ relation.status = JobStatus.NEW
+ relation.save()
- # save them all at once to reduce the chance
- # of a lab polling and only seeing partial change
hardware_relation.save()
net_relation.save()
- ssh_relation.save()
@classmethod
def makeSnapshotTask(cls, image, booking, host):
@@ -753,7 +872,7 @@ class JobFactory(object):
job = None
try:
job = Job.objects.get(booking=booking)
- except:
+ except Exception:
job = Job.objects.create(status=JobStatus.NEW, booking=booking)
cls.makeHardwareConfigs(
hosts=hosts,
@@ -764,7 +883,7 @@ class JobFactory(object):
job=job
)
cls.makeSoftware(
- hosts=hosts,
+ booking=booking,
job=job
)
all_users = list(booking.collaborators.all())
@@ -796,7 +915,7 @@ class JobFactory(object):
hardware_config = None
try:
hardware_config = HardwareConfig.objects.get(relation__host=host)
- except:
+ except Exception:
hardware_config = HardwareConfig()
relation = HostHardwareRelation()
@@ -839,7 +958,7 @@ class JobFactory(object):
network_config = None
try:
network_config = NetworkConfig.objects.get(relation__host=host)
- except:
+ except Exception:
network_config = NetworkConfig.objects.create()
relation = HostNetworkRelation()
@@ -855,28 +974,42 @@ class JobFactory(object):
network_config.save()
@classmethod
- def makeSoftware(cls, hosts=[], job=Job()):
- def init_config(host):
- opnfv_config = OpnfvApiConfig()
- if host is not None:
- opnfv = host.config.bundle.opnfv_config.first()
- opnfv_config.installer = opnfv.installer.name
- opnfv_config.scenario = opnfv.scenario.name
- opnfv_config.save()
- return opnfv_config
-
+ def make_bridge_config(cls, booking):
+ if booking.resource.hosts.count() < 2:
+ return None
try:
- host = None
- if len(hosts) > 0:
- host = hosts[0]
- opnfv_config = init_config(host)
+ jumphost_config = HostOPNFVConfig.objects.filter(
+ role__name__iexact="jumphost"
+ )
+ jumphost = Host.objects.get(
+ bundle=booking.resource,
+ config=jumphost_config.host_config
+ )
+ except Exception:
+ return None
+ br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
+ for iface in jumphost.interfaces.all():
+ br_config.interfaces.add(iface)
+ return br_config
- for host in hosts:
- opnfv_config.roles.add(host)
- software_config = SoftwareConfig.objects.create(opnfv=opnfv_config)
- software_config.save()
- software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
- software_relation.save()
- return software_relation
- except:
+ @classmethod
+ def makeSoftware(cls, booking=None, job=Job()):
+
+ if not booking.opnfv_config:
return None
+
+ opnfv_api_config = OpnfvApiConfig.objects.create(
+ opnfv_config=booking.opnfv_config,
+ installer=booking.opnfv_config.installer.name,
+ scenario=booking.opnfv_config.scenario.name,
+ bridge_config=cls.make_bridge_config(booking)
+ )
+
+ opnfv_api_config.set_xdf(booking, False)
+ opnfv_api_config.save()
+
+ for host in booking.resource.hosts.all():
+ opnfv_api_config.roles.add(host)
+ software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config)
+ software_relation = SoftwareRelation.objects.create(job=job, config=software_config)
+ return software_relation
diff --git a/dashboard/src/api/tests/test_models_unittest.py b/dashboard/src/api/tests/test_models_unittest.py
new file mode 100644
index 0000000..e6f97a6
--- /dev/null
+++ b/dashboard/src/api/tests/test_models_unittest.py
@@ -0,0 +1,269 @@
+##############################################################################
+# Copyright (c) 2019 Sawyer Bergeron, Parker Berberian, 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 api.models import (
+ Job,
+ JobStatus,
+ JobFactory,
+ HostNetworkRelation,
+ HostHardwareRelation,
+ SoftwareRelation,
+ AccessConfig,
+ SnapshotRelation
+)
+
+from resource_inventory.models import (
+ OPNFVRole,
+ HostProfile,
+)
+
+from django.test import TestCase, Client
+
+from dashboard.testing_utils import (
+ make_host,
+ make_user,
+ make_user_profile,
+ make_lab,
+ make_installer,
+ make_image,
+ make_scenario,
+ make_os,
+ make_complete_host_profile,
+ make_booking,
+)
+
+
+class ValidBookingCreatesValidJob(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.user = make_user(False, username="newtestuser", password="testpassword")
+ cls.userprofile = make_user_profile(cls.user)
+ cls.lab = make_lab()
+
+ cls.host_profile = make_complete_host_profile(cls.lab)
+ cls.scenario = make_scenario()
+ cls.installer = make_installer([cls.scenario])
+ os = make_os([cls.installer])
+ cls.image = make_image(cls.lab, 1, cls.user, os, cls.host_profile)
+ for i in range(30):
+ make_host(cls.host_profile, cls.lab, name="host" + str(i), labid="host" + str(i))
+ cls.client = Client()
+
+ def setUp(self):
+ self.booking, self.compute_hostnames, self.jump_hostname = self.create_multinode_generic_booking()
+
+ def create_multinode_generic_booking(self):
+ topology = {}
+
+ compute_hostnames = ["cmp01", "cmp02", "cmp03"]
+
+ host_type = HostProfile.objects.first()
+
+ universal_networks = [
+ {"name": "public", "tagged": False, "public": True},
+ {"name": "admin", "tagged": True, "public": False}]
+ compute_networks = [{"name": "private", "tagged": True, "public": False}]
+ jumphost_networks = [{"name": "external", "tagged": True, "public": True}]
+
+ # generate a bunch of extra networks
+ for i in range(10):
+ net = {"tagged": False, "public": False}
+ net["name"] = "net" + str(i)
+ universal_networks.append(net)
+
+ jumphost_info = {
+ "type": host_type,
+ "role": OPNFVRole.objects.get_or_create(name="Jumphost")[0],
+ "nets": self.make_networks(host_type, jumphost_networks + universal_networks),
+ "image": self.image
+ }
+ topology["jump"] = jumphost_info
+
+ for hostname in compute_hostnames:
+ host_info = {
+ "type": host_type,
+ "role": OPNFVRole.objects.get_or_create(name="Compute")[0],
+ "nets": self.make_networks(host_type, compute_networks + universal_networks),
+ "image": self.image
+ }
+ topology[hostname] = host_info
+
+ booking = make_booking(
+ owner=self.user,
+ lab=self.lab,
+ topology=topology,
+ installer=self.installer,
+ scenario=self.scenario
+ )
+
+ if not booking.resource:
+ raise Exception("Booking does not have a resource when trying to pass to makeCompleteJob")
+ return booking, compute_hostnames, "jump"
+
+ def make_networks(self, hostprofile, nets):
+ """
+ distributes nets accross hostprofile's interfaces
+ returns a 2D array
+ """
+ network_struct = []
+ count = hostprofile.interfaceprofile.all().count()
+ for i in range(count):
+ network_struct.append([])
+ while(nets):
+ index = len(nets) % count
+ network_struct[index].append(nets.pop())
+
+ return network_struct
+
+ #################################################################
+ # Complete Job Tests
+ #################################################################
+
+ def test_complete_job_makes_access_configs(self):
+ JobFactory.makeCompleteJob(self.booking)
+ job = Job.objects.get(booking=self.booking)
+ self.assertIsNotNone(job)
+
+ access_configs = AccessConfig.objects.filter(accessrelation__job=job)
+
+ vpn_configs = access_configs.filter(access_type="vpn")
+ ssh_configs = access_configs.filter(access_type="ssh")
+
+ self.assertFalse(AccessConfig.objects.exclude(access_type__in=["vpn", "ssh"]).exists())
+
+ all_users = list(self.booking.collaborators.all())
+ all_users.append(self.booking.owner)
+
+ for user in all_users:
+ self.assertTrue(vpn_configs.filter(user=user).exists())
+ self.assertTrue(ssh_configs.filter(user=user).exists())
+
+ def test_complete_job_makes_network_configs(self):
+ JobFactory.makeCompleteJob(self.booking)
+ job = Job.objects.get(booking=self.booking)
+ self.assertIsNotNone(job)
+
+ booking_hosts = self.booking.resource.hosts.all()
+
+ netrelations = HostNetworkRelation.objects.filter(job=job)
+ netconfigs = [r.config for r in netrelations]
+
+ netrelation_hosts = [r.host for r in netrelations]
+
+ for config in netconfigs:
+ for interface in config.interfaces.all():
+ self.assertTrue(interface.host in booking_hosts)
+
+ # if no interfaces are referenced that shouldn't have vlans,
+ # and no vlans exist outside those accounted for in netconfigs,
+ # then the api is faithfully representing networks
+ # as netconfigs reference resource_inventory models directly
+
+ # this test relies on the assumption that
+ # every interface is configured, whether it does or does not have vlans
+ # if this is not true, the test fails
+
+ for host in booking_hosts:
+ self.assertTrue(host in netrelation_hosts)
+ relation = HostNetworkRelation.objects.filter(job=job).get(host=host)
+
+ # do 2 direction matching that interfaces are one to one
+ config = relation.config
+ for interface in config.interfaces.all():
+ self.assertTrue(interface in host.interfaces)
+ for interface in host.interfaces.all():
+ self.assertTrue(interface in config.interfaces)
+
+ for host in netrelation_hosts:
+ self.assertTrue(host in booking_hosts)
+
+ def test_complete_job_makes_hardware_configs(self):
+ JobFactory.makeCompleteJob(self.booking)
+ job = Job.objects.get(booking=self.booking)
+ self.assertIsNotNone(job)
+
+ hardware_relations = HostHardwareRelation.objects.filter(job=job)
+
+ job_hosts = [r.host for r in hardware_relations]
+
+ booking_hosts = self.booking.resource.hosts.all()
+
+ self.assertEqual(len(booking_hosts), len(job_hosts))
+
+ for relation in hardware_relations:
+ self.assertTrue(relation.host in booking_hosts)
+ self.assertEqual(relation.status, JobStatus.NEW)
+ config = relation.config
+ host = relation.host
+ self.assertEqual(config.hostname, host.template.resource.name)
+
+ def test_complete_job_makes_software_configs(self):
+ JobFactory.makeCompleteJob(self.booking)
+ job = Job.objects.get(booking=self.booking)
+ self.assertIsNotNone(job)
+
+ srelation = SoftwareRelation.objects.filter(job=job).first()
+ self.assertIsNotNone(srelation)
+
+ sconfig = srelation.config
+ self.assertIsNotNone(sconfig)
+
+ oconfig = sconfig.opnfv
+ self.assertIsNotNone(oconfig)
+
+ # not onetoone in models, but first() is safe here based on how ConfigBundle and a matching OPNFVConfig are created
+ # this should, however, be made explicit
+ self.assertEqual(oconfig.installer, self.booking.config_bundle.opnfv_config.first().installer.name)
+ self.assertEqual(oconfig.scenario, self.booking.config_bundle.opnfv_config.first().scenario.name)
+
+ for host in oconfig.roles.all():
+ role_name = host.config.host_opnfv_config.first().role.name
+ if str(role_name).lower() == "jumphost":
+ self.assertEqual(host.template.resource.name, self.jump_hostname)
+ elif str(role_name).lower() == "compute":
+ self.assertTrue(host.template.resource.name in self.compute_hostnames)
+ else:
+ self.fail(msg="Host with non-configured role name related to job: " + str(role_name))
+
+ def test_make_snapshot_task(self):
+ host = self.booking.resource.hosts.first()
+ image = make_image(self.lab, -1, None, None, host.profile)
+
+ Job.objects.create(booking=self.booking)
+
+ JobFactory.makeSnapshotTask(image, self.booking, host)
+
+ snap_relation = SnapshotRelation.objects.get(job=self.booking.job)
+ config = snap_relation.config
+ self.assertEqual(host.id, config.host.id)
+ self.assertEqual(config.dashboard_id, image.id)
+ self.assertEqual(snap_relation.snapshot.id, image.id)
+
+ def test_make_hardware_configs(self):
+ hosts = self.booking.resource.hosts.all()
+ job = Job.objects.create(booking=self.booking)
+ JobFactory.makeHardwareConfigs(hosts=hosts, job=job)
+
+ hardware_relations = HostHardwareRelation.objects.filter(job=job)
+
+ self.assertEqual(hardware_relations.count(), hosts.count())
+
+ host_set = set([h.id for h in hosts])
+
+ for relation in hardware_relations:
+ try:
+ host_set.remove(relation.host.id)
+ except KeyError:
+ self.fail("Hardware Relation/Config not created for host " + str(relation.host))
+
+ self.assertEqual(relation.config.power, "on")
+ self.assertTrue(relation.config.ipmi_create)
+ # TODO: the rest of hwconf attrs
+
+ self.assertEqual(len(host_set), 0)
diff --git a/dashboard/src/api/tests/test_serializers.py b/dashboard/src/api/tests/test_serializers.py
deleted file mode 100644
index c1fa5af..0000000
--- a/dashboard/src/api/tests/test_serializers.py
+++ /dev/null
@@ -1,229 +0,0 @@
-##############################################################################
-# Copyright (c) 2018 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 django.test import TestCase
-from booking.models import Booking
-from account.models import Lab
-from api.serializers.booking_serializer import BookingField
-from datetime import timedelta
-from django.utils import timezone
-from django.contrib.auth.models import Permission, User
-from resource_inventory.models import (
- Image,
- OPNFVRole,
- HostConfiguration,
- HostProfile,
- InterfaceProfile,
- DiskProfile,
- CpuProfile,
- RamProfile,
- GenericResourceBundle,
- GenericResource,
- GenericHost,
- Host,
- Vlan,
- Interface,
- ConfigBundle,
- ResourceBundle
-)
-
-
-class BookingSerializerTestCase(TestCase):
-
- count = 0
-
- def makeHostConfigurations(self, hosts, config):
- lab_user = User.objects.create(username="asfasdfasdf")
- owner = User.objects.create(username="asfasdfasdffffff")
- lab = Lab.objects.create(
- lab_user=lab_user,
- name="TestLab123123",
- contact_email="mail@email.com",
- contact_phone=""
- )
- jumphost = True
- for host in hosts:
- image = Image.objects.create(
- lab_id=12,
- from_lab=lab,
- name="this is a test image",
- owner=owner
- )
- name = "jumphost"
- if not jumphost:
- name = "compute"
- role = OPNFVRole.objects.create(
- name=name,
- description="stuff"
- )
-
- HostConfiguration.objects.create(
- host=host,
- image=image,
- bundle=config,
- opnfvRole=role
- )
- jumphost = False
-
- def setUp(self):
- self.serializer = BookingField()
- lab_user = User.objects.create(username="lab user")
- lab = Lab.objects.create(name="test lab", lab_user=lab_user)
- # create hostProfile
- hostProfile = HostProfile.objects.create(
- host_type=0,
- name='Test profile',
- description='a test profile'
- )
- InterfaceProfile.objects.create(
- speed=1000,
- name='eno3',
- host=hostProfile
- )
- DiskProfile.objects.create(
- size=1000,
- media_type="SSD",
- name='/dev/sda',
- host=hostProfile
- )
- CpuProfile.objects.create(
- cores=96,
- architecture="x86_64",
- cpus=2,
- host=hostProfile
- )
- RamProfile.objects.create(
- amount=256,
- channels=4,
- host=hostProfile
- )
-
- # create GenericResourceBundle
- genericBundle = GenericResourceBundle.objects.create()
-
- gres1 = GenericResource.objects.create(
- bundle=genericBundle,
- name='generic resource ' + str(self.count)
- )
- self.count += 1
- gHost1 = GenericHost.objects.create(
- resource=gres1,
- profile=hostProfile
- )
-
- gres2 = GenericResource.objects.create(
- bundle=genericBundle,
- name='generic resource ' + str(self.count)
- )
- self.count += 1
- gHost2 = GenericHost.objects.create(
- resource=gres2,
- profile=hostProfile
- )
- user1 = User.objects.create(username='user1')
-
- add_booking_perm = Permission.objects.get(codename='add_booking')
- user1.user_permissions.add(add_booking_perm)
-
- user1 = User.objects.get(pk=user1.id)
-
- conf = ConfigBundle.objects.create(owner=user1, name="test conf")
- self.makeHostConfigurations([gHost1, gHost2], conf)
-
- # actual resource bundle
- bundle = ResourceBundle.objects.create(
- template=genericBundle
- )
-
- host1 = Host.objects.create(
- template=gHost1,
- booked=True,
- name='host1',
- bundle=bundle,
- profile=hostProfile,
- lab=lab
- )
-
- host2 = Host.objects.create(
- template=gHost2,
- booked=True,
- name='host2',
- bundle=bundle,
- profile=hostProfile,
- lab=lab
- )
-
- vlan1 = Vlan.objects.create(vlan_id=300, tagged=False)
- vlan2 = Vlan.objects.create(vlan_id=300, tagged=False)
-
- iface1 = Interface.objects.create(
- mac_address='00:11:22:33:44:55',
- bus_address='some bus address',
- switch_name='switch1',
- port_name='port10',
- host=host1
- )
-
- iface1.config = [vlan1]
-
- iface2 = Interface.objects.create(
- mac_address='00:11:22:33:44:56',
- bus_address='some bus address',
- switch_name='switch1',
- port_name='port12',
- host=host2
- )
-
- iface2.config = [vlan2]
-
- # finally, can create booking
- self.booking = Booking.objects.create(
- owner=user1,
- start=timezone.now(),
- end=timezone.now() + timedelta(weeks=1),
- purpose='Testing',
- resource=bundle,
- config_bundle=conf
- )
-
- serialized_booking = {}
-
- host1 = {}
- host1['hostname'] = 'host1'
- host1['image'] = {} # TODO: Images
- host1['deploy_image'] = True
- host2 = {}
- host2['hostname'] = 'host2'
- host2['image'] = {} # TODO: Images
- host2['deploy_image'] = True
-
- serialized_booking['hosts'] = [host1, host2]
-
- net = {}
- net['name'] = 'network_name'
- net['vlan_id'] = 300
- netHost1 = {}
- netHost1['hostname'] = 'host1'
- netHost1['tagged'] = False
- netHost1['interface'] = 0
- netHost2 = {}
- netHost2['hostname'] = 'host2'
- netHost2['tagged'] = False
- netHost2['interface'] = 0
- net['hosts'] = [netHost1, netHost2]
-
- serialized_booking['networking'] = [net]
- serialized_booking['jumphost'] = 'host1'
-
- self.serialized_booking = serialized_booking
-
- def test_to_representation(self):
- keys = ['hosts', 'networking', 'jumphost']
- serialized_form = self.serializer.to_representation(self.booking)
- for key in keys:
- self.assertEquals(serialized_form[key], self.serialized_booking)
diff --git a/dashboard/src/api/urls.py b/dashboard/src/api/urls.py
index 50cc6ac..7a48425 100644
--- a/dashboard/src/api/urls.py
+++ b/dashboard/src/api/urls.py
@@ -39,6 +39,10 @@ from api.views import (
new_jobs,
current_jobs,
done_jobs,
+ update_host_bmc,
+ lab_host,
+ get_pdf,
+ get_idf,
GenerateTokenView
)
@@ -51,6 +55,10 @@ urlpatterns = [
path('labs/<slug:lab_name>/profile', lab_profile),
path('labs/<slug:lab_name>/status', lab_status),
path('labs/<slug:lab_name>/inventory', lab_inventory),
+ path('labs/<slug:lab_name>/hosts/<slug:host_id>', lab_host),
+ path('labs/<slug:lab_name>/hosts/<slug:host_id>/bmc', update_host_bmc),
+ path('labs/<slug:lab_name>/booking/<int:booking_id>/pdf', get_pdf, name="get-pdf"),
+ path('labs/<slug:lab_name>/booking/<int:booking_id>/idf', get_idf, name="get-idf"),
path('labs/<slug:lab_name>/jobs/<int:job_id>', specific_job),
path('labs/<slug:lab_name>/jobs/<int:job_id>/<slug:task_id>', specific_task),
path('labs/<slug:lab_name>/jobs/new', new_jobs),
diff --git a/dashboard/src/api/views.py b/dashboard/src/api/views.py
index c72c85c..fb28958 100644
--- a/dashboard/src/api/views.py
+++ b/dashboard/src/api/views.py
@@ -13,7 +13,7 @@ from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.views import View
-from django.http.response import JsonResponse
+from django.http.response import JsonResponse, HttpResponse
from rest_framework import viewsets
from rest_framework.authtoken.models import Token
from django.views.decorators.csrf import csrf_exempt
@@ -54,6 +54,28 @@ def lab_inventory(request, lab_name=""):
return JsonResponse(lab_manager.get_inventory(), safe=False)
+@csrf_exempt
+def lab_host(request, lab_name="", host_id=""):
+ lab_token = request.META.get('HTTP_AUTH_TOKEN')
+ lab_manager = LabManagerTracker.get(lab_name, lab_token)
+ if request.method == "GET":
+ return JsonResponse(lab_manager.get_host(host_id), safe=False)
+ if request.method == "POST":
+ return JsonResponse(lab_manager.update_host(host_id, request.POST), safe=False)
+
+
+def get_pdf(request, lab_name="", booking_id=""):
+ lab_token = request.META.get('HTTP_AUTH_TOKEN')
+ lab_manager = LabManagerTracker.get(lab_name, lab_token)
+ return HttpResponse(lab_manager.get_pdf(booking_id), content_type="text/plain")
+
+
+def get_idf(request, lab_name="", booking_id=""):
+ lab_token = request.META.get('HTTP_AUTH_TOKEN')
+ lab_manager = LabManagerTracker.get(lab_name, lab_token)
+ return HttpResponse(lab_manager.get_idf(booking_id), content_type="text/plain")
+
+
def lab_status(request, lab_name=""):
lab_token = request.META.get('HTTP_AUTH_TOKEN')
lab_manager = LabManagerTracker.get(lab_name, lab_token)
@@ -62,6 +84,18 @@ def lab_status(request, lab_name=""):
return JsonResponse(lab_manager.get_status(), safe=False)
+@csrf_exempt
+def update_host_bmc(request, lab_name="", host_id=""):
+ lab_token = request.META.get('HTTP_AUTH_TOKEN')
+ lab_manager = LabManagerTracker.get(lab_name, lab_token)
+ if request.method == "POST":
+ # update / create RemoteInfo for host
+ return JsonResponse(
+ lab_manager.update_host_remote_info(request.POST, host_id),
+ safe=False
+ )
+
+
def lab_profile(request, lab_name=""):
lab_token = request.META.get('HTTP_AUTH_TOKEN')
lab_manager = LabManagerTracker.get(lab_name, lab_token)
@@ -79,6 +113,8 @@ def specific_task(request, lab_name="", job_id="", task_id=""):
task.status = request.POST.get('status')
if 'message' in request.POST:
task.message = request.POST.get('message')
+ if 'lab_token' in request.POST:
+ task.lab_token = request.POST.get('lab_token')
task.save()
NotificationHandler.task_updated(task)
d = {}
@@ -93,6 +129,7 @@ def specific_task(request, lab_name="", job_id="", task_id=""):
return JsonResponse(get_task(task_id).config.get_delta())
+@csrf_exempt
def specific_job(request, lab_name="", job_id=""):
lab_token = request.META.get('HTTP_AUTH_TOKEN')
lab_manager = LabManagerTracker.get(lab_name, lab_token)
diff --git a/dashboard/src/booking/forms.py b/dashboard/src/booking/forms.py
index 7ba5af0..df88cc6 100644
--- a/dashboard/src/booking/forms.py
+++ b/dashboard/src/booking/forms.py
@@ -8,55 +8,47 @@
##############################################################################
import django.forms as forms
from django.forms.widgets import NumberInput
-from django.db.models import Q
from workflow.forms import (
- SearchableSelectMultipleWidget,
MultipleSelectFilterField,
MultipleSelectFilterWidget,
FormUtils)
from account.models import UserProfile
from resource_inventory.models import Image, Installer, Scenario
+from workflow.forms import SearchableSelectMultipleField
+from booking.lib import get_user_items, get_user_field_opts
class QuickBookingForm(forms.Form):
purpose = forms.CharField(max_length=1000)
project = forms.CharField(max_length=400)
- image = forms.ModelChoiceField(queryset=Image.objects.all())
hostname = forms.CharField(max_length=400)
installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False)
scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False)
def __init__(self, data=None, user=None, *args, **kwargs):
- chosen_users = []
if "default_user" in kwargs:
default_user = kwargs.pop("default_user")
else:
default_user = "you"
self.default_user = default_user
- if "chosen_users" in kwargs:
- chosen_users = kwargs.pop("chosen_users")
- elif data and "users" in data:
- chosen_users = data.getlist("users")
-
- if user:
- self.image = forms.ModelChoiceField(queryset=Image.objects.filter(
- Q(public=True) | Q(owner=user)), required=False)
- else:
- self.image = forms.ModelChoiceField(queryset=Image.objects.all(), required=False)
super(QuickBookingForm, self).__init__(data=data, **kwargs)
- self.fields['users'] = forms.CharField(
- widget=SearchableSelectMultipleWidget(
- attrs=self.build_search_widget_attrs(chosen_users, default_user=default_user)
- ),
- required=False
+ self.fields["image"] = forms.ModelChoiceField(
+ Image.objects.filter(public=True) | Image.objects.filter(owner=user)
+ )
+
+ self.fields['users'] = SearchableSelectMultipleField(
+ queryset=UserProfile.objects.select_related('user').exclude(user=user),
+ items=get_user_items(exclude=user),
+ required=False,
+ **get_user_field_opts()
)
+
attrs = FormUtils.getLabData(0)
- attrs['selection_data'] = 'false'
- self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(attrs=attrs))
+ self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**attrs))
self.fields['length'] = forms.IntegerField(
widget=NumberInput(
attrs={
diff --git a/dashboard/src/booking/lib.py b/dashboard/src/booking/lib.py
new file mode 100644
index 0000000..8132c75
--- /dev/null
+++ b/dashboard/src/booking/lib.py
@@ -0,0 +1,36 @@
+##############################################################################
+# Copyright (c) 2019 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 account.models import UserProfile
+
+
+def get_user_field_opts():
+ return {
+ 'show_from_noentry': False,
+ 'show_x_results': 5,
+ 'results_scrollable': True,
+ 'selectable_limit': -1,
+ 'placeholder': 'Search for other users',
+ 'name': 'users',
+ 'disabled': False
+ }
+
+
+def get_user_items(exclude=None):
+ qs = UserProfile.objects.select_related('user').exclude(user=exclude)
+ items = {}
+ for up in qs:
+ item = {
+ 'id': up.id,
+ 'expanded_name': up.full_name,
+ 'small_name': up.user.username,
+ 'string': up.email_addr
+ }
+ items[up.id] = item
+ return items
diff --git a/dashboard/src/booking/migrations/0005_booking_idf.py b/dashboard/src/booking/migrations/0005_booking_idf.py
new file mode 100644
index 0000000..31e9170
--- /dev/null
+++ b/dashboard/src/booking/migrations/0005_booking_idf.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2019-04-12 19:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0004_auto_20190124_1700'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='booking',
+ name='idf',
+ field=models.TextField(blank=True, default=''),
+ ),
+ ]
diff --git a/dashboard/src/booking/migrations/0006_booking_opnfv_config.py b/dashboard/src/booking/migrations/0006_booking_opnfv_config.py
new file mode 100644
index 0000000..e5ffc71
--- /dev/null
+++ b/dashboard/src/booking/migrations/0006_booking_opnfv_config.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1 on 2019-05-01 18:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0010_auto_20190430_1405'),
+ ('booking', '0005_booking_idf'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='booking',
+ name='opnfv_config',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.OPNFVConfig'),
+ ),
+ ]
diff --git a/dashboard/src/booking/models.py b/dashboard/src/booking/models.py
index 8612abd..9836730 100644
--- a/dashboard/src/booking/models.py
+++ b/dashboard/src/booking/models.py
@@ -9,7 +9,7 @@
##############################################################################
-from resource_inventory.models import ResourceBundle, ConfigBundle
+from resource_inventory.models import ResourceBundle, ConfigBundle, OPNFVConfig
from account.models import Lab
from django.contrib.auth.models import User
from django.db import models
@@ -29,9 +29,11 @@ class Booking(models.Model):
ext_count = models.IntegerField(default=2)
resource = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True)
config_bundle = models.ForeignKey(ConfigBundle, on_delete=models.SET_NULL, null=True)
+ opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.SET_NULL, null=True)
project = models.CharField(max_length=100, default="", blank=True, null=True)
lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL)
pdf = models.TextField(blank=True, default="")
+ idf = models.TextField(blank=True, default="")
class Meta:
db_table = 'booking'
diff --git a/dashboard/src/booking/quick_deployer.py b/dashboard/src/booking/quick_deployer.py
index c431017..0e0cc5a 100644
--- a/dashboard/src/booking/quick_deployer.py
+++ b/dashboard/src/booking/quick_deployer.py
@@ -12,7 +12,6 @@ import json
import uuid
import re
from django.db.models import Q
-from django.contrib.auth.models import User
from datetime import timedelta
from django.utils import timezone
from account.models import Lab
@@ -22,7 +21,6 @@ from resource_inventory.models import (
Image,
GenericResourceBundle,
ConfigBundle,
- Vlan,
Host,
HostProfile,
HostConfiguration,
@@ -30,14 +28,21 @@ from resource_inventory.models import (
GenericHost,
GenericInterface,
OPNFVRole,
- OPNFVConfig
+ OPNFVConfig,
+ Network,
+ NetworkConnection,
+ NetworkRole,
+ HostOPNFVConfig,
)
from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
+from notifier.manager import NotificationHandler
from booking.models import Booking
from dashboard.exceptions import (
InvalidHostnameException,
ResourceAvailabilityException,
- ModelValidationException
+ ModelValidationException,
+ BookingLengthException
)
from api.models import JobFactory
@@ -87,164 +92,241 @@ class NoRemainingPublicNetwork(Exception):
pass
-def create_from_form(form, request):
- quick_booking_id = str(uuid.uuid4())
+class BookingPermissionException(Exception):
+ pass
- host_field = form.cleaned_data['filter_field']
- host_json = json.loads(host_field)
- purpose_field = form.cleaned_data['purpose']
- project_field = form.cleaned_data['project']
- users_field = form.cleaned_data['users']
- host_name = form.cleaned_data['hostname']
- length = form.cleaned_data['length']
- image = form.cleaned_data['image']
- scenario = form.cleaned_data['scenario']
- installer = form.cleaned_data['installer']
+def parse_host_field(host_json):
+ lab, profile = (None, None)
+ lab_dict = host_json['lab']
+ for lab_info in lab_dict.values():
+ if lab_info['selected']:
+ lab = Lab.objects.get(lab_user__id=lab_info['id'])
- # get all initial info we need to validate
- lab_dict = host_json['labs'][0]
- lab_id = list(lab_dict.keys())[0]
- lab_user_id = int(lab_id.split("_")[-1])
- lab = Lab.objects.get(lab_user__id=lab_user_id)
-
- host_dict = host_json['hosts'][0]
- profile_id = list(host_dict.keys())[0]
- profile_id = int(profile_id.split("_")[-1])
- profile = HostProfile.objects.get(id=profile_id)
-
- # check validity of field data before trying to apply to models
- if not lab:
- raise LabDNE("Lab with provided ID does not exist")
- if not profile:
- raise HostProfileDNE("Host type with provided ID does not exist")
-
- # check that hostname is valid
- if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", host_name):
- raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
- # check that image os is compatible with installer
- if installer in image.os.sup_installers.all():
- #if installer not here, we can omit that and not check for scenario
- if not scenario:
- raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
- if scenario not in installer.sup_scenarios.all():
- raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
- if image.from_lab != lab:
- raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
- if image.host_type != profile:
- raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
- if not image.public and image.owner != request.user:
- raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
+ host_dict = host_json['host']
+ for host_info in host_dict.values():
+ if host_info['selected']:
+ profile = HostProfile.objects.get(pk=host_info['id'])
- # check if host type is available
- #ResourceManager.getInstance().acquireHost(ghost, lab.name)
+ if lab is None:
+ raise NoLabSelectedError("No lab was selected")
+ if profile is None:
+ raise HostProfileDNE("No Host was selected")
+
+ return lab, profile
+
+
+def check_available_matching_host(lab, hostprofile):
available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
- if not profile in available_host_types:
+ if hostprofile not in available_host_types:
# TODO: handle deleting generic resource in this instance along with grb
- raise HostNotAvailable("Could not book selected host due to changed availability. Try again later")
+ raise HostNotAvailable('Requested host type is not available. Please try again later. Host availability can be viewed in the "Hosts" tab to the left.')
- # check if any hosts with profile at lab are still available
- hostset = Host.objects.filter(lab=lab, profile=profile).filter(booked=False).filter(working=True)
- if not hostset.first():
+ hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
+ if not hostset.exists():
raise HostNotAvailable("Couldn't find any matching unbooked hosts")
- # generate GenericResourceBundle
- if len(host_json['labs']) != 1:
- raise NoLabSelectedError("No lab was selected")
+ return True
+
- grbundle = GenericResourceBundle(owner=request.user)
+def generate_grb(owner, lab, common_id):
+ grbundle = GenericResourceBundle(owner=owner)
grbundle.lab = lab
- grbundle.name = "grbundle for quick booking with uid " + quick_booking_id
+ grbundle.name = "grbundle for quick booking with uid " + common_id
grbundle.description = "grbundle created for quick-deploy booking"
grbundle.save()
- # generate GenericResource, GenericHost
- gresource = GenericResource(bundle=grbundle, name=host_name)
+ return grbundle
+
+
+def generate_gresource(bundle, hostname):
+ if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
+ raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
+ gresource = GenericResource(bundle=bundle, name=hostname)
gresource.save()
+ return gresource
+
+
+def generate_ghost(generic_resource, host_profile):
ghost = GenericHost()
- ghost.resource = gresource
- ghost.profile = profile
+ ghost.resource = generic_resource
+ ghost.profile = host_profile
ghost.save()
- # generate config bundle
+ return ghost
+
+
+def generate_config_bundle(owner, common_id, grbundle):
cbundle = ConfigBundle()
- cbundle.owner = request.user
- cbundle.name = "configbundle for quick booking with uid " + quick_booking_id
+ cbundle.owner = owner
+ cbundle.name = "configbundle for quick booking with uid " + common_id
cbundle.description = "configbundle created for quick-deploy booking"
cbundle.bundle = grbundle
cbundle.save()
- # generate OPNFVConfig pointing to cbundle
- if installer:
- opnfvconfig = OPNFVConfig()
- opnfvconfig.scenario = scenario
- opnfvconfig.installer = installer
- opnfvconfig.bundle = cbundle
- opnfvconfig.save()
+ return cbundle
+
+
+def generate_opnfvconfig(scenario, installer, config_bundle):
+ opnfvconfig = OPNFVConfig()
+ opnfvconfig.scenario = scenario
+ opnfvconfig.installer = installer
+ opnfvconfig.bundle = config_bundle
+ opnfvconfig.save()
+
+ return opnfvconfig
+
- # generate HostConfiguration pointing to cbundle
+def generate_hostconfig(generic_host, image, config_bundle):
hconf = HostConfiguration()
- hconf.host = ghost
+ hconf.host = generic_host
hconf.image = image
- hconf.opnfvRole = OPNFVRole.objects.get(name="Jumphost")
- if not hconf.opnfvRole:
- raise OPNFVRoleDNE("No jumphost role was found")
- hconf.bundle = cbundle
+ hconf.bundle = config_bundle
+ hconf.is_head_node = True
hconf.save()
- # construct generic interfaces
- for interface_profile in profile.interfaceprofile.all():
- generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
- generic_interface.save()
- ghost.save()
-
- # get vlan, assign to first interface
- publicnetwork = lab.vlan_manager.get_public_vlan()
- publicvlan = publicnetwork.vlan
- if not publicnetwork:
- raise NoRemainingPublicNetwork("No public networks were available for your pod")
- lab.vlan_manager.reserve_public_vlan(publicvlan)
+ return hconf
- vlan = Vlan.objects.create(vlan_id=publicvlan, tagged=False, public=True)
- vlan.save()
- ghost.generic_interfaces.first().vlans.add(vlan)
- ghost.generic_interfaces.first().save()
- # generate resource bundle
+def generate_hostopnfv(hostconfig, opnfvconfig):
+ config = HostOPNFVConfig()
+ role = None
+ try:
+ role = OPNFVRole.objects.get(name="Jumphost")
+ except Exception:
+ role = OPNFVRole.objects.create(
+ name="Jumphost",
+ description="Single server jumphost role"
+ )
+ config.role = role
+ config.host_config = hostconfig
+ config.opnfv_config = opnfvconfig
+ config.save()
+ return config
+
+
+def generate_resource_bundle(generic_resource_bundle, config_bundle): # warning: requires cleanup
try:
- resource_bundle = ResourceManager.getInstance().convertResourceBundle(grbundle, config=cbundle)
+ resource_manager = ResourceManager.getInstance()
+ resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
+ return resource_bundle
except ResourceAvailabilityException:
raise ResourceAvailabilityException("Requested resources not available")
except ModelValidationException:
raise ModelValidationException("Encountered error while saving grbundle")
+
+def check_invariants(request, **kwargs):
+ installer = kwargs['installer']
+ image = kwargs['image']
+ scenario = kwargs['scenario']
+ lab = kwargs['lab']
+ host_profile = kwargs['host_profile']
+ length = kwargs['length']
+ # check that image os is compatible with installer
+ if installer in image.os.sup_installers.all():
+ # if installer not here, we can omit that and not check for scenario
+ if not scenario:
+ raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
+ if scenario not in installer.sup_scenarios.all():
+ raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
+ if image.from_lab != lab:
+ raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
+ if image.host_type != host_profile:
+ raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
+ if not image.public and image.owner != request.user:
+ raise ImageOwnershipInvalid("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")
+
+
+def configure_networking(grb, config):
+ # create network
+ net = Network.objects.create(name="public", bundle=grb, is_public=True)
+ # connect network to generic host
+ grb.getHosts()[0].generic_interfaces.first().connections.add(
+ NetworkConnection.objects.create(network=net, vlan_is_tagged=False)
+ )
+ # asign network role
+ role = NetworkRole.objects.create(name="public", network=net)
+ opnfv_config = config.opnfv_config.first()
+ if opnfv_config:
+ opnfv_config.networks.add(role)
+
+
+def create_from_form(form, request):
+ quick_booking_id = str(uuid.uuid4())
+
+ host_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 = 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, host_profile = parse_host_field(host_field)
+ data = form.cleaned_data
+ data['lab'] = lab
+ data['host_profile'] = host_profile
+ check_invariants(request, **data)
+
+ # check booking privileges
+ if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
+ raise BookingPermissionException("You do not have permission to have more than 3 bookings at a time.")
+
+ check_available_matching_host(lab, host_profile) # requires cleanup if failure after this point
+
+ grbundle = generate_grb(request.user, lab, quick_booking_id)
+ gresource = generate_gresource(grbundle, hostname)
+ ghost = generate_ghost(gresource, host_profile)
+ cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
+ hconf = generate_hostconfig(ghost, image, cbundle)
+
+ # if no installer provided, just create blank host
+ opnfv_config = None
+ if installer:
+ opnfv_config = generate_opnfvconfig(scenario, installer, cbundle)
+ generate_hostopnfv(hconf, opnfv_config)
+
+ # construct generic interfaces
+ for interface_profile in host_profile.interfaceprofile.all():
+ generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
+ generic_interface.save()
+
+ configure_networking(grbundle, cbundle)
+
+ # generate resource bundle
+ resource_bundle = generate_resource_bundle(grbundle, cbundle)
+
# generate booking
- booking = Booking()
- booking.purpose = purpose_field
- booking.project = project_field
- booking.lab = lab
- booking.owner = request.user
- booking.start = timezone.now()
- booking.end = timezone.now() + timedelta(days=int(length))
- booking.resource = resource_bundle
- booking.pdf = ResourceManager().makePDF(booking.resource)
- booking.config_bundle = cbundle
+ booking = Booking.objects.create(
+ purpose=purpose_field,
+ project=project_field,
+ lab=lab,
+ owner=request.user,
+ start=timezone.now(),
+ end=timezone.now() + timedelta(days=int(length)),
+ resource=resource_bundle,
+ config_bundle=cbundle,
+ opnfv_config=opnfv_config
+ )
+ booking.pdf = PDFTemplater.makePDF(booking)
+
+ for collaborator in users_field: # list of UserProfiles
+ booking.collaborators.add(collaborator.user)
+
booking.save()
- print("users field:")
- print(users_field)
- print(type(users_field))
- #users_field = json.loads(users_field)
- users_field = users_field[2:-2]
- if users_field: #may be empty after split, if no collaborators entered
- users_field = json.loads(users_field)
- for collaborator in users_field:
- user = User.objects.get(id=collaborator['id'])
- booking.collaborators.add(user)
- booking.save()
# generate job
JobFactory.makeCompleteJob(booking)
+ NotificationHandler.notify_new_booking(booking)
+
+ return booking
def drop_filter(user):
diff --git a/dashboard/src/booking/stats.py b/dashboard/src/booking/stats.py
index b706577..383723a 100644
--- a/dashboard/src/booking/stats.py
+++ b/dashboard/src/booking/stats.py
@@ -25,34 +25,34 @@ class StatisticsManager(object):
some point in the given date span is the number of days to plot.
The last x value will always be the current time
"""
- x_set = set()
+ data = []
x = []
y = []
users = []
now = datetime.datetime.now(pytz.utc)
delta = datetime.timedelta(days=span)
end = now - delta
- bookings = Booking.objects.filter(start__lte=now, end__gte=end)
- for booking in bookings:
- x_set.add(booking.start)
- if booking.end < now:
- x_set.add(booking.end)
+ bookings = Booking.objects.filter(start__lte=now, end__gte=end).prefetch_related("collaborators")
+ for booking in bookings: # collect data from each booking
+ user_list = [u.pk for u in booking.collaborators.all()]
+ user_list.append(booking.owner.pk)
+ data.append((booking.start, 1, user_list))
+ data.append((booking.end, -1, user_list))
- x_set.add(now)
- x_set.add(end)
+ # sort based on time
+ data.sort(key=lambda i: i[0])
- x_list = list(x_set)
- x_list.sort(reverse=True)
- for time in x_list:
- x.append(str(time))
- active = Booking.objects.filter(start__lte=time, end__gt=time)
- booking_count = len(active)
- users_set = set()
- for booking in active:
- users_set.add(booking.owner)
- for user in booking.collaborators.all():
- users_set.add(user)
- y.append(booking_count)
- users.append(len(users_set))
+ # collect data
+ count = 0
+ active_users = {}
+ for datum in data:
+ x.append(str(datum[0])) # time
+ count += datum[1] # booking count
+ y.append(count)
+ for pk in datum[2]: # maintain count of each user's active bookings
+ active_users[pk] = active_users.setdefault(pk, 0) + datum[1]
+ if active_users[pk] == 0:
+ del active_users[pk]
+ users.append(len([x for x in active_users.values() if x > 0]))
return {"booking": [x, y], "user": [x, users]}
diff --git a/dashboard/src/booking/tests/test_models.py b/dashboard/src/booking/tests/test_models.py
index c7fb25d..6170295 100644
--- a/dashboard/src/booking/tests/test_models.py
+++ b/dashboard/src/booking/tests/test_models.py
@@ -230,10 +230,3 @@ class BookingModelTestCase(TestCase):
booking.save()
except Exception:
self.fail("save() threw an exception")
- booking.end = booking.end + timedelta(weeks=2)
- self.assertRaises(ValueError, booking.save)
- booking.end = booking.end - timedelta(days=8)
- try:
- self.assertTrue(booking.save())
- except Exception:
- self.fail("save() threw an exception")
diff --git a/dashboard/src/booking/tests/test_quick_booking.py b/dashboard/src/booking/tests/test_quick_booking.py
new file mode 100644
index 0000000..e445860
--- /dev/null
+++ b/dashboard/src/booking/tests/test_quick_booking.py
@@ -0,0 +1,150 @@
+##############################################################################
+# 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
+##############################################################################
+
+import datetime
+
+from django.test import TestCase, Client
+
+from booking.models import Booking
+from dashboard.testing_utils import (
+ make_host,
+ make_user,
+ make_user_profile,
+ make_lab,
+ make_installer,
+ make_image,
+ make_scenario,
+ make_os,
+ make_complete_host_profile,
+ make_opnfv_role,
+ make_public_net,
+)
+
+
+class QuickBookingValidFormTestCase(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.user = make_user(False, username="newtestuser", password="testpassword")
+ make_user_profile(cls.user, True)
+
+ lab_user = make_user(True)
+ cls.lab = make_lab(lab_user)
+
+ cls.host_profile = make_complete_host_profile(cls.lab)
+ cls.scenario = make_scenario()
+ cls.installer = make_installer([cls.scenario])
+ os = make_os([cls.installer])
+ cls.image = make_image(cls.lab, 1, cls.user, os, cls.host_profile)
+ cls.host = make_host(cls.host_profile, cls.lab)
+ cls.role = make_opnfv_role()
+ cls.pubnet = make_public_net(10, cls.lab)
+
+ cls.post_data = cls.build_post_data()
+ cls.client = Client()
+
+ @classmethod
+ def build_post_data(cls):
+ return {
+ 'filter_field': '{"hosts":[{"host_' + str(cls.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(cls.lab.lab_user.id) + '":"true"}]}',
+ 'purpose': 'my_purpose',
+ 'project': 'my_project',
+ 'length': '3',
+ 'ignore_this': 1,
+ 'users': '',
+ 'hostname': 'my_host',
+ 'image': str(cls.image.id),
+ 'installer': str(cls.installer.id),
+ 'scenario': str(cls.scenario.id)
+ }
+
+ def post(self, changed_fields={}):
+ payload = self.post_data.copy()
+ payload.update(changed_fields)
+ response = self.client.post('/booking/quick/', payload)
+ return response
+
+ def setUp(self):
+ self.client.login(username=self.user.username, password="testpassword")
+
+ def assertValidBooking(self, booking):
+ self.assertEqual(booking.owner, self.user)
+ self.assertEqual(booking.purpose, 'my_purpose')
+ self.assertEqual(booking.project, 'my_project')
+ delta = booking.end - booking.start
+ delta -= datetime.timedelta(days=3)
+ self.assertLess(delta, datetime.timedelta(minutes=1))
+
+ resource_bundle = booking.resource
+ config_bundle = booking.config_bundle
+
+ opnfv_config = config_bundle.opnfv_config.first()
+ self.assertEqual(self.installer, opnfv_config.installer)
+ self.assertEqual(self.scenario, opnfv_config.scenario)
+
+ host = resource_bundle.hosts.first()
+ self.assertEqual(host.profile, self.host_profile)
+ self.assertEqual(host.template.resource.name, 'my_host')
+
+ def test_with_too_long_length(self):
+ response = self.post({'length': '22'})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_negative_length(self):
+ response = self.post({'length': '-1'})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_installer(self):
+ response = self.post({'installer': str(self.installer.id + 100)})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_scenario(self):
+ response = self.post({'scenario': str(self.scenario.id + 100)})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_host_id(self):
+ response = self.post({'filter_field': '{"hosts":[{"host_' + str(self.host_profile.id + 100) + '":"true"}], "labs": [{"lab_' + str(self.lab.lab_user.id) + '":"true"}]}'})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_lab_id(self):
+ response = self.post({'filter_field': '{"hosts":[{"host_' + str(self.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(self.lab.lab_user.id + 100) + '":"true"}]}'})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_invalid_empty_filter_field(self):
+ response = self.post({'filter_field': ''})
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNone(Booking.objects.first())
+
+ def test_with_garbage_users_field(self): # expected behavior: treat as though field is empty if it has garbage data
+ response = self.post({'users': 'X�]QP�槰DP�+m���h�U�_�yJA:.rDi��QN|.��C��n�P��F!��D�����5ȅj�9�LV��'}) # output from /dev/urandom
+
+ self.assertEqual(response.status_code, 200)
+ booking = Booking.objects.first()
+ self.assertIsNotNone(booking)
+ self.assertValidBooking(booking)
+
+ def test_with_valid_form(self):
+ response = self.post()
+
+ self.assertEqual(response.status_code, 200)
+ booking = Booking.objects.first()
+ self.assertIsNotNone(booking)
+ self.assertValidBooking(booking)
diff --git a/dashboard/src/booking/views.py b/dashboard/src/booking/views.py
index 1e14b8e..bad7dc9 100644
--- a/dashboard/src/booking/views.py
+++ b/dashboard/src/booking/views.py
@@ -16,6 +16,7 @@ from django.views import View
from django.views.generic import TemplateView
from django.shortcuts import redirect, render
from django.db.models import Q
+from django.urls import reverse
from resource_inventory.models import ResourceBundle, HostProfile, Image, Host
from resource_inventory.resource_manager import ResourceManager
@@ -47,7 +48,7 @@ def quick_create(request):
context['lab_profile_map'] = profiles
- context['form'] = QuickBookingForm(initial={}, chosen_users=[], default_user=request.user.username, user=request.user)
+ context['form'] = QuickBookingForm(default_user=request.user.username, user=request.user)
context.update(drop_filter(request.user))
@@ -60,15 +61,13 @@ def quick_create(request):
if form.is_valid():
try:
- create_from_form(form, request)
+ booking = create_from_form(form, request)
+ messages.success(request, "We've processed your request. "
+ "Check Account->My Bookings for the status of your new booking")
+ return redirect(reverse('booking:booking_detail', kwargs={'booking_id': booking.id}))
except Exception as e:
- messages.error(request, "Whoops, looks like an error occurred. "
- "Let the admins know that you got the following message: " + str(e))
- return render(request, 'workflow/exit_redirect.html', context)
-
- messages.success(request, "We've processed your request. "
- "Check Account->My Bookings for the status of your new booking")
- return render(request, 'workflow/exit_redirect.html', context)
+ messages.error(request, "Whoops, an error occurred: " + str(e))
+ return render(request, 'booking/quick_deploy.html', context)
else:
messages.error(request, "Looks like the form didn't validate. Check that you entered everything correctly")
return render(request, 'booking/quick_deploy.html', context)
diff --git a/dashboard/src/dashboard/actions.py b/dashboard/src/dashboard/actions.py
new file mode 100644
index 0000000..44b1fdd
--- /dev/null
+++ b/dashboard/src/dashboard/actions.py
@@ -0,0 +1,47 @@
+##############################################################################
+# Copyright (c) 2019 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 resource_inventory.models import Host, Vlan
+from account.models import Lab
+from booking.models import Booking
+from datetime import timedelta
+from django.utils import timezone
+
+
+def free_leaked_hosts(free_old_bookings=False, old_booking_age=timedelta(days=1)):
+ bundles = [booking.resource for booking in Booking.objects.filter(end__gt=timezone.now())]
+ active_hosts = set()
+ for bundle in bundles:
+ active_hosts.update([host for host in bundle.hosts.all()])
+
+ marked_hosts = set(Host.objects.filter(booked=True))
+
+ for host in (marked_hosts - active_hosts):
+ host.booked = False
+ host.save()
+
+
+def free_leaked_public_vlans():
+ booked_host_interfaces = []
+
+ for lab in Lab.objects.all():
+
+ for host in Host.objects.filter(booked=True).filter(lab=lab):
+ for interface in host.interfaces.all():
+ booked_host_interfaces.append(interface)
+
+ in_use_vlans = Vlan.objects.filter(public=True).distinct('vlan_id').filter(interface__in=booked_host_interfaces)
+
+ manager = lab.vlan_manager
+
+ for vlan in Vlan.objects.all():
+ if vlan not in in_use_vlans:
+ if vlan.public:
+ manager.release_public_vlan(vlan.vlan_id)
+ manager.release_vlans(vlan)
diff --git a/dashboard/src/dashboard/exceptions.py b/dashboard/src/dashboard/exceptions.py
index 9c16a06..7111bf8 100644
--- a/dashboard/src/dashboard/exceptions.py
+++ b/dashboard/src/dashboard/exceptions.py
@@ -50,3 +50,7 @@ class InvalidVlanConfigurationException(Exception):
class NetworkExistsException(Exception):
pass
+
+
+class BookingLengthException(Exception):
+ pass
diff --git a/dashboard/src/dashboard/populate_db_iol.py b/dashboard/src/dashboard/populate_db_iol.py
index 4368520..916dd97 100644
--- a/dashboard/src/dashboard/populate_db_iol.py
+++ b/dashboard/src/dashboard/populate_db_iol.py
@@ -307,7 +307,7 @@ class Populator:
size = 0
try:
size = int(disk_data['size'].split('.')[0])
- except:
+ except Exception:
size = int(disk_data['size'].split('.')[0][:-1])
DiskProfile.objects.create(
size=size,
diff --git a/dashboard/src/dashboard/tasks.py b/dashboard/src/dashboard/tasks.py
index 837e7a1..597629f 100644
--- a/dashboard/src/dashboard/tasks.py
+++ b/dashboard/src/dashboard/tasks.py
@@ -11,7 +11,6 @@
from celery import shared_task
from django.utils import timezone
-from django.db.models import Q
from booking.models import Booking
from notifier.manager import NotificationHandler
from api.models import Job, JobStatus, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, AccessRelation
@@ -41,7 +40,7 @@ def booking_poll():
if vlan.public:
try:
host.lab.vlan_manager.release_public_vlan(vlan.vlan_id)
- except: # will fail if we already released in this loop
+ except Exception: # will fail if we already released in this loop
pass
else:
vlans.append(vlan.vlan_id)
diff --git a/dashboard/src/dashboard/testing_utils.py b/dashboard/src/dashboard/testing_utils.py
new file mode 100644
index 0000000..a96b6d0
--- /dev/null
+++ b/dashboard/src/dashboard/testing_utils.py
@@ -0,0 +1,396 @@
+##############################################################################
+# 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 django.contrib.auth.models import User
+from django.core.files.base import ContentFile
+from django.utils import timezone
+
+import json
+import re
+from datetime import timedelta
+
+from dashboard.exceptions import InvalidHostnameException
+from booking.models import Booking
+from account.models import UserProfile, Lab, LabStatus, VlanManager, PublicNetwork
+from resource_inventory.models import (
+ Host,
+ HostProfile,
+ InterfaceProfile,
+ DiskProfile,
+ CpuProfile,
+ Opsys,
+ Image,
+ Scenario,
+ Installer,
+ OPNFVRole,
+ RamProfile,
+ Network,
+ GenericResourceBundle,
+ GenericResource,
+ GenericHost,
+ ConfigBundle,
+ GenericInterface,
+ HostConfiguration,
+ OPNFVConfig,
+ NetworkConnection,
+ HostOPNFVConfig
+)
+from resource_inventory.resource_manager import ResourceManager
+
+"""
+Info for make_booking() function:
+[topology] argument structure:
+ the [topology] argument should describe the structure of the pod
+ the top level should be a dictionary, with each key being a hostname
+ each value in the top level should be a dictionary with two keys:
+ "type" should map to a host profile instance
+ "nets" should map to a list of interfaces each with a list of
+ dictionaries each defining a network in the format
+ { "name": "netname", "tagged": True|False, "public": True|False }
+ each network is defined if a matching name is not found
+
+ sample argument structure:
+ topology={
+ "host1": {
+ "type": instanceOf HostProfile,
+ "role": instanceOf OPNFVRole
+ "image": instanceOf Image
+ "nets": [
+ 0: [
+ 0: { "name": "public", "tagged": True, "public": True },
+ 1: { "name": "private", "tagged": False, "public": False },
+ ]
+ 1: []
+ ]
+ }
+ }
+"""
+
+
+def make_booking(owner=None, start=timezone.now(),
+ end=timezone.now() + timedelta(days=1),
+ lab=None, purpose="my_purpose",
+ project="my_project", collaborators=[],
+ topology={}, installer=None, scenario=None):
+
+ grb, host_set = make_grb(topology, owner, lab)
+ config_bundle, opnfv_bundle = make_config_bundle(grb, owner, topology, host_set, installer, scenario)
+ resource = ResourceManager.getInstance().convertResourceBundle(grb, config=config_bundle)
+ if not resource:
+ raise Exception("Resource not created")
+
+ return Booking.objects.create(
+ resource=resource,
+ config_bundle=config_bundle,
+ start=start,
+ end=end,
+ owner=owner,
+ purpose=purpose,
+ project=project,
+ lab=lab,
+ opnfv_config=opnfv_bundle
+ )
+
+
+def make_config_bundle(grb, owner, topology={}, host_set={},
+ installer=None, scenario=None):
+ cb = ConfigBundle.objects.create(
+ owner=owner,
+ name="config bundle " + str(ConfigBundle.objects.count()),
+ description="cb generated by make_config_bundle() method"
+ )
+
+ opnfv_config = OPNFVConfig.objects.create(
+ installer=installer,
+ scenario=scenario,
+ bundle=cb
+ )
+
+ # generate host configurations based on topology and host set
+ for hostname, host_info in topology.items():
+ host_config = HostConfiguration.objects.create(
+ host=host_set[hostname],
+ image=host_info["image"],
+ bundle=cb,
+ is_head_node=host_info['role'].name.lower() == "jumphost"
+ )
+ HostOPNFVConfig.objects.create(
+ role=host_info["role"],
+ host_config=host_config,
+ opnfv_config=opnfv_config
+ )
+ return cb, opnfv_config
+
+
+def make_network(name, lab, grb, public):
+ network = Network(name=name, bundle=grb, is_public=public)
+ if public:
+ public_net = lab.vlan_manager.get_public_vlan()
+ if not public_net:
+ raise Exception("No more public networks available")
+ lab.vlan_manager.reserve_public_vlan(public_net.vlan)
+ network.vlan_id = public_net.vlan
+ else:
+ private_net = lab.vlan_manager.get_vlan()
+ if not private_net:
+ raise Exception("No more generic vlans are available")
+ lab.vlan_manager.reserve_vlans([private_net])
+ network.vlan_id = private_net
+
+ network.save()
+ return network
+
+
+def make_grb(topology, owner, lab):
+
+ grb = GenericResourceBundle.objects.create(
+ owner=owner,
+ lab=lab,
+ name="Generic ResourceBundle " + str(GenericResourceBundle.objects.count()),
+ description="grb generated by make_grb() method"
+ )
+
+ networks = {}
+ host_set = {}
+
+ for hostname, info in topology.items():
+ host_profile = info["type"]
+
+ # need to construct host from hostname and type
+ generic_host = make_generic_host(grb, host_profile, hostname)
+ host_set[hostname] = generic_host
+
+ # set up networks
+ nets = info["nets"]
+ for interface_index, interface_profile in enumerate(host_profile.interfaceprofile.all()):
+ generic_interface = GenericInterface.objects.create(host=generic_host, profile=interface_profile)
+ netconfig = nets[interface_index]
+ for network_info in netconfig:
+ network_name = network_info["name"]
+ if network_name not in networks:
+ networks[network_name] = make_network(network_name, lab, grb, network_info['public'])
+
+ generic_interface.connections.add(NetworkConnection.objects.create(
+ network=networks[network_name],
+ vlan_is_tagged=network_info["tagged"]
+ ))
+
+ return grb, host_set
+
+
+def make_generic_host(grb, host_profile, hostname):
+ if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
+ raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions")
+ gresource = GenericResource.objects.create(bundle=grb, name=hostname)
+
+ return GenericHost.objects.create(resource=gresource, profile=host_profile)
+
+
+def make_user(is_superuser=False, username="testuser",
+ password="testpassword", email="default_email@user.com"):
+ user = User.objects.create_user(username=username, email=email, password=password)
+ user.is_superuser = is_superuser
+ user.save()
+
+ return user
+
+
+def make_user_profile(user=None, email_addr="email@email.com",
+ company="company", full_name="John Doe",
+ booking_privledge=True, ssh_file=None):
+ user = user or User.objects.first() or make_user()
+ profile = UserProfile.objects.create(
+ email_addr=email_addr,
+ company=company,
+ full_name=full_name,
+ booking_privledge=booking_privledge,
+ user=user
+ )
+ profile.ssh_public_key.save("user_ssh_key", ssh_file if ssh_file else ContentFile("public key content string"))
+
+ return profile
+
+
+def make_vlan_manager(vlans=None, block_size=20, allow_overlapping=False, reserved_vlans=None):
+ if not vlans:
+ vlans = [vlan % 2 for vlan in range(4095)]
+ if not reserved_vlans:
+ reserved_vlans = [0 for i in range(4095)]
+
+ return VlanManager.objects.create(
+ vlans=json.dumps(vlans),
+ reserved_vlans=json.dumps(vlans),
+ block_size=block_size,
+ allow_overlapping=allow_overlapping
+ )
+
+
+def make_lab(user=None, name="Test_Lab_Instance",
+ status=LabStatus.UP, vlan_manager=None,
+ pub_net_count=5):
+ if not vlan_manager:
+ vlan_manager = make_vlan_manager()
+
+ if not user:
+ user = make_user()
+
+ lab = Lab.objects.create(
+ lab_user=user,
+ name=name,
+ contact_email='test_lab@test_site.org',
+ contact_phone='603 123 4567',
+ status=status,
+ vlan_manager=vlan_manager,
+ description='test lab instantiation',
+ api_token='12345678'
+ )
+
+ for i in range(pub_net_count):
+ make_public_net(vlan=i * 2 + 1, lab=lab)
+
+ return lab
+
+
+"""
+resource_inventory instantiation section for permanent resources
+"""
+
+
+def make_complete_host_profile(lab, name="test_hostprofile"):
+ host_profile = make_host_profile(lab, name=name)
+ make_disk_profile(host_profile, 500, name=name)
+ make_cpu_profile(host_profile)
+ make_interface_profile(host_profile, name=name)
+ make_ram_profile(host_profile)
+
+ return host_profile
+
+
+def make_host_profile(lab, host_type=0, name="test hostprofile"):
+ host_profile = HostProfile.objects.create(
+ host_type=host_type,
+ name=name,
+ description='test hostprofile instance'
+ )
+ host_profile.labs.add(lab)
+
+ return host_profile
+
+
+def make_ram_profile(host, channels=4, amount=256):
+ return RamProfile.objects.create(
+ host=host,
+ amount=amount,
+ channels=channels
+ )
+
+
+def make_disk_profile(hostprofile, size=0, media_type="SSD",
+ name="test diskprofile", rotation=0,
+ interface="sata"):
+ return DiskProfile.objects.create(
+ name=name,
+ size=size,
+ media_type=media_type,
+ host=hostprofile,
+ rotation=rotation,
+ interface=interface
+ )
+
+
+def make_cpu_profile(hostprofile,
+ cores=4,
+ architecture="x86_64",
+ cpus=4,):
+ return CpuProfile.objects.create(
+ cores=cores,
+ architecture=architecture,
+ cpus=cpus,
+ host=hostprofile,
+ cflags=''
+ )
+
+
+def make_interface_profile(hostprofile,
+ speed=1000,
+ name="test interface profile",
+ nic_type="pcie"):
+ return InterfaceProfile.objects.create(
+ host=hostprofile,
+ name=name,
+ speed=speed,
+ nic_type=nic_type
+ )
+
+
+def make_image(lab, lab_id, owner, os, host_profile,
+ public=True, name="default image", description="default image"):
+ return Image.objects.create(
+ from_lab=lab,
+ lab_id=lab_id,
+ os=os,
+ host_type=host_profile,
+ public=public,
+ name=name,
+ description=description
+ )
+
+
+def make_scenario(name="test scenario"):
+ return Scenario.objects.create(name=name)
+
+
+def make_installer(scenarios, name="test installer"):
+ installer = Installer.objects.create(name=name)
+ for scenario in scenarios:
+ installer.sup_scenarios.add(scenario)
+
+ return installer
+
+
+def make_os(installers, name="test OS"):
+ os = Opsys.objects.create(name=name)
+ for installer in installers:
+ os.sup_installers.add(installer)
+
+ return os
+
+
+def make_host(host_profile, lab, labid="test_host", name="test_host",
+ booked=False, working=True, config=None, template=None,
+ bundle=None, model="Model 1", vendor="ACME"):
+ return Host.objects.create(
+ lab=lab,
+ profile=host_profile,
+ name=name,
+ booked=booked,
+ working=working,
+ config=config,
+ template=template,
+ bundle=bundle,
+ model=model,
+ vendor=vendor
+ )
+
+
+def make_opnfv_role(name="Jumphost", description="test opnfvrole"):
+ return OPNFVRole.objects.create(
+ name=name,
+ description=description
+ )
+
+
+def make_public_net(vlan, lab, in_use=False,
+ cidr="0.0.0.0/0", gateway="0.0.0.0"):
+ return PublicNetwork.objects.create(
+ lab=lab,
+ vlan=vlan,
+ cidr=cidr,
+ gateway=gateway
+ )
diff --git a/dashboard/src/dashboard/views.py b/dashboard/src/dashboard/views.py
index c4a6685..aaad7ab 100644
--- a/dashboard/src/dashboard/views.py
+++ b/dashboard/src/dashboard/views.py
@@ -46,7 +46,7 @@ def lab_detail_view(request, lab_name):
'title': "Lab Overview",
'lab': lab,
'hostprofiles': lab.hostprofiles.all(),
- 'images': images
+ 'images': images,
}
)
diff --git a/dashboard/src/notifier/manager.py b/dashboard/src/notifier/manager.py
index f03c2cc..240cf85 100644
--- a/dashboard/src/notifier/manager.py
+++ b/dashboard/src/notifier/manager.py
@@ -18,13 +18,13 @@ class NotificationHandler(object):
@classmethod
def notify_new_booking(cls, booking):
template = "notifier/new_booking.html"
- titles = ["You have a new Booking", "You have been added to a Booking"]
+ titles = ["You have a new booking (" + str(booking.id) + ")", "You have been added to a booking (" + str(booking.id) + ")"]
cls.booking_notify(booking, template, titles)
@classmethod
def notify_booking_end(cls, booking):
template = "notifier/end_booking.html"
- titles = ["Your booking has ended", "A booking you collaborate on has ended"]
+ titles = ["Your booking (" + str(booking.id) + ") has ended", "A booking (" + str(booking.id) + ") that you collaborate on has ended"]
cls.booking_notify(booking, template, titles)
@classmethod
diff --git a/dashboard/src/notifier/migrations/0003_auto_20190123_1741.py b/dashboard/src/notifier/migrations/0003_auto_20190123_1741.py
new file mode 100644
index 0000000..f491993
--- /dev/null
+++ b/dashboard/src/notifier/migrations/0003_auto_20190123_1741.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.1 on 2019-01-23 17:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('notifier', '0002_auto_20181102_1631'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='notification',
+ name='is_html',
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name='notification',
+ name='is_read',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/dashboard/src/notifier/migrations/0004_auto_20190124_2115.py b/dashboard/src/notifier/migrations/0004_auto_20190124_2115.py
new file mode 100644
index 0000000..306ec7b
--- /dev/null
+++ b/dashboard/src/notifier/migrations/0004_auto_20190124_2115.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.1 on 2019-01-24 21:15
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('account', '0003_publicnetwork'),
+ ('notifier', '0003_auto_20190123_1741'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='notification',
+ name='is_read',
+ ),
+ migrations.AddField(
+ model_name='notification',
+ name='read_by',
+ field=models.ManyToManyField(related_name='read_notifications', to='account.UserProfile'),
+ ),
+ ]
diff --git a/dashboard/src/notifier/migrations/0005_auto_20190306_1616.py b/dashboard/src/notifier/migrations/0005_auto_20190306_1616.py
new file mode 100644
index 0000000..d92c988
--- /dev/null
+++ b/dashboard/src/notifier/migrations/0005_auto_20190306_1616.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2019-03-06 16:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('notifier', '0004_auto_20190124_2115'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='notification',
+ name='recipients',
+ field=models.ManyToManyField(related_name='notifications', to='account.UserProfile'),
+ ),
+ ]
diff --git a/dashboard/src/notifier/models.py b/dashboard/src/notifier/models.py
index 5e7c60e..49189e8 100644
--- a/dashboard/src/notifier/models.py
+++ b/dashboard/src/notifier/models.py
@@ -14,7 +14,9 @@ from account.models import UserProfile
class Notification(models.Model):
title = models.CharField(max_length=150)
content = models.TextField()
- recipients = models.ManyToManyField(UserProfile)
+ recipients = models.ManyToManyField(UserProfile, related_name='notifications')
+ is_html = models.BooleanField(default=True)
+ read_by = models.ManyToManyField(UserProfile, related_name='read_notifications')
def __str__(self):
return self.title
diff --git a/dashboard/src/notifier/views.py b/dashboard/src/notifier/views.py
index 4ee757f..3a85eda 100644
--- a/dashboard/src/notifier/views.py
+++ b/dashboard/src/notifier/views.py
@@ -7,27 +7,52 @@
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################
-from notifier.models import Notification
from django.shortcuts import render
+from notifier.models import Notification
+from django.db.models import Q
def InboxView(request):
if request.user.is_authenticated:
user = request.user
else:
- return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
+ return render(request, "dashboard/login.html",
+ {'title': 'Authentication Required'})
- return render(request, "notifier/inbox.html", {'notifications': Notification.objects.filter(recipients=user.userprofile)})
+ return render(request,
+ "notifier/inbox.html",
+ {'unread_notifications': Notification.objects.filter(recipients=user.userprofile).order_by('-id').filter(~Q(read_by=user.userprofile)),
+ 'read_notifications': Notification.objects.filter(recipients=user.userprofile).order_by('-id').filter(read_by=user.userprofile)})
def NotificationView(request, notification_id):
+
if request.user.is_authenticated:
user = request.user
else:
- return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
+ return render(request,
+ "dashboard/login.html",
+ {'title': 'Authentication Required'})
notification = Notification.objects.get(id=notification_id)
if user.userprofile not in notification.recipients.all():
- return render(request, "dashboard/login.html", {'title': 'Access Denied'})
-
- return render(request, "notifier/notification.html", {'notification': notification})
+ return render(request,
+ "dashboard/login.html", {'title': 'Access Denied'})
+
+ notification.read_by.add(user.userprofile)
+ notification.save()
+ if request.method == 'POST':
+ if 'delete' in request.POST:
+ # handle deleting
+ notification.recipients.remove(user.userprofile)
+ if not notification.recipients.exists():
+ notification.delete()
+ else:
+ notification.save()
+
+ if 'unread' in request.POST:
+ notification.read_by.remove(user.userprofile)
+ notification.save()
+
+ return render(request,
+ "notifier/notification.html", {'notification': notification})
diff --git a/dashboard/src/pharos_dashboard/settings.py b/dashboard/src/pharos_dashboard/settings.py
index 793eec7..86de78c 100644
--- a/dashboard/src/pharos_dashboard/settings.py
+++ b/dashboard/src/pharos_dashboard/settings.py
@@ -35,7 +35,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
- 'bootstrap3',
+ 'bootstrap4',
'crispy_forms',
'rest_framework',
'rest_framework.authtoken',
diff --git a/dashboard/src/resource_inventory/admin.py b/dashboard/src/resource_inventory/admin.py
index e063cc0..7ff510b 100644
--- a/dashboard/src/resource_inventory/admin.py
+++ b/dashboard/src/resource_inventory/admin.py
@@ -32,7 +32,8 @@ from resource_inventory.models import (
OPNFVConfig,
OPNFVRole,
Image,
- HostConfiguration
+ HostConfiguration,
+ RemoteInfo
)
profiles = [HostProfile, InterfaceProfile, DiskProfile, CpuProfile, RamProfile]
@@ -47,6 +48,6 @@ physical = [Host, Interface, Network, Vlan, ResourceBundle]
admin.site.register(physical)
-config = [Scenario, Installer, Opsys, ConfigBundle, OPNFVConfig, OPNFVRole, Image, HostConfiguration]
+config = [Scenario, Installer, Opsys, ConfigBundle, OPNFVConfig, OPNFVRole, Image, HostConfiguration, RemoteInfo]
admin.site.register(config)
diff --git a/dashboard/src/resource_inventory/idf_templater.py b/dashboard/src/resource_inventory/idf_templater.py
new file mode 100644
index 0000000..bf6eda0
--- /dev/null
+++ b/dashboard/src/resource_inventory/idf_templater.py
@@ -0,0 +1,151 @@
+##############################################################################
+# Copyright (c) 2019 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 django.template.loader import render_to_string
+
+from account.models import PublicNetwork
+
+from resource_inventory.models import Vlan
+
+
+class IDFTemplater:
+ """
+ Utility class to create a full IDF yaml file
+ """
+ net_names = ["admin", "mgmt", "private", "public"]
+ bridge_names = {
+ "admin": "br-admin",
+ "mgmt": "br-mgmt",
+ "private": "br-private",
+ "public": "br-public"
+ }
+
+ def __init__(self):
+ self.networks = {}
+ for i, name in enumerate(self.net_names):
+ self.networks[name] = {
+ "name": name,
+ "vlan": -1,
+ "interface": i,
+ "ip": "10.250." + str(i) + ".0",
+ "netmask": 24
+ }
+
+ def makeIDF(self, booking):
+ """
+ fills the installer descriptor file template with info about the resource
+ """
+ template = "dashboard/idf.yaml"
+ info = {}
+ info['version'] = "0.1"
+ info['net_config'] = self.get_net_config(booking)
+ info['fuel'] = self.get_fuel_config(booking)
+
+ return render_to_string(template, context=info)
+
+ def get_net_config(self, booking):
+ net_config = {}
+ try:
+ net_config['oob'] = self.get_oob_net(booking)
+ except Exception:
+ net_config['oob'] = {}
+ try:
+ net_config['public'] = self.get_public_net(booking)
+ except Exception:
+ net_config['public'] = {}
+
+ for net in [net for net in self.net_names if net != "public"]:
+ try:
+ net_config[net] = self.get_single_net_config(booking, net)
+ except Exception:
+ net_config[net] = {}
+
+ return net_config
+
+ def get_public_net(self, booking):
+ public = {}
+ config = booking.opnfv_config
+ public_role = config.networks.get(name="public")
+ public_vlan = Vlan.objects.filter(network=public_role.network).first()
+ public_network = PublicNetwork.objects.get(vlan=public_vlan.vlan_id, lab=booking.lab)
+ self.networks['public']['vlan'] = public_vlan.vlan_id
+ public['interface'] = self.networks['public']['interface']
+ public['vlan'] = public_network.vlan # untagged??
+ public['network'] = public_network.cidr.split("/")[0]
+ public['mask'] = public_network.cidr.split("/")[1]
+ # public['ip_range'] = 4 # necesary?
+ public['gateway'] = public_network.gateway
+ public['dns'] = ["1.1.1.1", "8.8.8.8"]
+
+ return public
+
+ def get_oob_net(self, booking):
+ net = {}
+ hosts = booking.resource.hosts.all()
+ addrs = [host.remote_management.address for host in hosts]
+ net['ip_range'] = ",".join(addrs)
+ net['vlan'] = "native"
+ return net
+
+ def get_single_net_config(self, booking, net_name):
+ config = booking.opnfv_config
+ role = config.networks.get(name=net_name)
+ vlan = Vlan.objects.filter(network=role.network).first()
+ self.networks[net_name]['vlan'] = vlan.vlan_id
+ net = {}
+ net['interface'] = self.networks[net_name]['interface']
+ net['vlan'] = vlan.vlan_id
+ net['network'] = self.networks[net_name]['ip']
+ net['mask'] = self.networks[net_name]['netmask']
+
+ return net
+
+ def get_fuel_config(self, booking):
+ fuel = {}
+ fuel['jumphost'] = {}
+ try:
+ fuel['jumphost']['bridges'] = self.get_fuel_bridges()
+ except Exception:
+ fuel['jumphost']['bridges'] = {}
+
+ fuel['network'] = {}
+ try:
+ fuel['network']['nodes'] = self.get_fuel_nodes(booking)
+ except Exception:
+ fuel['network']['nodes'] = {}
+
+ return fuel
+
+ def get_fuel_bridges(self):
+ return self.bridge_names
+
+ def get_fuel_nodes(self, booking):
+ jumphost = booking.opnfv_config.host_opnfv_config.get(
+ role__name__iexact="jumphost"
+ )
+ hosts = booking.resource.hosts.exclude(pk=jumphost.pk)
+ nodes = []
+ for host in hosts:
+ node = {}
+ ordered_interfaces = self.get_node_interfaces(host)
+ node['interfaces'] = [iface['name'] for iface in ordered_interfaces]
+ node['bus_addrs'] = [iface['bus'] for iface in ordered_interfaces]
+ nodes.append(node)
+
+ return nodes
+
+ def get_node_interfaces(self, node):
+ # TODO: this should sync with pdf ordering
+ interfaces = []
+
+ for iface in node.interfaces.all():
+ interfaces.append({"name": iface.name, "bus": iface.bus_address})
+
+ return interfaces
diff --git a/dashboard/src/resource_inventory/migrations/0007_auto_20190306_1616.py b/dashboard/src/resource_inventory/migrations/0007_auto_20190306_1616.py
new file mode 100644
index 0000000..19a49c5
--- /dev/null
+++ b/dashboard/src/resource_inventory/migrations/0007_auto_20190306_1616.py
@@ -0,0 +1,31 @@
+# Generated by Django 2.1 on 2019-03-06 16:16
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0006_auto_20190124_1700'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='RemoteInfo',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('address', models.CharField(max_length=15)),
+ ('mac_address', models.CharField(max_length=17)),
+ ('password', models.CharField(max_length=100)),
+ ('user', models.CharField(max_length=100)),
+ ('management_type', models.CharField(default='ipmi', max_length=50)),
+ ('versions', models.CharField(max_length=100)),
+ ],
+ ),
+ migrations.AlterField(
+ model_name='genericinterface',
+ name='profile',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.InterfaceProfile'),
+ ),
+ ]
diff --git a/dashboard/src/resource_inventory/migrations/0008_host_remote_management.py b/dashboard/src/resource_inventory/migrations/0008_host_remote_management.py
new file mode 100644
index 0000000..f74a535
--- /dev/null
+++ b/dashboard/src/resource_inventory/migrations/0008_host_remote_management.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.1 on 2019-03-06 16:42
+
+from django.db import migrations, models
+import resource_inventory.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0007_auto_20190306_1616'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='host',
+ name='remote_management',
+ field=models.ForeignKey(default=resource_inventory.models.get_default_remote_info, on_delete=models.SET(resource_inventory.models.get_default_remote_info), to='resource_inventory.RemoteInfo'),
+ ),
+ ]
diff --git a/dashboard/src/resource_inventory/migrations/0009_auto_20190315_1757.py b/dashboard/src/resource_inventory/migrations/0009_auto_20190315_1757.py
new file mode 100644
index 0000000..92ed0e9
--- /dev/null
+++ b/dashboard/src/resource_inventory/migrations/0009_auto_20190315_1757.py
@@ -0,0 +1,73 @@
+# Generated by Django 2.1 on 2019-03-15 17:57
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0008_host_remote_management'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='NetworkConnection',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('vlan_is_tagged', models.BooleanField()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='NetworkRole',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name='genericinterface',
+ name='vlans',
+ ),
+ migrations.RemoveField(
+ model_name='network',
+ name='vlan_id',
+ ),
+ migrations.AddField(
+ model_name='network',
+ name='bundle',
+ field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='networks', to='resource_inventory.GenericResourceBundle'),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='network',
+ name='is_public',
+ field=models.BooleanField(default=False),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='vlan',
+ name='network',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.Network'),
+ ),
+ migrations.AddField(
+ model_name='networkrole',
+ name='network',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Network'),
+ ),
+ migrations.AddField(
+ model_name='networkconnection',
+ name='network',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Network'),
+ ),
+ migrations.AddField(
+ model_name='genericinterface',
+ name='connections',
+ field=models.ManyToManyField(to='resource_inventory.NetworkConnection'),
+ ),
+ migrations.AddField(
+ model_name='opnfvconfig',
+ name='networks',
+ field=models.ManyToManyField(to='resource_inventory.NetworkRole'),
+ ),
+ ]
diff --git a/dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py b/dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py
new file mode 100644
index 0000000..3823eaf
--- /dev/null
+++ b/dashboard/src/resource_inventory/migrations/0010_auto_20190430_1405.py
@@ -0,0 +1,54 @@
+# Generated by Django 2.1 on 2019-04-30 14:05
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0009_auto_20190315_1757'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='HostOPNFVConfig',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name='hostconfiguration',
+ name='opnfvRole',
+ ),
+ migrations.AddField(
+ model_name='hostconfiguration',
+ name='is_head_node',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='opnfvconfig',
+ name='description',
+ field=models.CharField(blank=True, default='', max_length=600),
+ ),
+ migrations.AddField(
+ model_name='opnfvconfig',
+ name='name',
+ field=models.CharField(blank=True, default='', max_length=300),
+ ),
+ migrations.AddField(
+ model_name='hostopnfvconfig',
+ name='host_config',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='host_opnfv_config', to='resource_inventory.HostConfiguration'),
+ ),
+ migrations.AddField(
+ model_name='hostopnfvconfig',
+ name='opnfv_config',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='host_opnfv_config', to='resource_inventory.OPNFVConfig'),
+ ),
+ migrations.AddField(
+ model_name='hostopnfvconfig',
+ name='role',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='host_opnfv_configs', to='resource_inventory.OPNFVRole'),
+ ),
+ ]
diff --git a/dashboard/src/resource_inventory/models.py b/dashboard/src/resource_inventory/models.py
index e1f2aa3..b9f2c44 100644
--- a/dashboard/src/resource_inventory/models.py
+++ b/dashboard/src/resource_inventory/models.py
@@ -105,26 +105,6 @@ class RamProfile(models.Model):
return str(self.amount) + "G for " + str(self.host)
-# Networking -- located here due to import order requirements
-class Network(models.Model):
- id = models.AutoField(primary_key=True)
- vlan_id = models.IntegerField()
- name = models.CharField(max_length=100)
-
- def __str__(self):
- return self.name
-
-
-class Vlan(models.Model):
- id = models.AutoField(primary_key=True)
- vlan_id = models.IntegerField()
- tagged = models.BooleanField()
- public = models.BooleanField(default=False)
-
- def __str__(self):
- return str(self.vlan_id) + ("_T" if self.tagged else "")
-
-
# Generic resource templates
class GenericResourceBundle(models.Model):
id = models.AutoField(primary_key=True)
@@ -145,6 +125,32 @@ class GenericResourceBundle(models.Model):
return self.name
+class Network(models.Model):
+ id = models.AutoField(primary_key=True)
+ name = models.CharField(max_length=100)
+ bundle = models.ForeignKey(GenericResourceBundle, on_delete=models.CASCADE, related_name="networks")
+ is_public = models.BooleanField()
+
+ def __str__(self):
+ return self.name
+
+
+class NetworkConnection(models.Model):
+ network = models.ForeignKey(Network, on_delete=models.CASCADE)
+ vlan_is_tagged = models.BooleanField()
+
+
+class Vlan(models.Model):
+ id = models.AutoField(primary_key=True)
+ vlan_id = models.IntegerField()
+ tagged = models.BooleanField()
+ public = models.BooleanField(default=False)
+ network = models.ForeignKey(Network, on_delete=models.DO_NOTHING, null=True)
+
+ def __str__(self):
+ return str(self.vlan_id) + ("_T" if self.tagged else "")
+
+
class GenericResource(models.Model):
bundle = models.ForeignKey(GenericResourceBundle, related_name='generic_resources', on_delete=models.CASCADE)
hostname_validchars = RegexValidator(regex=r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))', message="Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)")
@@ -184,15 +190,15 @@ class ResourceBundle(models.Model):
return "Resource bundle " + str(self.id) + " with no template"
return "instance of " + str(self.template)
-
-# Networking
+ def get_host(self, role="Jumphost"):
+ return Host.objects.filter(bundle=self, config__is_head_node=True).first() # should only ever be one, but it is not an invariant in the models
class GenericInterface(models.Model):
id = models.AutoField(primary_key=True)
- vlans = models.ManyToManyField(Vlan)
profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE)
host = models.ForeignKey(GenericHost, on_delete=models.CASCADE, related_name='generic_interfaces')
+ connections = models.ManyToManyField(NetworkConnection)
def __str__(self):
return "type " + str(self.profile) + " on host " + str(self.host)
@@ -224,6 +230,11 @@ class Opsys(models.Model):
return self.name
+class NetworkRole(models.Model):
+ name = models.CharField(max_length=100)
+ network = models.ForeignKey(Network, on_delete=models.CASCADE)
+
+
class ConfigBundle(models.Model):
id = models.AutoField(primary_key=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -240,6 +251,9 @@ class OPNFVConfig(models.Model):
installer = models.ForeignKey(Installer, on_delete=models.CASCADE)
scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE)
bundle = models.ForeignKey(ConfigBundle, related_name="opnfv_config", on_delete=models.CASCADE)
+ networks = models.ManyToManyField(NetworkRole)
+ name = models.CharField(max_length=300, blank=True, default="")
+ description = models.CharField(max_length=600, blank=True, default="")
def __str__(self):
return "OPNFV job with " + str(self.installer) + " and " + str(self.scenario)
@@ -266,7 +280,7 @@ class Image(models.Model):
public = models.BooleanField(default=True)
host_type = models.ForeignKey(HostProfile, on_delete=models.CASCADE)
description = models.TextField()
- os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE) #sentinal?
+ os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE)
def __str__(self):
return self.name
@@ -285,12 +299,38 @@ class HostConfiguration(models.Model):
host = models.ForeignKey(GenericHost, related_name="configuration", on_delete=models.CASCADE)
image = models.ForeignKey(Image, on_delete=models.PROTECT)
bundle = models.ForeignKey(ConfigBundle, related_name="hostConfigurations", null=True, on_delete=models.CASCADE)
- opnfvRole = models.ForeignKey(OPNFVRole, on_delete=models.SET(get_sentinal_opnfv_role))
+ is_head_node = models.BooleanField(default=False)
def __str__(self):
return "config with " + str(self.host) + " and image " + str(self.image)
+class HostOPNFVConfig(models.Model):
+ role = models.ForeignKey(OPNFVRole, related_name="host_opnfv_configs", on_delete=models.CASCADE)
+ host_config = models.ForeignKey(HostConfiguration, related_name="host_opnfv_config", on_delete=models.CASCADE)
+ opnfv_config = models.ForeignKey(OPNFVConfig, related_name="host_opnfv_config", on_delete=models.CASCADE)
+
+
+class RemoteInfo(models.Model):
+ address = models.CharField(max_length=15)
+ mac_address = models.CharField(max_length=17)
+ password = models.CharField(max_length=100)
+ user = models.CharField(max_length=100)
+ management_type = models.CharField(max_length=50, default="ipmi")
+ versions = models.CharField(max_length=100) # json serialized list of floats
+
+
+def get_default_remote_info():
+ return RemoteInfo.objects.get_or_create(
+ address="default",
+ mac_address="default",
+ password="default",
+ user="default",
+ management_type="default",
+ versions="[default]"
+ )[0].pk
+
+
# Concrete host, actual machine in a lab
class Host(models.Model):
id = models.AutoField(primary_key=True)
@@ -305,6 +345,7 @@ class Host(models.Model):
working = models.BooleanField(default=True)
vendor = models.CharField(max_length=100, default="unknown")
model = models.CharField(max_length=150, default="unknown")
+ remote_management = models.ForeignKey(RemoteInfo, default=get_default_remote_info, on_delete=models.SET(get_default_remote_info))
def __str__(self):
return self.name
@@ -320,3 +361,11 @@ class Interface(models.Model):
def __str__(self):
return self.mac_address + " on host " + str(self.host)
+
+
+class OPNFV_SETTINGS():
+ """
+ This is a static configuration class
+ """
+ # all the required network types in PDF/IDF spec
+ NETWORK_ROLES = ["public", "private", "admin", "mgmt"]
diff --git a/dashboard/src/resource_inventory/pdf_templater.py b/dashboard/src/resource_inventory/pdf_templater.py
new file mode 100644
index 0000000..2302530
--- /dev/null
+++ b/dashboard/src/resource_inventory/pdf_templater.py
@@ -0,0 +1,193 @@
+##############################################################################
+# 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 django.template.loader import render_to_string
+import booking
+from resource_inventory.models import Host, InterfaceProfile
+
+
+class PDFTemplater:
+ """
+ Utility class to create a full PDF yaml file
+ """
+
+ @classmethod
+ def makePDF(cls, booking):
+ """
+ fills the pod descriptor file template with info about the resource
+ """
+ template = "dashboard/pdf.yaml"
+ info = {}
+ info['details'] = cls.get_pdf_details(booking.resource)
+ info['jumphost'] = cls.get_pdf_jumphost(booking)
+ info['nodes'] = cls.get_pdf_nodes(booking)
+
+ return render_to_string(template, context=info)
+
+ @classmethod
+ def get_pdf_details(cls, resource):
+ """
+ Info for the "details" section
+ """
+ details = {}
+ owner = "Anon"
+ email = "email@mail.com"
+ resource_lab = resource.template.lab
+ lab = resource_lab.name
+ location = resource_lab.location
+ pod_type = "development"
+ link = "https://wiki.opnfv.org/display/INF/Pharos+Laas"
+
+ try:
+ # try to get more specific info that may fail, we dont care if it does
+ booking_owner = booking.models.Booking.objects.get(resource=resource).owner
+ owner = booking_owner.username
+ email = booking_owner.userprofile.email_addr
+ except Exception:
+ pass
+
+ details['contact'] = email
+ details['lab'] = lab
+ details['link'] = link
+ details['owner'] = owner
+ details['location'] = location
+ details['type'] = pod_type
+
+ return details
+
+ @classmethod
+ def get_jumphost(cls, booking):
+ jumphost = None
+ if booking.opnfv_config:
+ jumphost_opnfv_config = booking.opnfv_config.host_opnfv_config.get(
+ role__name__iexact="jumphost"
+ )
+ jumphost = booking.resource.hosts.get(config=jumphost_opnfv_config.host_config)
+ else: # if there is no opnfv config, use headnode
+ jumphost = Host.objects.filter(
+ bundle=booking.resource,
+ config__is_head_node=True
+ ).first()
+
+ return jumphost
+
+ @classmethod
+ def get_pdf_jumphost(cls, booking):
+ """
+ returns a dict of all the info for the "jumphost" section
+ """
+ jumphost = cls.get_jumphost(booking)
+ jumphost_info = cls.get_pdf_host(jumphost)
+ jumphost_info['os'] = jumphost.config.image.os.name
+ return jumphost_info
+
+ @classmethod
+ def get_pdf_nodes(cls, booking):
+ """
+ returns a list of all the "nodes" (every host except jumphost)
+ """
+ pdf_nodes = []
+ nodes = set(Host.objects.filter(bundle=booking.resource))
+ nodes.discard(cls.get_jumphost(booking))
+
+ for node in nodes:
+ pdf_nodes.append(cls.get_pdf_host(node))
+
+ return pdf_nodes
+
+ @classmethod
+ def get_pdf_host(cls, host):
+ """
+ method to gather all needed info about a host
+ returns a dict
+ """
+ host_info = {}
+ host_info['name'] = host.template.resource.name
+ host_info['node'] = cls.get_pdf_host_node(host)
+ host_info['disks'] = []
+ for disk in host.profile.storageprofile.all():
+ host_info['disks'].append(cls.get_pdf_host_disk(disk))
+
+ host_info['interfaces'] = []
+ for interface in host.interfaces.all():
+ host_info['interfaces'].append(cls.get_pdf_host_iface(interface))
+
+ host_info['remote'] = cls.get_pdf_host_remote_management(host)
+
+ return host_info
+
+ @classmethod
+ def get_pdf_host_node(cls, host):
+ """
+ returns "node" info for a given host
+ """
+ d = {}
+ d['type'] = "baremetal"
+ d['vendor'] = host.vendor
+ d['model'] = host.model
+ d['memory'] = str(host.profile.ramprofile.first().amount) + "G"
+
+ cpu = host.profile.cpuprofile.first()
+ d['arch'] = cpu.architecture
+ d['cpus'] = cpu.cpus
+ d['cores'] = cpu.cores
+ cflags = cpu.cflags
+ if cflags and cflags.strip():
+ d['cpu_cflags'] = cflags
+ else:
+ d['cpu_cflags'] = "none"
+
+ return d
+
+ @classmethod
+ def get_pdf_host_disk(cls, disk):
+ """
+ returns a dict describing the given disk
+ """
+ disk_info = {}
+ disk_info['name'] = disk.name
+ disk_info['capacity'] = str(disk.size) + "G"
+ disk_info['type'] = disk.media_type
+ disk_info['interface'] = disk.interface
+ disk_info['rotation'] = disk.rotation
+ return disk_info
+
+ @classmethod
+ def get_pdf_host_iface(cls, interface):
+ """
+ returns a dict describing given interface
+ """
+ iface_info = {}
+ iface_info['features'] = "none"
+ iface_info['mac_address'] = interface.mac_address
+ iface_info['name'] = interface.name
+ speed = "unknown"
+ try:
+ profile = InterfaceProfile.objects.get(host=interface.host.profile, name=interface.name)
+ speed = str(int(profile.speed / 1000)) + "gb"
+ except Exception:
+ pass
+ iface_info['speed'] = speed
+ return iface_info
+
+ @classmethod
+ def get_pdf_host_remote_management(cls, host):
+ """
+ gives the remote params of the host
+ """
+ man = host.remote_management
+ mgmt = {}
+ mgmt['address'] = man.address
+ mgmt['mac_address'] = man.mac_address
+ mgmt['pass'] = man.password
+ mgmt['type'] = man.management_type
+ mgmt['user'] = man.user
+ mgmt['versions'] = [man.versions]
+ return mgmt
diff --git a/dashboard/src/resource_inventory/resource_manager.py b/dashboard/src/resource_inventory/resource_manager.py
index 812fcd7..652e4e3 100644
--- a/dashboard/src/resource_inventory/resource_manager.py
+++ b/dashboard/src/resource_inventory/resource_manager.py
@@ -8,16 +8,20 @@
##############################################################################
-from django.template.loader import render_to_string
-
-import booking
from dashboard.exceptions import (
ResourceExistenceException,
ResourceAvailabilityException,
ResourceProvisioningException,
ModelValidationException,
)
-from resource_inventory.models import Host, HostConfiguration, ResourceBundle, HostProfile
+from resource_inventory.models import (
+ Host,
+ HostConfiguration,
+ ResourceBundle,
+ HostProfile,
+ Network,
+ Vlan
+)
class ResourceManager:
@@ -38,61 +42,102 @@ class ResourceManager:
hostprofileset = HostProfile.objects.filter(host__in=hostset, labs=lab)
return set(hostprofileset)
+ def hostsAvailable(self, grb):
+ """
+ This method will check if the given GenericResourceBundle
+ is available. No changes to the database
+ """
+
+ # count up hosts
+ profile_count = {}
+ for host in grb.getHosts():
+ if host.profile not in profile_count:
+ profile_count[host.profile] = 0
+ profile_count[host.profile] += 1
+
+ # check that all required hosts are available
+ for profile in profile_count.keys():
+ available = Host.objects.filter(
+ booked=False,
+ lab=grb.lab,
+ profile=profile
+ ).count()
+ needed = profile_count[profile]
+ if available < needed:
+ return False
+ return True
+
# public interface
def deleteResourceBundle(self, resourceBundle):
for host in Host.objects.filter(bundle=resourceBundle):
self.releaseHost(host)
resourceBundle.delete()
- def convertResourceBundle(self, genericResourceBundle, lab=None, config=None):
+ def get_vlans(self, genericResourceBundle):
+ networks = {}
+ vlan_manager = genericResourceBundle.lab.vlan_manager
+ for network in genericResourceBundle.networks.all():
+ if network.is_public:
+ public_net = vlan_manager.get_public_vlan()
+ vlan_manager.reserve_public_vlan(public_net.vlan)
+ networks[network.name] = public_net.vlan
+ else:
+ vlan = vlan_manager.get_vlan()
+ vlan_manager.reserve_vlans(vlan)
+ networks[network.name] = vlan
+ return networks
+
+ def convertResourceBundle(self, genericResourceBundle, config=None):
"""
Takes in a GenericResourceBundle and 'converts' it into a ResourceBundle
"""
- resource_bundle = ResourceBundle()
- resource_bundle.template = genericResourceBundle
- resource_bundle.save()
-
- hosts = genericResourceBundle.getHosts()
-
- # current supported case: user creating new booking
- # currently unsupported: editing existing booking
-
+ resource_bundle = ResourceBundle.objects.create(template=genericResourceBundle)
+ generic_hosts = genericResourceBundle.getHosts()
physical_hosts = []
- for host in hosts:
+ vlan_map = self.get_vlans(genericResourceBundle)
+
+ for generic_host in generic_hosts:
host_config = None
if config:
- host_config = HostConfiguration.objects.get(bundle=config, host=host)
+ host_config = HostConfiguration.objects.get(bundle=config, host=generic_host)
try:
- physical_host = self.acquireHost(host, genericResourceBundle.lab.name)
+ physical_host = self.acquireHost(generic_host, genericResourceBundle.lab.name)
except ResourceAvailabilityException:
- self.fail_acquire(physical_hosts)
+ self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
raise ResourceAvailabilityException("Could not provision hosts, not enough available")
try:
physical_host.bundle = resource_bundle
- physical_host.template = host
+ physical_host.template = generic_host
physical_host.config = host_config
physical_hosts.append(physical_host)
- self.configureNetworking(physical_host)
+ self.configureNetworking(physical_host, vlan_map)
except Exception:
- self.fail_acquire(physical_hosts)
+ self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
raise ResourceProvisioningException("Network configuration failed.")
try:
physical_host.save()
except Exception:
- self.fail_acquire(physical_hosts)
+ self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
raise ModelValidationException("Saving hosts failed")
return resource_bundle
- def configureNetworking(self, host):
+ def configureNetworking(self, host, vlan_map):
generic_interfaces = list(host.template.generic_interfaces.all())
for int_num, physical_interface in enumerate(host.interfaces.all()):
generic_interface = generic_interfaces[int_num]
physical_interface.config.clear()
- for vlan in generic_interface.vlans.all():
- physical_interface.config.add(vlan)
+ for connection in generic_interface.connections.all():
+ physical_interface.config.add(
+ Vlan.objects.create(
+ vlan_id=vlan_map[connection.network.name],
+ tagged=connection.vlan_is_tagged,
+ public=connection.network.is_public,
+ network=connection.network
+ )
+ )
# private interface
def acquireHost(self, genericHost, labName):
@@ -114,93 +159,16 @@ class ResourceManager:
host.booked = False
host.save()
- def fail_acquire(self, hosts):
+ def releaseNetworks(self, grb, vlan_manager, vlans):
+ for net_name, vlan_id in vlans.items():
+ net = Network.objects.get(name=net_name, bundle=grb)
+ if(net.is_public):
+ vlan_manager.release_public_vlan(vlan_id)
+ else:
+ vlan_manager.release_vlans(vlan_id)
+
+ def fail_acquire(self, hosts, vlans, grb):
+ vlan_manager = grb.lab.vlan_manager
+ self.releaseNetworks(grb, vlan_manager, vlans)
for host in hosts:
self.releaseHost(host)
-
- def makePDF(self, resource):
- """
- fills the pod descriptor file template with info about the resource
- """
- template = "dashboard/pdf.yaml"
- info = {}
- info['details'] = self.get_pdf_details(resource)
- info['jumphost'] = self.get_pdf_jumphost(resource)
- info['nodes'] = self.get_pdf_nodes(resource)
-
- return render_to_string(template, context=info)
-
- def get_pdf_details(self, resource):
- details = {}
- owner = "Anon"
- email = "email@mail.com"
- resource_lab = resource.template.lab
- lab = resource_lab.name
- location = resource_lab.location
- pod_type = "development"
- link = "https://wiki.opnfv.org/display/INF/Pharos+Laas"
-
- try:
- # try to get more specific info that may fail, we dont care if it does
- booking_owner = booking.models.Booking.objects.get(resource=resource).owner
- owner = booking_owner.username
- email = booking_owner.userprofile.email_addr
- except Exception:
- pass
-
- details['owner'] = owner
- details['email'] = email
- details['lab'] = lab
- details['location'] = location
- details['type'] = pod_type
- details['link'] = link
-
- return details
-
- def get_pdf_jumphost(self, resource):
- jumphost = Host.objects.get(bundle=resource, config__opnfvRole__name__iexact="jumphost")
- return self.get_pdf_host(jumphost)
-
- def get_pdf_nodes(self, resource):
- pdf_nodes = []
- nodes = Host.objects.filter(bundle=resource).exclude(config__opnfvRole__name__iexact="jumphost")
- for node in nodes:
- pdf_nodes.append(self.get_pdf_host(node))
-
- return pdf_nodes
-
- def get_pdf_host(self, host):
- host_info = {}
- host_info['name'] = host.template.resource.name
- host_info['node'] = {}
- host_info['node']['type'] = "baremetal"
- host_info['node']['vendor'] = host.vendor
- host_info['node']['model'] = host.model
- host_info['node']['arch'] = host.profile.cpuprofile.first().architecture
- host_info['node']['cpus'] = host.profile.cpuprofile.first().cpus
- host_info['node']['cores'] = host.profile.cpuprofile.first().cores
- cflags = host.profile.cpuprofile.first().cflags
- if cflags and cflags.strip():
- host_info['node']['cpu_cflags'] = cflags
- host_info['node']['memory'] = str(host.profile.ramprofile.first().amount) + "G"
- host_info['disks'] = []
- for disk in host.profile.storageprofile.all():
- disk_info = {}
- disk_info['name'] = disk.name
- disk_info['capacity'] = str(disk.size) + "G"
- disk_info['type'] = disk.media_type
- disk_info['interface'] = disk.interface
- disk_info['rotation'] = disk.rotation
- host_info['disks'].append(disk_info)
-
- host_info['interfaces'] = []
- for interface in host.interfaces.all():
- iface_info = {}
- iface_info['name'] = interface.name
- iface_info['address'] = "unknown"
- iface_info['mac_address'] = interface.mac_address
- vlans = "|".join([str(vlan.vlan_id) for vlan in interface.config.all()])
- iface_info['vlans'] = vlans
- host_info['interfaces'].append(iface_info)
-
- return host_info
diff --git a/dashboard/src/resource_inventory/urls.py b/dashboard/src/resource_inventory/urls.py
index 4e159ba..a72871b 100644
--- a/dashboard/src/resource_inventory/urls.py
+++ b/dashboard/src/resource_inventory/urls.py
@@ -25,10 +25,11 @@ Including another URLconf
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url
-from resource_inventory.views import HostView
+from resource_inventory.views import HostView, hostprofile_detail_view
app_name = "resource"
urlpatterns = [
- url(r'^hosts$', HostView.as_view(), name='hosts')
+ url(r'^hosts$', HostView.as_view(), name='hosts'),
+ url(r'^profiles/(?P<hostprofile_id>.+)/$', hostprofile_detail_view, name='host_detail'),
]
diff --git a/dashboard/src/resource_inventory/views.py b/dashboard/src/resource_inventory/views.py
index 2937bd7..8c3d899 100644
--- a/dashboard/src/resource_inventory/views.py
+++ b/dashboard/src/resource_inventory/views.py
@@ -9,8 +9,10 @@
from django.views.generic import TemplateView
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
-from resource_inventory.models import Host
+from resource_inventory.models import HostProfile, Host
class HostView(TemplateView):
@@ -21,3 +23,16 @@ class HostView(TemplateView):
hosts = Host.objects.filter(working=True)
context.update({'hosts': hosts, 'title': "Hardware Resources"})
return context
+
+
+def hostprofile_detail_view(request, hostprofile_id):
+ hostprofile = get_object_or_404(HostProfile, id=hostprofile_id)
+
+ return render(
+ request,
+ "resource/hostprofile_detail.html",
+ {
+ 'title': "Host Type: " + str(hostprofile.name),
+ 'hostprofile': hostprofile
+ }
+ )
diff --git a/dashboard/src/static/bower.json b/dashboard/src/static/bower.json
index 9ae744a..dda786d 100644
--- a/dashboard/src/static/bower.json
+++ b/dashboard/src/static/bower.json
@@ -16,12 +16,14 @@
"tests"
],
"dependencies": {
- "eonasdan-bootstrap-datetimepicker": "^4.17.37",
"fullcalendar": "^2.9.0",
"jquery-migrate": "^3.0.0",
- "startbootstrap-sb-admin-2-blackrockdigital": "^3.3.7"
- },
- "resolutions": {
- "font-awesome": "~4.6.3"
+ "bootstrap": "4.3.1",
+ "popper.js": "1.14.3",
+ "Font-Awesome": "5.9.0",
+ "datatables.net": "1.10.19",
+ "datatables.net-bs4": "1.10.19",
+ "datatables.net-responsive": "2.1.1",
+ "datatables.net-responsive-bs4": "2.2.3"
}
}
diff --git a/dashboard/src/static/css/base.css b/dashboard/src/static/css/base.css
new file mode 100644
index 0000000..c51728c
--- /dev/null
+++ b/dashboard/src/static/css/base.css
@@ -0,0 +1,8 @@
+/* Rotating arrows when dropdown happens */
+i.fas.rotate {
+ transition: transform 0.3s ease-in-out;
+}
+
+a[aria-expanded="true"] > i.rotate {
+ transform: rotate(180deg);
+}
diff --git a/dashboard/src/static/css/detail_view.css b/dashboard/src/static/css/detail_view.css
index 7948d85..c3d0a4d 100644
--- a/dashboard/src/static/css/detail_view.css
+++ b/dashboard/src/static/css/detail_view.css
@@ -24,14 +24,15 @@
}
.detail_card {
- border: 2px;
- border-color: black;
border-radius: 5px;
+ border-top: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
margin: 5px;
padding-left: 25px;
padding-right: 25px;
padding-bottom: 15px;
- box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75);
display: flex;
flex-direction: column;
justify-content: space-between;
diff --git a/dashboard/src/static/js/dashboard.js b/dashboard/src/static/js/dashboard.js
new file mode 100644
index 0000000..84c3703
--- /dev/null
+++ b/dashboard/src/static/js/dashboard.js
@@ -0,0 +1,1134 @@
+class MultipleSelectFilterWidget {
+
+ constructor(neighbors, items, initial) {
+ this.inputs = [];
+ this.graph_neighbors = neighbors;
+ this.filter_items = items;
+ this.result = {};
+ this.dropdown_count = 0;
+
+ for(let nodeId in this.filter_items) {
+ const node = this.filter_items[nodeId];
+ this.result[node.class] = {}
+ }
+
+ this.make_selection(initial);
+ }
+
+ make_selection( initial_data ){
+ if(!initial_data || jQuery.isEmptyObject(initial_data))
+ return;
+ for(let item_class in initial_data) {
+ const selected_items = initial_data[item_class];
+ for( let node_id in selected_items ){
+ const node = this.filter_items[node_id];
+ const selection_data = selected_items[node_id]
+ if( selection_data.selected ) {
+ this.select(node);
+ this.markAndSweep(node);
+ this.updateResult(node);
+ }
+ if(node['multiple']){
+ this.make_multiple_selection(node, selection_data);
+ }
+ }
+ }
+ }
+
+ make_multiple_selection(node, selection_data){
+ const prepop_data = selection_data.values;
+ for(let k in prepop_data){
+ const div = this.add_item_prepopulate(node, prepop_data[k]);
+ this.updateObjectResult(node, div.id, prepop_data[k]);
+ }
+ }
+
+ markAndSweep(root){
+ for(let i in this.filter_items) {
+ const node = this.filter_items[i];
+ node['marked'] = true; //mark all nodes
+ }
+
+ const toCheck = [root];
+ while(toCheck.length > 0){
+ const node = toCheck.pop();
+ if(!node['marked']) {
+ continue; //already visited, just continue
+ }
+ node['marked'] = false; //mark as visited
+ if(node['follow'] || node == root){ //add neighbors if we want to follow this node
+ const neighbors = this.graph_neighbors[node.id];
+ for(let neighId of neighbors) {
+ const neighbor = this.filter_items[neighId];
+ toCheck.push(neighbor);
+ }
+ }
+ }
+
+ //now remove all nodes still marked
+ for(let i in this.filter_items){
+ const node = this.filter_items[i];
+ if(node['marked']){
+ this.disable_node(node);
+ }
+ }
+ }
+
+ process(node) {
+ if(node['selected']) {
+ this.markAndSweep(node);
+ }
+ else { //TODO: make this not dumb
+ const selected = []
+ //remember the currently selected, then reset everything and reselect one at a time
+ for(let nodeId in this.filter_items) {
+ node = this.filter_items[nodeId];
+ if(node['selected']) {
+ selected.push(node);
+ }
+ this.clear(node);
+ }
+ for(let node of selected) {
+ this.select(node);
+ this.markAndSweep(node);
+ }
+ }
+ }
+
+ select(node) {
+ const elem = document.getElementById(node['id']);
+ node['selected'] = true;
+ elem.classList.remove('disabled_node', 'cleared_node');
+ elem.classList.add('selected_node');
+ }
+
+ clear(node) {
+ const elem = document.getElementById(node['id']);
+ node['selected'] = false;
+ node['selectable'] = true;
+ elem.classList.add('cleared_node')
+ elem.classList.remove('disabled_node', 'selected_node');
+ }
+
+ disable_node(node) {
+ const elem = document.getElementById(node['id']);
+ node['selected'] = false;
+ node['selectable'] = false;
+ elem.classList.remove('cleared_node', 'selected_node');
+ elem.classList.add('disabled_node');
+ }
+
+ processClick(id){
+ const node = this.filter_items[id];
+ if(!node['selectable'])
+ return;
+
+ if(node['multiple']){
+ return this.processClickMultiple(node);
+ } else {
+ return this.processClickSingle(node);
+ }
+ }
+
+ processClickSingle(node){
+ node['selected'] = !node['selected']; //toggle on click
+ if(node['selected']) {
+ this.select(node);
+ } else {
+ this.clear(node);
+ }
+ this.process(node);
+ this.updateResult(node);
+ }
+
+ processClickMultiple(node){
+ this.select(node);
+ const div = this.add_item_prepopulate(node, false);
+ this.process(node);
+ this.updateObjectResult(node, div.id, "");
+ }
+
+ restrictchars(input){
+ if( input.validity.patternMismatch ){
+ input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
+ input.reportValidity();
+ }
+ input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
+ this.checkunique(input);
+ }
+
+ checkunique(tocheck){ //TODO: use set
+ const val = tocheck.value;
+ for( let input of this.inputs ){
+ if( input.value == val && input != tocheck){
+ tocheck.setCustomValidity("All hostnames must be unique");
+ tocheck.reportValidity();
+ return;
+ }
+ }
+ tocheck.setCustomValidity("");
+ }
+
+ make_remove_button(div, node){
+ const button = document.createElement("BUTTON");
+ button.type = "button";
+ button.appendChild(document.createTextNode("Remove"));
+ button.classList.add("btn", "btn-danger");
+ const that = this;
+ button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
+ return button;
+ }
+
+ make_input(div, node, prepopulate){
+ const input = document.createElement("INPUT");
+ input.type = node.form.type;
+ input.name = node.id + node.form.name
+ input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
+ input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
+ input.placeholder = node.form.placeholder;
+ this.inputs.push(input);
+ const that = this;
+ input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
+ input.oninput = function() { that.restrictchars(this); };
+ if(prepopulate)
+ input.value = prepopulate;
+ return input;
+ }
+
+ add_item_prepopulate(node, prepopulate){
+ const div = document.createElement("DIV");
+ div.id = "dropdown_" + this.dropdown_count;
+ div.classList.add("dropdown_item");
+ this.dropdown_count++;
+ const label = document.createElement("H5")
+ label.appendChild(document.createTextNode(node['name']))
+ div.appendChild(label);
+ div.appendChild(this.make_input(div, node, prepopulate));
+ div.appendChild(this.make_remove_button(div, node));
+ document.getElementById("dropdown_wrapper").appendChild(div);
+ return div;
+ }
+
+ remove_dropdown(div_id, node_id){
+ const div = document.getElementById(div_id);
+ const node = this.filter_items[node_id]
+ const parent = div.parentNode;
+ div.parentNode.removeChild(div);
+ delete this.result[node.class][node.id]['values'][div.id];
+
+ //checks if we have removed last item in class
+ if(jQuery.isEmptyObject(this.result[node.class][node.id]['values'])){
+ delete this.result[node.class][node.id];
+ this.clear(node);
+ }
+ }
+
+ updateResult(node){
+ if(!node['multiple']){
+ this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
+ if(!node.selected)
+ delete this.result[node.class][node.id];
+ }
+ }
+
+ updateObjectResult(node, childKey, childValue){
+ if(!this.result[node.class][node.id])
+ this.result[node.class][node.id] = {selected: true, id: node.model_id, values: {}}
+
+ this.result[node.class][node.id]['values'][childKey] = childValue;
+ }
+
+ finish(){
+ document.getElementById("filter_field").value = JSON.stringify(this.result);
+ }
+}
+
+class NetworkStep {
+ constructor(debug, xml, hosts, added_hosts, removed_host_ids, graphContainer, overviewContainer, toolbarContainer){
+ if(!this.check_support())
+ return;
+
+ this.currentWindow = null;
+ this.netCount = 0;
+ this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
+ this.hostCount = 0;
+ this.lastHostBottom = 100;
+ this.networks = new Set();
+ this.has_public_net = false;
+ this.debug = debug;
+ this.editor = new mxEditor();
+ this.graph = this.editor.graph;
+
+ this.editor.setGraphContainer(graphContainer);
+ this.doGlobalConfig();
+ this.prefill(xml, hosts, added_hosts, removed_host_ids);
+ this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
+ this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
+
+ if(this.debug){
+ this.editor.addAction('printXML', function(editor, cell) {
+ mxLog.write(this.encodeGraph());
+ mxLog.show();
+ }.bind(this));
+ this.addToolbarButton(this.editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
+ }
+
+ new mxOutline(this.graph, overviewContainer);
+ //sets the edge color to be the same as the network
+ this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
+ //hooks up double click functionality
+ this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
+
+ if(!this.has_public_net){
+ this.addPublicNetwork();
+ }
+ }
+
+ check_support(){
+ if (!mxClient.isBrowserSupported()) {
+ mxUtils.error('Browser is not supported', 200, false);
+ return false;
+ }
+ return true;
+ }
+
+ prefill(xml, hosts, added_hosts, removed_host_ids){
+ //populate existing data
+ if(xml){
+ this.restoreFromXml(xml, this.editor);
+ } else if(hosts){
+ for(const host of hosts)
+ this.makeHost(host);
+ }
+
+ //apply any changes
+ if(added_hosts){
+ for(const host of added_hosts)
+ this.makeHost(host);
+ this.updateHosts([]); //TODO: why?
+ }
+ this.updateHosts(removed_host_ids);
+ }
+
+ cellConnectionHandler(sender, event){
+ const edge = event.getProperty('edge');
+ const terminal = event.getProperty('terminal')
+ const source = event.getProperty('source');
+ if(this.checkAllowed(edge, terminal, source)) {
+ this.colorEdge(edge, terminal, source);
+ this.alertVlan(edge, terminal, source);
+ }
+ }
+
+ doubleClickHandler(evt, cell) {
+ if( cell != null ){
+ if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
+ cell = cell.getParent();
+ }
+ if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
+ this.createDeleteDialog(cell.getId());
+ }
+ else {
+ this.showDetailWindow(cell);
+ }
+ }
+ }
+
+ alertVlan(edge, terminal, source) {
+ if( terminal == null || edge.getTerminal(!source) == null) {
+ return;
+ }
+ const form = document.createElement("form");
+ const tagged = document.createElement("input");
+ tagged.type = "radio";
+ tagged.name = "tagged";
+ tagged.value = "True";
+ form.appendChild(tagged);
+ form.appendChild(document.createTextNode(" Tagged"));
+ form.appendChild(document.createElement("br"));
+
+ const untagged = document.createElement("input");
+ untagged.type = "radio";
+ untagged.name = "tagged";
+ untagged.value = "False";
+ form.appendChild(untagged);
+ form.appendChild(document.createTextNode(" Untagged"));
+ form.appendChild(document.createElement("br"));
+
+ const yes_button = document.createElement("button");
+ yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
+ yes_button.appendChild(document.createTextNode("Okay"));
+
+ const cancel_button = document.createElement("button");
+ cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
+ cancel_button.appendChild(document.createTextNode("Cancel"));
+
+ const error_div = document.createElement("div");
+ error_div.id = "current_window_errors";
+ form.appendChild(error_div);
+
+ const content = document.createElement('div');
+ content.appendChild(form);
+ content.appendChild(yes_button);
+ content.appendChild(cancel_button);
+ this.showWindow("Vlan Selection", content, 200, 200);
+ }
+
+ createDeleteDialog(id) {
+ const content = document.createElement('div');
+ const remove_button = document.createElement("button");
+ remove_button.style.width = '46%';
+ remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
+ remove_button.appendChild(document.createTextNode("Remove"));
+ const cancel_button = document.createElement("button");
+ cancel_button.style.width = '46%';
+ cancel_button.onclick = function() { this.closeWindow();}.bind(this);
+ cancel_button.appendChild(document.createTextNode("Cancel"));
+
+ content.appendChild(remove_button);
+ content.appendChild(cancel_button);
+ this.showWindow('Do you want to delete this network?', content, 200, 62);
+ }
+
+ checkAllowed(edge, terminal, source) {
+ //check if other terminal is null, and that they are different
+ const otherTerminal = edge.getTerminal(!source);
+ if(terminal != null && otherTerminal != null) {
+ if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
+ otherTerminal.getParent().getId().split('_')[0] ) {
+ //not allowed
+ this.graph.removeCells([edge]);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ colorEdge(edge, terminal, source) {
+ if(terminal.getParent().getId().indexOf('network') >= 0) {
+ const styles = terminal.getParent().getStyle().split(';');
+ let color = 'black';
+ for(let style of styles){
+ const kvp = style.split('=');
+ if(kvp[0] == "fillColor"){
+ color = kvp[1];
+ }
+ }
+ edge.setStyle('strokeColor=' + color);
+ }
+ }
+
+ showDetailWindow(cell) {
+ const info = JSON.parse(cell.getValue());
+ const content = document.createElement("div");
+ const pre_tag = document.createElement("pre");
+ pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
+ const ok_button = document.createElement("button");
+ ok_button.onclick = function() { this.closeWindow();};
+ content.appendChild(pre_tag);
+ content.appendChild(ok_button);
+ this.showWindow('Details', content, 400, 400);
+ }
+
+ restoreFromXml(xml, editor) {
+ const doc = mxUtils.parseXml(xml);
+ const node = doc.documentElement;
+ editor.readGraphModel(node);
+
+ //Iterate over all children, and parse the networks to add them to the sidebar
+ for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
+ if(cell.getId().indexOf("network") > -1) {
+ const info = JSON.parse(cell.getValue());
+ const name = info['name'];
+ this.networks.add(name);
+ const styles = cell.getStyle().split(";");
+ let color = null;
+ for(const style of styles){
+ const kvp = style.split('=');
+ if(kvp[0] == "fillColor") {
+ color = kvp[1];
+ break;
+ }
+ }
+ if(info.public){
+ this.has_public_net = true;
+ }
+ this.netCount++;
+ this.makeSidebarNetwork(name, color, cell.getId());
+ }
+ }
+ }
+
+ deleteCell(cellId) {
+ var cell = this.graph.getModel().getCell(cellId);
+ if( cellId.indexOf("network") > -1 ) {
+ let elem = document.getElementById(cellId);
+ elem.parentElement.removeChild(elem);
+ }
+ this.graph.removeCells([cell]);
+ this.currentWindow.destroy();
+ }
+
+ newNetworkWindow() {
+ const input = document.createElement("input");
+ input.type = "text";
+ input.name = "net_name";
+ input.maxlength = 100;
+ input.id = "net_name_input";
+ input.style.margin = "5px";
+
+ const yes_button = document.createElement("button");
+ yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
+ yes_button.appendChild(document.createTextNode("Okay"));
+
+ const cancel_button = document.createElement("button");
+ cancel_button.onclick = function() {this.closeWindow();}.bind(this);
+ cancel_button.appendChild(document.createTextNode("Cancel"));
+
+ const error_div = document.createElement("div");
+ error_div.id = "current_window_errors";
+
+ const content = document.createElement("div");
+ content.appendChild(document.createTextNode("Name: "));
+ content.appendChild(input);
+ content.appendChild(document.createElement("br"));
+ content.appendChild(yes_button);
+ content.appendChild(cancel_button);
+ content.appendChild(document.createElement("br"));
+ content.appendChild(error_div);
+
+ this.showWindow("Network Creation", content, 300, 300);
+ }
+
+ parseNetworkWindow() {
+ const net_name = document.getElementById("net_name_input").value
+ const error_div = document.getElementById("current_window_errors");
+ if( this.networks.has(net_name) ){
+ error_div.innerHTML = "All network names must be unique";
+ return;
+ }
+ this.addNetwork(net_name);
+ this.currentWindow.destroy();
+ }
+
+ addToolbarButton(editor, toolbar, action, label, image, isTransparent) {
+ const button = document.createElement('button');
+ button.style.fontSize = '10';
+ if (image != null) {
+ const img = document.createElement('img');
+ img.setAttribute('src', image);
+ img.style.width = '16px';
+ img.style.height = '16px';
+ img.style.verticalAlign = 'middle';
+ img.style.marginRight = '2px';
+ button.appendChild(img);
+ }
+ if (isTransparent) {
+ button.style.background = 'transparent';
+ button.style.color = '#FFFFFF';
+ button.style.border = 'none';
+ }
+ mxEvent.addListener(button, 'click', function(evt) {
+ editor.execute(action);
+ });
+ mxUtils.write(button, label);
+ toolbar.appendChild(button);
+ };
+
+ encodeGraph() {
+ const encoder = new mxCodec();
+ const xml = encoder.encode(this.graph.getModel());
+ return mxUtils.getXml(xml);
+ }
+
+ doGlobalConfig() {
+ //general graph stuff
+ this.graph.setMultigraph(false);
+ this.graph.setCellsSelectable(false);
+ this.graph.setCellsMovable(false);
+
+ //testing
+ this.graph.vertexLabelIsMovable = true;
+
+ //edge behavior
+ this.graph.setConnectable(true);
+ this.graph.setAllowDanglingEdges(false);
+ mxEdgeHandler.prototype.snapToTerminals = true;
+ mxConstants.MIN_HOTSPOT_SIZE = 16;
+ mxConstants.DEFAULT_HOTSPOT = 1;
+ //edge 'style' (still affects behavior greatly)
+ const style = this.graph.getStylesheet().getDefaultEdgeStyle();
+ style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
+ style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
+ style[mxConstants.STYLE_ROUNDED] = true;
+ style[mxConstants.STYLE_FONTCOLOR] = 'black';
+ style[mxConstants.STYLE_STROKECOLOR] = 'red';
+ style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
+ style[mxConstants.STYLE_STROKEWIDTH] = '3';
+ style[mxConstants.STYLE_ROUNDED] = true;
+ style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
+
+ const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
+ hostStyle[mxConstants.STYLE_ROUNDED] = 1;
+
+ this.graph.convertValueToString = function(cell) {
+ try{
+ //changes value for edges with xml value
+ if(cell.isEdge()) {
+ if(JSON.parse(cell.getValue())["tagged"]) {
+ return "tagged";
+ }
+ return "untagged";
+ }
+ else{
+ return JSON.parse(cell.getValue())['name'];
+ }
+ }
+ catch(e){
+ return cell.getValue();
+ }
+ };
+ }
+
+ showWindow(title, content, width, height) {
+ //create transparent black background
+ const background = document.createElement('div');
+ background.style.position = 'absolute';
+ background.style.left = '0px';
+ background.style.top = '0px';
+ background.style.right = '0px';
+ background.style.bottom = '0px';
+ background.style.background = 'black';
+ mxUtils.setOpacity(background, 50);
+ document.body.appendChild(background);
+
+ const x = Math.max(0, document.body.scrollWidth/2-width/2);
+ const y = Math.max(10, (document.body.scrollHeight ||
+ document.documentElement.scrollHeight)/2-height*2/3);
+
+ const wnd = new mxWindow(title, content, x, y, width, height, false, true);
+ wnd.setClosable(false);
+
+ wnd.addListener(mxEvent.DESTROY, function(evt) {
+ this.graph.setEnabled(true);
+ mxEffects.fadeOut(background, 50, true, 10, 30, true);
+ }.bind(this));
+ this.currentWindow = wnd;
+
+ this.graph.setEnabled(false);
+ this.currentWindow.setVisible(true);
+ };
+
+ closeWindow() {
+ //allows the current window to be destroyed
+ this.currentWindow.destroy();
+ };
+
+ othersUntagged(edgeID) {
+ const edge = this.graph.getModel().getCell(edgeID);
+ const end1 = edge.getTerminal(true);
+ const end2 = edge.getTerminal(false);
+
+ if( end1.getParent().getId().split('_')[0] == 'host' ){
+ var netint = end1;
+ } else {
+ var netint = end2;
+ }
+
+ var edges = netint.edges;
+ for( let edge of edges) {
+ if( edge.getValue() ) {
+ var tagged = JSON.parse(edge.getValue()).tagged;
+ } else {
+ var tagged = true;
+ }
+ if( !tagged ) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+
+ deleteVlanWindow(edgeID) {
+ const cell = this.graph.getModel().getCell(edgeID);
+ this.graph.removeCells([cell]);
+ this.currentWindow.destroy();
+ }
+
+ parseVlanWindow(edgeID) {
+ //do parsing and data manipulation
+ const radios = document.getElementsByName("tagged");
+ const edge = this.graph.getModel().getCell(edgeID);
+
+ for(let radio of radios){
+ if(radio.checked) {
+ //set edge to be tagged or untagged
+ if( radio.value == "False") {
+ if( this.othersUntagged(edgeID) ) {
+ document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
+ return;
+ }
+ }
+ const edgeVal = {tagged: radio.value == "True"};
+ edge.setValue(JSON.stringify(edgeVal));
+ break;
+ }
+ }
+ this.graph.refresh(edge);
+ this.closeWindow();
+ }
+
+ makeMxNetwork(net_name, is_public = false) {
+ const model = this.graph.getModel();
+ const width = 10;
+ const height = 1700;
+ const xoff = 400 + (30 * this.netCount);
+ const yoff = -10;
+ let color = this.netColors[this.netCount];
+ if( this.netCount > (this.netColors.length - 1)) {
+ color = Math.floor(Math.random() * 16777215); //int in possible color space
+ color = '#' + color.toString(16).toUpperCase(); //convert to hex
+ }
+ const net_val = { name: net_name, public: is_public};
+ const net = this.graph.insertVertex(
+ this.graph.getDefaultParent(),
+ 'network_' + this.netCount,
+ JSON.stringify(net_val),
+ xoff,
+ yoff,
+ width,
+ height,
+ 'fillColor=' + color,
+ false
+ );
+ const num_ports = 45;
+ for(var i=0; i<num_ports; i++){
+ let port = this.graph.insertVertex(
+ net,
+ null,
+ '',
+ 0,
+ (1/num_ports) * i,
+ 10,
+ height / num_ports,
+ 'fillColor=black;opacity=0',
+ true
+ );
+ }
+
+ const ret_val = { color: color, element_id: "network_" + this.netCount };
+
+ this.networks.add(net_name);
+ this.netCount++;
+ return ret_val;
+ }
+
+ addPublicNetwork() {
+ const net = this.makeMxNetwork("public", true);
+ this.makeSidebarNetwork("public", net['color'], net['element_id']);
+ this.has_public_net = true;
+ }
+
+ addNetwork(net_name) {
+ const ret = this.makeMxNetwork(net_name);
+ this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
+ }
+
+ updateHosts(removed) {
+ const cells = []
+ for(const hostID of removed) {
+ cells.push(this.graph.getModel().getCell("host_" + hostID));
+ }
+ this.graph.removeCells(cells);
+
+ const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
+ let topdist = 100;
+ for(const i in hosts) {
+ const host = hosts[i];
+ if(host.id.startsWith("host_")){
+ const geometry = host.getGeometry();
+ geometry.y = topdist + 50;
+ topdist = geometry.y + geometry.height;
+ host.setGeometry(geometry);
+ }
+ }
+ }
+
+ makeSidebarNetwork(net_name, color, net_id){
+ const newNet = document.createElement("li");
+ const colorBlob = document.createElement("div");
+ colorBlob.className = "colorblob";
+ const textContainer = document.createElement("p");
+ textContainer.className = "network_innertext";
+ newNet.id = net_id;
+ const deletebutton = document.createElement("button");
+ deletebutton.className = "btn btn-danger";
+ deletebutton.style = "float: right; height: 20px; line-height: 8px; vertical-align: middle; width: 20px; padding-left: 5px;";
+ deletebutton.appendChild(document.createTextNode("X"));
+ deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
+ textContainer.appendChild(document.createTextNode(net_name));
+ colorBlob.style['background'] = color;
+ newNet.appendChild(colorBlob);
+ newNet.appendChild(textContainer);
+ if( net_name != "public" ) {
+ newNet.appendChild(deletebutton);
+ }
+ document.getElementById("network_list").appendChild(newNet);
+ }
+
+ makeHost(hostInfo) {
+ const value = JSON.stringify(hostInfo['value']);
+ const interfaces = hostInfo['interfaces'];
+ const width = 100;
+ const height = (25 * interfaces.length) + 25;
+ const xoff = 75;
+ const yoff = this.lastHostBottom + 50;
+ this.lastHostBottom = yoff + height;
+ const host = this.graph.insertVertex(
+ this.graph.getDefaultParent(),
+ 'host_' + hostInfo['id'],
+ value,
+ xoff,
+ yoff,
+ width,
+ height,
+ 'editable=0',
+ false
+ );
+ host.getGeometry().offset = new mxPoint(-50,0);
+ host.setConnectable(false);
+ this.hostCount++;
+
+ for(var i=0; i<interfaces.length; i++) {
+ const port = this.graph.insertVertex(
+ host,
+ null,
+ JSON.stringify(interfaces[i]),
+ 90,
+ (i * 25) + 12,
+ 20,
+ 20,
+ 'fillColor=blue;editable=0',
+ false
+ );
+ port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
+ this.graph.refresh(port);
+ }
+ this.graph.refresh(host);
+ }
+
+ submitForm() {
+ const form = document.getElementById("xml_form");
+ const input_elem = document.getElementById("hidden_xml_input");
+ input_elem.value = this.encodeGraph(this.graph);
+ const req = new XMLHttpRequest();
+ req.open("POST", "/wf/workflow/", false);
+ req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ req.onerror = function() { alert("problem with form submission"); }
+ const formData = $("#xml_form").serialize();
+ req.send(formData);
+ }
+}
+
+class SearchableSelectMultipleWidget {
+ constructor(format_vars, field_dataset, field_initial) {
+ this.format_vars = format_vars;
+ this.items = field_dataset;
+ this.initial = field_initial;
+
+ this.expanded_name_trie = {"isComplete": false};
+ this.small_name_trie = {"isComplete": false};
+ this.string_trie = {"isComplete": false};
+
+ this.added_items = new Set();
+
+ for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
+ {
+ this[e] = format_vars[e];
+ }
+
+ this.search_field_init();
+
+ if( this.show_from_noentry )
+ {
+ this.search("");
+ }
+ }
+
+ disable() {
+ const textfield = document.getElementById("user_field");
+ const drop = document.getElementById("drop_results");
+
+ textfield.disabled = "True";
+ drop.style.display = "none";
+
+ const btns = document.getElementsByClassName("btn-remove");
+ for( const btn of btns )
+ {
+ btn.classList.add("disabled");
+ btn.onclick = "";
+ }
+ }
+
+ search_field_init() {
+ this.build_all_tries(this.items);
+
+ for( const elem of this.initial )
+ {
+ this.select_item(elem);
+ }
+ if(this.initial.length == 1)
+ {
+ this.search(this.items[this.initial[0]]["small_name"]);
+ document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
+ }
+ }
+
+ build_all_tries(dict)
+ {
+ for( const key in dict )
+ {
+ this.add_item(dict[key]);
+ }
+ }
+
+ add_item(item)
+ {
+ const id = item['id'];
+ this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
+ this.add_to_tree(item['small_name'], id, this.small_name_trie);
+ this.add_to_tree(item['string'], id, this.string_trie);
+ }
+
+ add_to_tree(str, id, trie)
+ {
+ let inner_trie = trie;
+ while( str )
+ {
+ if( !inner_trie[str.charAt(0)] )
+ {
+ var new_trie = {};
+ inner_trie[str.charAt(0)] = new_trie;
+ }
+ else
+ {
+ var new_trie = inner_trie[str.charAt(0)];
+ }
+
+ if( str.length == 1 )
+ {
+ new_trie.isComplete = true;
+ if( !new_trie.ids )
+ {
+ new_trie.ids = [];
+ }
+ new_trie.ids.push(id);
+ }
+ inner_trie = new_trie;
+ str = str.substring(1);
+ }
+ }
+
+ search(input)
+ {
+ if( input.length == 0 && !this.show_from_noentry){
+ this.dropdown([]);
+ return;
+ }
+ else if( input.length == 0 && this.show_from_noentry)
+ {
+ this.dropdown(this.items); //show all items
+ }
+ else
+ {
+ const trees = []
+ const tr1 = this.getSubtree(input, this.expanded_name_trie);
+ trees.push(tr1);
+ const tr2 = this.getSubtree(input, this.small_name_trie);
+ trees.push(tr2);
+ const tr3 = this.getSubtree(input, this.string_trie);
+ trees.push(tr3);
+ const results = this.collate(trees);
+ this.dropdown(results);
+ }
+ }
+
+ getSubtree(input, given_trie)
+ {
+ /*
+ recursive function to return the trie accessed at input
+ */
+
+ if( input.length == 0 ){
+ return given_trie;
+ }
+
+ else{
+ const substr = input.substring(0, input.length - 1);
+ const last_char = input.charAt(input.length-1);
+ const subtrie = this.getSubtree(substr, given_trie);
+
+ if( !subtrie ) //substr not in the trie
+ {
+ return {};
+ }
+
+ const indexed_trie = subtrie[last_char];
+ return indexed_trie;
+ }
+ }
+
+ serialize(trie)
+ {
+ /*
+ takes in a trie and returns a list of its item id's
+ */
+ let itemIDs = [];
+ if ( !trie )
+ {
+ return itemIDs; //empty, base case
+ }
+ for( const key in trie )
+ {
+ if(key.length > 1)
+ {
+ continue;
+ }
+ itemIDs = itemIDs.concat(this.serialize(trie[key]));
+ }
+ if ( trie.isComplete )
+ {
+ itemIDs.push(...trie.ids);
+ }
+
+ return itemIDs;
+ }
+
+ collate(trees)
+ {
+ /*
+ takes a list of tries
+ returns a list of ids of objects that are available
+ */
+ const results = [];
+ for( const tree of trees )
+ {
+ const available_IDs = this.serialize(tree);
+
+ for( const itemID of available_IDs ) {
+ results[itemID] = this.items[itemID];
+ }
+ }
+ return results;
+ }
+
+ generate_element_text(obj)
+ {
+ const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
+ const result = content_strings.shift();
+ if( result == null || content_strings.length < 1) {
+ return result;
+ } else {
+ return result + " (" + content_strings.join(", ") + ")";
+ }
+ }
+
+ dropdown(ids)
+ {
+ /*
+ takes in a mapping of ids to objects in items
+ and displays them in the dropdown
+ */
+ const drop = document.getElementById("drop_results");
+ while(drop.firstChild)
+ {
+ drop.removeChild(drop.firstChild);
+ }
+
+ for( const id in ids )
+ {
+ const result_entry = document.createElement("li");
+ const result_button = document.createElement("a");
+ const obj = this.items[id];
+ const result_text = this.generate_element_text(obj);
+ result_button.appendChild(document.createTextNode(result_text));
+ result_button.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
+ const tooltip = document.createElement("span");
+ const tooltiptext = document.createTextNode(result_text);
+ tooltip.appendChild(tooltiptext);
+ tooltip.setAttribute('class', 'entry_tooltip');
+ result_button.appendChild(tooltip);
+ result_entry.appendChild(result_button);
+ drop.appendChild(result_entry);
+ }
+
+ const scroll_restrictor = document.getElementById("scroll_restrictor");
+
+ if( !drop.firstChild )
+ {
+ scroll_restrictor.style.visibility = 'hidden';
+ }
+ else
+ {
+ scroll_restrictor.style.visibility = 'inherit';
+ }
+ }
+
+ select_item(item_id)
+ {
+ if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
+ {
+ this.added_items.add(item_id);
+ }
+ this.update_selected_list();
+ // clear search bar contents
+ document.getElementById("user_field").value = "";
+ document.getElementById("user_field").focus();
+ this.search("");
+ }
+
+ remove_item(item_id)
+ {
+ this.added_items.delete(item_id);
+
+ this.update_selected_list()
+ document.getElementById("user_field").focus();
+ }
+
+ update_selected_list()
+ {
+ document.getElementById("added_number").innerText = this.added_items.size;
+ const selector = document.getElementById('selector');
+ selector.value = JSON.stringify([...this.added_items]);
+ const added_list = document.getElementById('added_list');
+
+ while(selector.firstChild)
+ {
+ selector.removeChild(selector.firstChild);
+ }
+ while(added_list.firstChild)
+ {
+ added_list.removeChild(added_list.firstChild);
+ }
+
+ let list_html = "";
+
+ for( const item_id of this.added_items )
+ {
+ const item = this.items[item_id];
+
+ const element_entry_text = this.generate_element_text(item);
+
+ list_html += '<div class="list_entry">'
+ + '<p class="added_entry_text">'
+ + element_entry_text
+ + '</p>'
+ + '<button onclick="searchable_select_multiple_widget.remove_item('
+ + item_id
+ + ')" class="btn-remove btn">remove</button>';
+ list_html += '</div>';
+ }
+ added_list.innerHTML = list_html;
+ }
+}
diff --git a/dashboard/src/templates/account/booking_list.html b/dashboard/src/templates/account/booking_list.html
index e56b19e..98ab5c8 100644
--- a/dashboard/src/templates/account/booking_list.html
+++ b/dashboard/src/templates/account/booking_list.html
@@ -3,20 +3,22 @@
<h2>Bookings I Own</h2>
<div class="card_container">
{% for booking in bookings %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Booking {{booking.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{booking.id}}</li>
- <li class="list-group-item">lab: {{booking.resource.template.lab.lab_user.username}}</li>
+ <li class="list-group-item">lab: {{booking.lab}}</li>
<li class="list-group-item">resource: {{booking.resource.template.name}}</li>
<li class="list-group-item">start: {{booking.start}}</li>
<li class="list-group-item">end: {{booking.end}}</li>
<li class="list-group-item">purpose: {{booking.purpose}}</li>
</ul>
</div>
- <div class="detail_button_container">
- <a class="btn btn-primary" href="/booking/detail/{{booking.id}}/">Details</a>
+ <div class="card-footer d-flex">
+ <a class="btn btn-primary ml-auto mr-2" href="/booking/detail/{{booking.id}}/">Details</a>
<button
class="btn btn-danger"
onclick='cancel_booking({{booking.id}});'
@@ -29,24 +31,53 @@
</div>
<h2>Bookings I Collaborate On</h2>
<div class="card_container">
- {% for booking in collab_bookings %}
- <div class="detail_card">
- <div>
+ {% for booking in collab_bookings %}
+ <div class="card">
+ <div class="card-header">
+ <h3>Booking {{booking.id}}</h3>
+ </div>
+ <div class="card-body">
+ <ul class="list-group">
+ <li class="list-group-item">id: {{booking.id}}</li>
+ <li class="list-group-item">lab: {{booking.lab}}</li>
+ <li class="list-group-item">resource: {{booking.resource.template.name}}</li>
+ <li class="list-group-item">start: {{booking.start}}</li>
+ <li class="list-group-item">end: {{booking.end}}</li>
+ <li class="list-group-item">purpose: {{booking.purpose}}</li>
+ </ul>
+ </div>
+ <div class="card-footer d-flex">
+ <a class="btn btn-primary ml-auto" href="/booking/detail/{{booking.id}}/">Details</a>
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+ <h2>Expired Bookings
+ <i class="fa fa-fw fa-caret-down" onclick='toggle_display("expired_bookings");'></i>
+ </h2>
+ <div id="expired_bookings" class="card_container" style="display:none;">
+ {% for booking in expired_bookings %}
+ <div class="card">
+ <div class="card-header">
<h3>Booking {{booking.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{booking.id}}</li>
<li class="list-group-item">lab: {{booking.lab}}</li>
- <li class="list-group-item">resource: {{booking.resource_name}}</li>
+ <li class="list-group-item">resource: {{booking.resource.template.name}}</li>
<li class="list-group-item">start: {{booking.start}}</li>
<li class="list-group-item">end: {{booking.end}}</li>
<li class="list-group-item">purpose: {{booking.purpose}}</li>
+ <li class="list-group-item">owner: {{booking.owner.userprofile.email_addr}}</li>
</ul>
</div>
- <a class="btn btn-primary" href="/booking/detail/{{booking.id}}/">Details</a>
+ <div class="card-footer d-flex">
+ <a class="btn btn-primary ml-auto" href="/booking/detail/{{booking.id}}/">Details</a>
+ </div>
</div>
{% endfor %}
</div>
-
<script>
var current_booking_id = -1;
function cancel_booking(booking_id) {
@@ -64,6 +95,15 @@
req.onerror = function() { alert("problem submitting form"); }
req.send(formData);
}
+
+ function toggle_display(elem_id){
+ var e = document.getElementById(elem_id);
+ if (e.style.display === "none"){
+ e.style.display = "grid";
+ } else {
+ e.style.display = "none";
+ }
+ }
</script>
<div class="modal fade" id="resModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true">
<div class="modal-dialog" style="width: 450px;" role="document">
diff --git a/dashboard/src/templates/account/configuration_list.html b/dashboard/src/templates/account/configuration_list.html
index b920ba6..6f7844a 100644
--- a/dashboard/src/templates/account/configuration_list.html
+++ b/dashboard/src/templates/account/configuration_list.html
@@ -2,9 +2,11 @@
{% block content %}
<div class="card_container">
{% for config in configurations %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Configuration {{config.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{config.id}}</li>
<li class="list-group-item">name: {{config.name}}</li>
@@ -12,10 +14,9 @@
<li class="list-group-item">resource: {{config.bundle}}</li>
</ul>
</div>
- <div class="detail_button_container">
+ <div class="card-footer">
<button
- class="btn btn-danger"
- style="width:49%;float:right;"
+ class="btn btn-danger w-100"
onclick='delete_config({{config.id}});'
data-toggle="modal"
data-target="#configModal"
diff --git a/dashboard/src/templates/account/image_list.html b/dashboard/src/templates/account/image_list.html
index cd83dcf..068e096 100644
--- a/dashboard/src/templates/account/image_list.html
+++ b/dashboard/src/templates/account/image_list.html
@@ -3,9 +3,11 @@
<h2>Images I Own</h2>
<div class="card_container">
{% for image in images %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Image {{image.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{image.id}}</li>
<li class="list-group-item">lab: {{image.from_lab.name}}</li>
@@ -14,10 +16,9 @@
<li class="list-group-item">host profile: {{image.host_type.name}}</li>
</ul>
</div>
- <div class="detail_button_container">
+ <div class="card-footer">
<button
- class="btn btn-danger"
- style="width:49%;float:right;"
+ class="btn btn-danger w-100"
onclick='delete_image({{image.id}});'
data-toggle="modal"
data-target="#imageModal"
@@ -29,9 +30,11 @@
<h2>Public Images</h2>
<div class="card_container">
{% for image in public_images %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Image {{image.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{image.id}}</li>
<li class="list-group-item">lab: {{image.from_lab.name}}</li>
diff --git a/dashboard/src/templates/account/resource_list.html b/dashboard/src/templates/account/resource_list.html
index 1391e8e..f92f78e 100644
--- a/dashboard/src/templates/account/resource_list.html
+++ b/dashboard/src/templates/account/resource_list.html
@@ -2,18 +2,20 @@
{% block content %}
<div class="card_container">
{% for resource in resources %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Resource {{resource.id}}</h3>
+ </div>
+ <div class="card-body p-4">
<ul class="list-group">
<li class="list-group-item">id: {{resource.id}}</li>
<li class="list-group-item">name: {{resource.name}}</li>
<li class="list-group-item">description: {{resource.description}}</li>
</ul>
</div>
- <div class="detail_button_container">
+ <div class="card-footer">
<button
- class="btn btn-danger"
+ class="btn btn-danger w-100"
onclick='delete_resource({{resource.id}});'
data-toggle="modal"
data-target="#resModal"
diff --git a/dashboard/src/templates/account/userprofile_update_form.html b/dashboard/src/templates/account/userprofile_update_form.html
index f1d2852..6ab8242 100644
--- a/dashboard/src/templates/account/userprofile_update_form.html
+++ b/dashboard/src/templates/account/userprofile_update_form.html
@@ -1,17 +1,12 @@
-{% extends "layout.html" %}
-{% load bootstrap3 %}
+{% extends "base.html" %}
+{% load bootstrap4 %}
-{% block basecontent %}
- <div class="container">
+{% block content %}
+ <div class="container-fluid">
<div class="row">
- <div class="col-md-4 col-md-offset-4">
+ <div class="col-12 col-xl-6">
{% bootstrap_messages %}
<div class="login-panel panel panel-default">
- <div class="panel-heading">
- <h3 class="panel-title">
- {{ title }}
- </h3>
- </div>
<div class="panel-body">
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
@@ -35,4 +30,4 @@
</div>
</div>
</div>
-{% endblock basecontent %}
+{% endblock content %}
diff --git a/dashboard/src/templates/base.html b/dashboard/src/templates/base.html
index 02c67dc..62a9ed5 100644
--- a/dashboard/src/templates/base.html
+++ b/dashboard/src/templates/base.html
@@ -1,52 +1,51 @@
{% extends "layout.html" %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% load staticfiles %}
{% block extrahead %}
- <!-- Custom CSS -->
- <link href="{% static "bower_components/startbootstrap-sb-admin-2-blackrockdigital/dist/css/sb-admin-2.min.css" %}"
- rel="stylesheet">
- <link href="{% static "css/theme.css" %}" rel="stylesheet">
- <link href="{% static "css/detail_view.css" %}" rel="stylesheet">
-
+<!-- Custom CSS -->
+<link href="{% static "css/detail_view.css" %}" rel="stylesheet">
+<link href="{% static "css/base.css" %}" rel="stylesheet">
<script type="text/javascript">
- function cwf(type)
- {
+ function cwf(type) {
$.ajax({
type: "POST",
url: "/",
- data: {"create":type},
- beforeSend: function(request) {
+ data: {
+ "create": type
+ },
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val()
+ $('input[name="csrfmiddlewaretoken"]').val()
);
}
}).done(function (data) {
window.location.replace("/wf/");
- }).fail(function(jqxHR, textstatus) {
- alert("Something went wrong...");});
+ }).fail(function (jqxHR, textstatus) {
+ alert("Something went wrong...");
+ });
}
- function continue_wf()
- {
+
+ function continue_wf() {
window.location.replace("/wf/");
}
- function toggle_create_drop()
- {
+ function toggle_create_drop() {
drop_div = document.getElementById("create_drop");
- if (drop_div.style.display === "none")
- {
+ if (drop_div.style.display === "none") {
drop_div.style.display = "inherit";
- }
- else
- {
+ } else {
drop_div.style.display = "none";
}
}
</script>
<style>
+ .navbar {
+ min-width: 200px;
+ }
+
.create_drop {
display: none;
width: 100%;
@@ -66,136 +65,172 @@
border-top: 1px solid #E7E7E7;
border-bottom: 1px solid #E7E7E7;
}
+
+ #wrapper {
+ height: 100vh;
+ }
</style>
{% endblock %}
{% block basecontent %}
- <div id="wrapper">
- <!-- Navigation -->
- <nav class="navbar navbar-default navbar-static-top" role="navigation"
- style="margin-bottom: 0">
- <div class="navbar-header">
- <button type="button" class="navbar-toggle" data-toggle="collapse"
- data-target=".navbar-collapse">
- <span class="sr-only">Toggle navigation</span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- <a href="https://www.opnfv.org/" class="navbar-left"><img
- src="{% static "img/opnfv-logo.png" %}"></a>
- <a class="navbar-brand" href={% url 'dashboard:index' %}>Pharos Dashboard</a>
+<div id="wrapper" class="d-flex flex-column">
+ <!-- Navigation -->
+ <nav class="navbar navbar-light bg-light navbar-fixed-top border-bottom py-0" role="navigation" style="margin-bottom: 0">
+ <div class="container-fluid">
+ <div class="col order-2 order-lg-1 text-center text-lg-left">
+ <a href="https://www.opnfv.org/" class="navbar-brand">
+ <img src="{% static "img/opnfv-logo.png" %}">
+ </a>
+ <a class="navbar-brand" href={% url 'dashboard:index' %}>
+ Pharos Dashboard
+ </a>
</div>
<!-- /.navbar-header -->
+ <div class="col-2 order-1 order-lg-3 d-lg-none">
+ <button class="btn border" type="button" data-toggle="collapse" data-target="#sidebar"
+ aria-expanded="false" aria-controls="sidebar">
+ <i class="fas fa-bars"></i>
+ </button>
+ </div>
+ <div class="col-2 order-3">
+ <ul class="nav ml-auto">
+ <li class="dropdown ml-auto">
+ <a class="nav-link p-0 text-dark p-2" data-toggle="dropdown" href="#">
+ {% if request.user.username %}
+ {{request.user.username}}
+ {% else %}
+ <i class="fas fa-user"></i>
+ {% endif %}
+ <i class="fas fa-caret-down rotate"></i>
+ </a>
+ <div class="dropdown-menu dropdown-menu-right">
+ {% if user.is_authenticated %}
+ <a href="{% url 'account:settings' %}" class="text-dark dropdown-item">
+ <i class="fas fa-cog"></i>
+ Settings
+ </a>
+ <a href="{% url 'account:logout' %}?next={{ request.path }}" class="text-dark dropdown-item">
+ <i class="fas fa-sign-out-alt"></i>
+ Logout
+ </a>
+ {% else %}
+ <a href="{% url 'account:login' %}" class="text-dark dropdown-item">
+ <i class="fas fa-sign-in-alt"></i>
+ Login with Jira
+ </a>
+ {% endif %}
+ </div> <!-- End dropdown-menu -->
+ </li> <!-- End dropdown -->
+ </ul>
+ </div> <!-- End top right account menu -->
+ </div>
+ </nav>
+ <!-- /.navbar-top-links -->
- <ul class="nav navbar-top-links navbar-right">
- <li class="dropdown">
- <a class="dropdown-toggle" data-toggle="dropdown" href="#">
- <i class="fa fa-user fa-fw"></i> <i class="fa fa-caret-down"></i>
- </a>
- <ul class="dropdown-menu dropdown-user">
- {% if user.is_authenticated %}
- <li><a href="{% url 'account:settings' %}"><i
- class="fa fa-gear fa-fw"></i>
- Settings</a>
- </li>
- <li class="divider"></li>
- <li><a href="{% url 'account:logout' %}?next={{ request.path }}"><i
- class="fa fa-sign-out fa-fw"></i>
- Logout</a>
- </li>
- {% else %}
- <li><a href="{% url 'account:login' %}"><i
- class="fa fa-sign-in fa-fw"></i>
- Login with Jira</a>
- <li>
- {% endif %}
- </ul>
- <!-- /.dropdown-user -->
- </li>
- <!-- /.dropdown -->
- </ul>
- <!-- /.navbar-top-links -->
-
- <div class="navbar-default sidebar" role="navigation">
- <div class="sidebar-nav navbar-collapse">
- <ul class="nav" id="side-menu">
- <li>
- <a href="/"><i class="fa fa-fw"></i>Home</a>
- </li>
- <li style="width: 100%;">
- <a href="javascript:toggle_create_drop();"><i class="fa fa-fw"></i>Create<i
- class="fa fa-fw fa-caret-down"></i>
+ <!-- Page Content -->
+ <div class="container-fluid d-lg-flex flex-lg-grow-1 px-0">
+ <div class="row h-100 w-100 mx-0">
+ <div class="col-12 col-lg-auto px-0 border-right border-left bg-light" role="navigation">
+ <nav class="navbar navbar-expand-lg border-bottom p-0 w-100">
+ <div class="collapse navbar-collapse" id="sidebar">
+ <div class="list-group list-group-flush w-100 bg-light">
+ <a href="/" class="list-group-item list-group-item-action bg-light">
+ Home
+ </a>
+ {% csrf_token %}
+ <a class="list-group-item list-group-item-action bg-light" data-toggle="collapse"
+ href="#createList" role="button">
+ Create <i class="fas fa-angle-down rotate"></i>
+ </a>
+ <div class="collapse" id="createList">
+ <a href="/booking/quick/" class="list-group-item list-group-item-action list-group-item-secondary">
+ Express Booking
+ </a>
+ <a href="#" onclick="cwf(0)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Book a Pod
+ </a>
+ <a href="#" onclick="cwf(1)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Design a Pod
+ </a>
+ <a href="#" onclick="cwf(2)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Configure a Pod
+ </a>
+ <a href="#" onclick="cwf(3)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Create a Snapshot
</a>
- {% csrf_token %}
- <div id="create_drop" class="create_drop" style="display:none">
- <button class="btn drop_btn" onclick="location.href='/booking/quick/'">Express Booking</a>
- <button class="btn drop_btn" onclick="cwf(0)">Book a Pod</button>
- <button class="btn drop_btn" onclick="cwf(1)">Design a Pod</button>
- <button class="btn drop_btn" onclick="cwf(2)">Configure a Pod</button>
- <button class="btn drop_btn" onclick="cwf(3)">Create a Snapshot</button>
- </div>
- </li>
- <li>
- <a href="{% url 'resource:hosts' %}"><i
- class="fa fa-fw"></i>Hosts
+ <a href="#" onclick="cwf(4)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Configure OPNFV
</a>
- </li>
- {% if user.is_authenticated %}
- <li>
- <a href="{% url 'account:users' %}"><i
- class="fa fa-fw"></i>User List
+ </div>
+ <a href="{% url 'resource:hosts' %}" class="list-group-item list-group-item-action bg-light">
+ Hosts
</a>
- </li>
- {% endif %}
- <li>
- <a href="{% url 'booking:list' %}"><i
- class="fa fa-fw"></i>Booking List
+ {% if user.is_authenticated %}
+ <a href="{% url 'account:users' %}" class="list-group-item list-group-item-action bg-light">
+ User List
+ </a>
+ {% endif %}
+ <a href="{% url 'booking:list' %}" class="list-group-item list-group-item-action bg-light">
+ Booking List
</a>
- </li>
- <li>
- <a href="{% url 'booking:stats' %}"><i
- class="fa fa-fw"></i>Booking Statistics</a>
- </li>
- <li>
- <a href="{% url 'account:my-account' %}"><i
- class="fa fa-fw"></i>Account
+ <a href="{% url 'booking:stats' %}" class="list-group-item list-group-item-action bg-light">
+ Booking Statistics
</a>
- </li>
- <li>
- <a href="{% url 'dashboard:all_labs' %}"><i
- class="fa fa-fw"></i>Lab Info
+ <!-- <a href="{% url 'account:my-account' %}" class="list-group-item list-group-item-action bg-light">
+ Account
+ </a> -->
+ <a class="list-group-item list-group-item-action bg-light" data-toggle="collapse"
+ href="#accountList" role="button">
+ Account <i class="fas fa-angle-down rotate"></i>
</a>
- </li>
- <li>
- <a href="{% url 'notifier:messages' %}"><i
- class="fa fa-fw"></i>Inbox
+ <div class="collapse" id="accountList">
+ <a href="{% url 'account:my-resources' %}" class="list-group-item list-group-item-action list-group-item-secondary">
+ My Resources
+ </a>
+ <a href="{% url 'account:my-bookings' %}" class="list-group-item list-group-item-action list-group-item-secondary">
+ My Bookings
+ </a>
+ <a href="{% url 'account:my-configurations' %}" class="list-group-item list-group-item-action list-group-item-secondary">
+ My Configurations
+ </a>
+ <a href="{% url 'account:my-images' %}" class="list-group-item list-group-item-action list-group-item-secondary">
+ My Snapshots
+ </a>
+ </div>
+ <a href="{% url 'dashboard:all_labs' %}" class="list-group-item list-group-item-action bg-light">
+ Lab Info
</a>
- </li>
- </ul>
- </div>
- <!-- /.sidebar-collapse -->
+ <a href="{% url 'notifier:messages' %}" class="list-group-item list-group-item-action bg-light">
+ Inbox
+ </a>
+ </div>
+ </div>
+ </nav>
+ <!--/.well -->
</div>
- <!-- /.navbar-static-side -->
- </nav>
+ <!--/span-->
- <!-- Page Content -->
- <div id="page-wrapper">
- {% if title %}
- <div class="row">
- <div class="col-lg-12">
- <h1 class="page-header">{{ title }}</h1>
+ <div class="col flex-grow-1 d-flex flex-column">
+ {% if title %}
+ <div class="row flex-shrink-1">
+ <div class="col-lg-12">
+ <h1 class="page-header">{{ title }}</h1>
+ </div>
+ <!-- /.col-lg-12 -->
</div>
- <!-- /.col-lg-12 -->
+ {% endif %}
+ <div id="bsm">{% bootstrap_messages %}</div>
+ <!-- Content block placed here -->
+ {% block content %}
+ {% endblock content %}
</div>
- {% endif %}
- <div id="bsm">{% bootstrap_messages %}</div>
+ <!--/span-->
- {% block content %}
- {% endblock content %}
</div>
- <!-- /#page-wrapper -->
+ <!--/row-->
</div>
- <!-- /#wrapper -->
+ <!-- /#page-wrapper -->
+</div>
+<!-- /#wrapper -->
{% endblock basecontent %}
diff --git a/dashboard/src/templates/booking/booking_calendar.html b/dashboard/src/templates/booking/booking_calendar.html
index 1b29dc2..ddcb45d 100644
--- a/dashboard/src/templates/booking/booking_calendar.html
+++ b/dashboard/src/templates/booking/booking_calendar.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block extrahead %}
{{ block.super }}
diff --git a/dashboard/src/templates/booking/booking_delete.html b/dashboard/src/templates/booking/booking_delete.html
index 76a5634..b89eb15 100644
--- a/dashboard/src/templates/booking/booking_delete.html
+++ b/dashboard/src/templates/booking/booking_delete.html
@@ -1,5 +1,5 @@
{% load jira_filters %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
<p>
Really delete Booking from {{ booking.start}} to {{ booking.end }}?
diff --git a/dashboard/src/templates/booking/booking_detail.html b/dashboard/src/templates/booking/booking_detail.html
index 44fca98..918f5af 100644
--- a/dashboard/src/templates/booking/booking_detail.html
+++ b/dashboard/src/templates/booking/booking_detail.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block extrahead %}
{{block.super}}
@@ -19,13 +19,13 @@
<div class="container-fluid">
<div class="row">
- <div class="col-lg-4">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="col-12 col-lg-5">
+ <div class="card mb-4">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Overview</h4>
- <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;">Expand</a>
+ <button data-toggle="collapse" data-target="#panel_overview" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body" id="panel_overview">
+ <div class="card-body collapse show" id="panel_overview">
<table class="table">
<tr>
<td>Purpose</td>
@@ -60,12 +60,12 @@
</div>
<div class="row">
<div class="col-lg-12">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="card">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Pod</h4>
- <a data-toggle="collapse" data-target="#pod_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#pod_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body pod_panel" id="pod_panel">
+ <div class="card-body collapse show" id="pod_panel">
<table class="table">
{% for host in booking.resource.hosts.all %}
<tr>
@@ -177,14 +177,14 @@
</div>
</div>
</div>
- <div class="col-lg-8">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="col">
+ <div class="card mb-4">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Deployment Progress</h4>
<p style="display: inline; margin-left: 10px;"> These are the different tasks that have to be completed before your deployment is ready</p>
- <a data-toggle="collapse" data-target="#panel_tasks" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#panel_tasks" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body" id="panel_tasks">
+ <div class="card-body collapse show" id="panel_tasks">
<table class="table">
<style>
.progress {
@@ -269,15 +269,15 @@
</div>
</div>
<div class="row">
- <div class="col-lg-8">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="col">
+ <div class="card">
+ <div class="card-header d-flex">
<h4 style="display: inline;">PDF</h4>
- <a data-toggle="collapse" data-target="#pdf_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#pdf_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body" id="pdf_panel" style="padding: 0px;">
+ <div class="card-body collapse show" id="pdf_panel" style="padding: 0px;">
<pre class="prettyprint lang-yaml" style="margin: 0px; padding: 15px; border: none;">
-{{pdf}}
+ {{pdf}}
</pre>
</div>
</div>
diff --git a/dashboard/src/templates/booking/booking_list.html b/dashboard/src/templates/booking/booking_list.html
index a245450..591ecc9 100644
--- a/dashboard/src/templates/booking/booking_list.html
+++ b/dashboard/src/templates/booking/booking_list.html
@@ -1,44 +1,38 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block extrahead %}
{{ block.super }}
<!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
rel="stylesheet">
<!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
rel="stylesheet">
{% endblock extrahead %}
{% block content %}
<div class="row">
- <div class="panel-body">
- <div class="dataTables_wrapper">
- <table class="table table-striped table-bordered table-hover" id="table"
- cellspacing="0"
- width="100%">
- {% include "booking/booking_table.html" %}
- </table>
- </div>
- <!-- /.table-responsive -->
- <!-- /.panel-body -->
- <!-- /.panel -->
+ <div class="col">
+ <div class="panel-body">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover" id="table"
+ cellspacing="0"
+ width="100%">
+ {% include "booking/booking_table.html" %}
+ </table>
+ </div>
+ </div>
</div>
- <!-- /.col-lg-12 -->
</div>
{% endblock content %}
{% block extrajs %}
<!-- DataTables JavaScript -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
- rel="stylesheet">
-
-
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
<script type="text/javascript">
$(document).ready(function () {
diff --git a/dashboard/src/templates/booking/booking_table.html b/dashboard/src/templates/booking/booking_table.html
index 5e82645..32a0146 100644
--- a/dashboard/src/templates/booking/booking_table.html
+++ b/dashboard/src/templates/booking/booking_table.html
@@ -5,11 +5,10 @@
<tr>
<th>Owner</th>
<th>Purpose</th>
+ <th>Project</th>
<th>Start</th>
<th>End</th>
<th>Operating System</th>
- <th>Installer</th>
- <th>Scenario</th>
</tr>
</thead>
<tbody>
@@ -22,19 +21,16 @@
{{ booking.purpose }}
</td>
<td>
- {{ booking.start }}
- </td>
- <td>
- {{ booking.end }}
+ {{ booking.project }}
</td>
<td>
- {{ booking.opsys }}
+ {{ booking.start }}
</td>
<td>
- {{ booking.installer }}
+ {{ booking.end }}
</td>
<td>
- {{ booking.scenario }}
+ {{ booking.resource.get_head_node.config.image.os.name }}
</td>
</tr>
{% endfor %}
diff --git a/dashboard/src/templates/booking/quick_deploy.html b/dashboard/src/templates/booking/quick_deploy.html
index 3837315..07f3d89 100644
--- a/dashboard/src/templates/booking/quick_deploy.html
+++ b/dashboard/src/templates/booking/quick_deploy.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
<style>
.grid_container {
@@ -9,8 +9,8 @@
padding: 30px;
}
.grid_element {
- border-radius: 3px;
- box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75);
+ border-radius: 5px;
+ border: 1px solid #ccc;
margin: 10px;
padding: 7px;
}
@@ -26,6 +26,23 @@
.grid_element_2third {
grid-column-start: span 8;
}
+ .collaborator_pane {
+ display: flex;
+ flex-direction: column;
+ }
+ #id_length {
+ -moz-appearance: none;
+ border: none;
+ box-shadow: none;
+ }
+
+ input[type=range]::-moz-range-track {
+ background: #cccccc;
+ }
+
+ .grid_element {
+ overflow: hidden;
+ }
</style>
{% bootstrap_form_errors form type='non_fields' %}
<form id="quick_booking_form" action="/booking/quick/" method="POST" class="form">
@@ -33,7 +50,7 @@
<div class="grid_container">
<div class="grid_element host_select_pane grid_element_wide">
<p>Please select a host type you wish to book. Only available types are shown.</p>
-{% bootstrap_field form.filter_field %}
+{% bootstrap_field form.filter_field show_label=False %}
</div>
<div class="grid_element booking_info_pane grid_element_1third">
{% bootstrap_field form.purpose %}
@@ -49,41 +66,49 @@
<label>Collaborators</label>
{{ form.users }}
</div>
-<div class="grid_element configuration_pane grid_element_1third">
- {% bootstrap_field form.hostname %}
- {% bootstrap_field form.image %}
- {% bootstrap_field form.installer %}
- {% bootstrap_field form.scenario %}
+<div class="grid_element_1third">
+ <div class="configuration_pane grid_element">
+ {% bootstrap_field form.hostname %}
+ {% bootstrap_field form.image %}
+ </div>
+ <div class="configuration_pane grid_element">
+ <strong>OPNFV: (Optional)</strong>
+ {% bootstrap_field form.installer %}
+ {% bootstrap_field form.scenario %}
+ </div>
</div>
</div>
<script type="text/javascript">
- var normalize = function(data)
+
+ function submit_form()
{
- //converts the top level keys in data to map to lists
- var normalized = {}
- for( var key in data ){
- normalized[key] = [];
- for( var subkey in data[key] ){
- normalized[key].push(data[key][subkey]);
- }
- }
- return normalized;
+ //formats data for form submission
+ multi_filter_widget.finish();
}
- var update_page_contents = function(response)
- {
- document.open();
- document.write(response);
- document.close();
+
+ function hide_dropdown(drop_id) {
+ var drop = document.getElementById(drop_id);
+ //select 'blank' option
+ for( var i=0; i < drop.length; i++ )
+ {
+ if ( drop.options[i].text == '---------' )
+ drop.selectedIndex = i;
+ }
+
+ //cross browser hide children
+ $('#id_image').children().hide();
+ for( var i = 0; i < drop.childNodes.length; i++ )
+ {
+ drop.childNodes[i].disabled = true; // closest we can get on safari to hiding it outright
+ }
}
- //form hamdler code
- submit_form = function()
- {
- //altered from initial prototype: form submits automatically,
- //but needs formatting for multiple select field
- var data = normalize(result);
- data = JSON.stringify(data);
- document.getElementById("filter_field").value = data;
+ function get_selected_value(key){
+ for( var attr in multi_filter_widget.result[key] ){
+ if(!(attr in {}) )
+ return attr;
+ }
+ return null;
}
var sup_image_dict = {{ image_filter|safe }};
@@ -91,46 +116,28 @@
var sup_scenario_dict = {{ scenario_filter|safe }};
function imageHider() {
- var data = normalize(result);
var drop = document.getElementById("id_image");
- for( var i=0; i < drop.length; i++ )
- {
- if ( drop.options[i].text == '---------' )
- {
- drop.selectedIndex = i;
- }
- }
- $('#id_image').children().hide();
+ hide_dropdown("id_image");
- var empty_map = {}
+ var lab_pk = get_selected_value("lab");
+ var host_pk = get_selected_value("host");
for ( var i=0; i < drop.childNodes.length; i++ )
{
var image_object = sup_image_dict[drop.childNodes[i].value];
if( image_object ) //weed out empty option
{
- var lab_pk = ""
- for( var j in data["labs"][0] )
- {
- if( j in {} ) { continue; }
- else { lab_pk = j; break; }
- }
- var host_pk = "";
- for( var j in data["hosts"][0] )
- {
- if( j in {} ) { continue; }
- else { host_pk = j; break; }
- }
if( image_object.host_profile == host_pk && image_object.lab == lab_pk )
{
drop.childNodes[i].style.display = "inherit";
+ drop.childNodes[i].disabled = false;
}
}
}
}
- $('#id_image').children().hide();
+ imageHider();
$('#id_installer').children().hide();
$('#id_scenario').children().hide();
@@ -151,22 +158,15 @@
document.getElementById('id_installer').addEventListener('change', scenarioHider);
function dropFilter(target, target_filter, master) {
- ob = document.getElementById(target);
+ var dropdown = document.getElementById(target);
- for(var i=0; i<ob.options.length; i++) {
- if ( ob.options[i].text == '---------' ) {
- ob.selectedIndex = i;
- }
- }
+ hide_dropdown(target);
- targ_id = "#" + target;
- $(targ_id).children().hide();
var drop = document.getElementById(master);
var opts = target_filter[drop.options[drop.selectedIndex].value];
if (!opts) {
opts = {};
}
- var emptyMap = {}
var map = Object.create(null);
for (var i = 0; i < opts.length; i++) {
@@ -174,33 +174,14 @@
map[j] = true;
}
- for (var i = 0; i < document.getElementById(target).childNodes.length; i++) {
- if (document.getElementById(target).childNodes[i].value in opts && !(document.getElementById(target).childNodes[i].value in emptyMap) ) {
- document.getElementById(target).childNodes[i].style.display = "inherit";
+ for (var i = 0; i < dropdown.childNodes.length; i++) {
+ if (dropdown.childNodes[i].value in opts && !(dropdown.childNodes[i].value in {}) ) {
+ dropdown.childNodes[i].style.display = "inherit";
+ dropdown.childNodes[i].disabled = false;
}
}
}
</script>
- <button onclick="submit_form();" class="btn btn-success">Confirm</button>
+ <button id="quick_booking_confirm" onclick="submit_form();" class="btn btn-success">Confirm</button>
</form>
-<script>
- //context vars
- var prefill_host_selection = "{{host_select_field_prefill_data|default:""|safe}}";
- var prefill_purpose = "{{prefill_purpose|default:""|safe}}";
- var prefill_project = "{{prefill_project|default:""|safe}}";
- var prefill_hostname = "{{prefill_hostname|default:""|safe}}";
-
- //to handle prefill
- function prefill_host_select_field(data)
- {
- //
- if(data)
- {
- make_selection(data);
- }
- }
-
- //call init functions
- prefill_host_select_field(prefill_host_selection);
-</script>
{% endblock %}
diff --git a/dashboard/src/templates/booking/stats.html b/dashboard/src/templates/booking/stats.html
index 42eebdd..8bc68cd 100644
--- a/dashboard/src/templates/booking/stats.html
+++ b/dashboard/src/templates/booking/stats.html
@@ -41,17 +41,28 @@ function getData(){
{% endblock %}
{% block content %}
- <p>Number of days to plot: </p>
- <div class="form-group">
- <input id="number_days" type="number" class="form-control" min="1" step="1" style="display:inline;width:200px"/>
- <button class="btn btn-primary" onclick="getData();" style="display:inline;">Submit</button>
- </div>
- <div id="all_graph_container">
- <div id="booking_graph_wrapper">
- <div id="booking_graph_container"/>
+ <div class="container-fluid">
+ <div class="row">
+ <div class="col">
+ <p>Number of days to plot: </p>
+ <div class="form-group">
+ <input id="number_days" type="number" class="form-control" min="1" step="1" style="display:inline;width:200px"/>
+ <button class="btn btn-primary" onclick="getData();" style="display:inline;">Submit</button>
+ </div>
+ </div>
</div>
- <div id="user_graph_wrapper" >
- <div id="user_graph_container"/>
+ <div class="row">
+ <div class="col-12 col-md-10">
+ <!-- These graphs do NOT get redrawn when the browser size is changed -->
+ <div id="all_graph_container border" class="mw-100">
+ <div id="booking_graph_wrapper">
+ <div id="booking_graph_container"/>
+ </div>
+ <div id="user_graph_wrapper">
+ <div id="user_graph_container"/>
+ </div>
+ </div>
+ </div>
</div>
</div>
<script>
diff --git a/dashboard/src/templates/booking/steps/booking_confirm.html b/dashboard/src/templates/booking/steps/booking_confirm.html
index 9c7e951..40c30a9 100644
--- a/dashboard/src/templates/booking/steps/booking_confirm.html
+++ b/dashboard/src/templates/booking/steps/booking_confirm.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/booking/steps/booking_meta.html b/dashboard/src/templates/booking/steps/booking_meta.html
index e4881ae..710d4ee 100644
--- a/dashboard/src/templates/booking/steps/booking_meta.html
+++ b/dashboard/src/templates/booking/steps/booking_meta.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
@@ -10,6 +10,17 @@
padding: 5%;
}
+ .bkcontrib_panel {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .bkcontrib_panel > .form-group {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ }
+
.panel{
padding: 5%;
/*border: solid 1px black;*/
@@ -21,6 +32,15 @@
grid-template-columns: 45% 10% 45%;
border: none;
}
+
+ #id_length {
+ -moz-appearance: none;
+ border: none;
+ box-shadow: none;
+ }
+ input[type=range]::-moz-range-track {
+ background: #cccccc;
+ }
</style>
{% bootstrap_form_errors form type='non_fields' %}
@@ -39,10 +59,11 @@
</script>
{% bootstrap_field form.info_file %}
<p>You must provide a url to your project's INFO.yaml file if you are a PTL and you are trying to book a POD with multiple servers in it.</p>
+ {% bootstrap_field form.deploy_opnfv %}
</div>
<div class="panel panel_center">
</div>
- <div class="panel">
+ <div class="panel bkcontrib_panel">
<p>You may add collaborators on your booking to share resources with coworkers.</p>
{% bootstrap_field form.users label="Collaborators" %}
</div>
diff --git a/dashboard/src/templates/booking/steps/resource_select.html b/dashboard/src/templates/booking/steps/resource_select.html
index 7ccceb3..382316f 100644
--- a/dashboard/src/templates/booking/steps/resource_select.html
+++ b/dashboard/src/templates/booking/steps/resource_select.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/booking/steps/swconfig_select.html b/dashboard/src/templates/booking/steps/swconfig_select.html
index 15c79d8..60a0df7 100644
--- a/dashboard/src/templates/booking/steps/swconfig_select.html
+++ b/dashboard/src/templates/booking/steps/swconfig_select.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/config_bundle/steps/assign_host_roles.html b/dashboard/src/templates/config_bundle/steps/assign_host_roles.html
new file mode 100644
index 0000000..b87a17f
--- /dev/null
+++ b/dashboard/src/templates/config_bundle/steps/assign_host_roles.html
@@ -0,0 +1,22 @@
+{% extends "config_bundle/steps/table_formset.html" %}
+
+{% load bootstrap4 %}
+
+{% block table %}
+<thead>
+ <tr>
+ <th>Host</th>
+ <th>Role</th>
+ </tr>
+</thead>
+<tbody>
+ {% for form in formset %}
+ <tr>
+ <td>{% bootstrap_field form.host_name show_label=False %}</td>
+ <td>{% bootstrap_field form.role show_label=False %}</td>
+ </tr>
+ {% endfor %}
+</tbody>
+
+{{formset.management_form}}
+{% endblock table %}
diff --git a/dashboard/src/templates/config_bundle/steps/assign_network_roles.html b/dashboard/src/templates/config_bundle/steps/assign_network_roles.html
new file mode 100644
index 0000000..aa1df44
--- /dev/null
+++ b/dashboard/src/templates/config_bundle/steps/assign_network_roles.html
@@ -0,0 +1,22 @@
+{% extends "config_bundle/steps/table_formset.html" %}
+
+{% load bootstrap4 %}
+
+{% block table %}
+<thead>
+ <tr>
+ <th>Role</th>
+ <th>Network</th>
+ </tr>
+</thead>
+<tbody>
+ {% for form in formset %}
+ <tr>
+ <td>{% bootstrap_field form.role show_label=False %}</td>
+ <td>{% bootstrap_field form.network show_label=False %}</td>
+ </tr>
+ {% endfor %}
+</tbody>
+
+{{formset.management_form}}
+{% endblock table %}
diff --git a/dashboard/src/templates/config_bundle/steps/config_software.html b/dashboard/src/templates/config_bundle/steps/config_software.html
index ca15c77..68417bc 100644
--- a/dashboard/src/templates/config_bundle/steps/config_software.html
+++ b/dashboard/src/templates/config_bundle/steps/config_software.html
@@ -1,63 +1,19 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
<form action="/wf/workflow/" method="POST" id="software_config_form" class="form">
{% csrf_token %}
<p>Give it a name:</p>
- {{ form.name }}
+ {% bootstrap_field form.name %}
<p>And a description:</p>
- {{ form.description }}
- <p>Install OPNFV?</p>
- {{ form.opnfv }}
- <p>Choose your:</p>
- <table>
- <thead>
- <tr>
- <th>Installer</th>
- <th>Scenario</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>{{form.installer}}</td>
- <td>{{form.scenario}}</td>
- </tr>
- </tbody>
- </table>
-
+ {% bootstrap_field form.description %}
</form>
-<script>
-var supported = {{supported|safe}};
-var installer_drop = document.getElementById("id_installer");
-installer_drop.addEventListener("change", filter);
-var scenario_drop = document.getElementById("id_scenario");
-var scenario_options = {};
-for(var i=0; i<scenario_drop.options.length; i++){
- var option = scenario_drop.options[i];
- scenario_options[option.text] = option;
-}
-
-scenario_drop.disabled=true;
-
-function filter(){
- //clear out existing options
- while(scenario_drop.firstChild){
- scenario_drop.removeChild(scenario_drop.firstChild)
- }
- var installer = installer_drop.options[installer_drop.selectedIndex].text;
- var options = supported[installer];
- for(var i=0; i<options.length; i++){
- scenario_drop.appendChild(scenario_options[options[i]]);
- }
- scenario_drop.disabled = false;
-}
-</script>
{% endblock content %}
diff --git a/dashboard/src/templates/config_bundle/steps/define_software.html b/dashboard/src/templates/config_bundle/steps/define_software.html
index 8e7be91..87e5997 100644
--- a/dashboard/src/templates/config_bundle/steps/define_software.html
+++ b/dashboard/src/templates/config_bundle/steps/define_software.html
@@ -1,102 +1,55 @@
-{% extends "workflow/viewport-element.html" %}
-{% load staticfiles %}
+{% extends "config_bundle/steps/table_formset.html" %}
+
+{% load bootstrap4 %}
+
+{% block table %}
+ <thead>
+ <tr>
+ <th>Device</th>
+ <th>Image</th>
+ <th>HeadNode</th>
+ </tr>
+ </thead>
+ <tbody>
+{% for form in formset %}
+ <tr>
+ <td>{% bootstrap_field form.host_name show_label=False %}</td>
+ <td>{% bootstrap_field form.image show_label=False %}</td>
+ <td class="table_hidden_input_parent">
+ <input id="radio_{{forloop.counter}}" class="my_radio" type="radio" name="headnode" value="{{forloop.counter}}">
+ {{ form.headnode }}
+ </td>
+ </tr>
+{% endfor %}
+{{formset.management_form}}
+
+{% endblock table %}
+
+{% block tablejs %}
+<script>
+
+ document.getElementById("radio_{{headnode}}").checked = true;
+
+</script>
+{% endblock tablejs %}
-{% load bootstrap3 %}
-
-{% block extrahead %}
- <!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
- rel="stylesheet">
-
- <!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}" rel="stylesheet">
-{% endblock extrahead %}
-
-{% block content %}
-{% if error %}
- <h1 style="text-align:center;">{{ error }}</h1>
-{% else %}
- <form style="width: 90%; margin: 5%;" method="post" action="" class="form" id="softwaredefinitionform">
- {% csrf_token %}
-
- <div class="row">
- <div class="col-lg-12">
- <div class="dataTables_wrapper">
- <table class="table table-striped table-bordered table-hover" id="table" cellspacing="0"
- width="100%">
-
- {% block table %}
- <thead>
- <tr>
- <th>Device</th>
- <th>Role</th>
- <th>Image</th>
- </tr>
- </thead>
- <tbody>
- {% for form in formset %}
- <tr>
- {% for field in form %}
- <td>{{ field }}</td>
- {% endfor %}
- </tr>
- {% endfor %}
- {{formset.management_form}}
-
- {% endblock table %}
-
- </table>
- </div>
- <!-- /.table-responsive -->
- <!-- /.panel-body -->
- <!-- /.panel -->
- </div>
- <!-- /.col-lg-12 -->
- </div>
- </form>
-
- <script>
-function filter_images(){
- var filter_data = {{filter_data|safe}};
- for(var key in filter_data){
- var dropdown = document.getElementById(key);
- var to_remove = filter_data[key];
- for(var i=0; i<to_remove.length; i++){
- for(var j=dropdown.children.length-1; j>=0; j--){
- if(dropdown.children[j].text == to_remove[i]){
- dropdown.removeChild(dropdown.children[j]);
- }
- }
- }
+{% block onleave %}
+var parents = document.getElementsByClassName("table_hidden_input_parent");
+for(var i=0; i<parents.length; i++){
+ var node = parents[i];
+ var radio = node.getElementsByClassName("my_radio")[0];
+ var checkbox = radio.nextElementSibling;
+ if(radio.checked){
+ checkbox.value = "True";
}
}
-filter_images();
- </script>
-{% endif %}
-{% endblock content %}
-
-{% block extrajs %}
- {{ block.super }}
- <!-- DataTables JavaScript -->
-
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
-
- <script src={% static "js/dataTables-sort.js" %}></script>
-
- {% block tablejs %}
- {% endblock tablejs %}
-{% endblock extrajs %}
-
-
-{% block onleave %}
-var form = $("#softwaredefinitionform");
+var form = $("#table_formset");
var formData = form.serialize();
var req = new XMLHttpRequest();
req.open("POST", "/wf/workflow/", false);
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
req.onerror = function() { alert("problem with form submission"); }
req.send(formData);
-{% endblock %}
+{% endblock onleave %}
diff --git a/dashboard/src/templates/config_bundle/steps/pick_installer.html b/dashboard/src/templates/config_bundle/steps/pick_installer.html
new file mode 100644
index 0000000..31a06de
--- /dev/null
+++ b/dashboard/src/templates/config_bundle/steps/pick_installer.html
@@ -0,0 +1,32 @@
+{% extends "workflow/viewport-element.html" %}
+{% load staticfiles %}
+
+{% load bootstrap4 %}
+
+{% block content %}
+
+{% if unavailable %}
+<h1>Please choose a config bundle first</h1>
+{% else %}
+
+<form id="installer_form" action="/wf/workflow/" method="POST" id="installer_config_form" class="form">
+ {% csrf_token %}
+ <p>Choose your installer:</p>
+ {% bootstrap_field form.installer %}
+ <p>Choose your scenario:</p>
+ {% bootstrap_field form.scenario %}
+</form>
+
+{% endif %}
+
+{% endblock content %}
+
+{% block onleave %}
+var form = $("#installer_form");
+var formData = form.serialize();
+var req = new XMLHttpRequest();
+req.open("POST", "/wf/workflow/", false);
+req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+req.onerror = function() { alert("problem with form submission"); }
+req.send(formData);
+{% endblock %}
diff --git a/dashboard/src/templates/config_bundle/steps/table_formset.html b/dashboard/src/templates/config_bundle/steps/table_formset.html
new file mode 100644
index 0000000..18edc72
--- /dev/null
+++ b/dashboard/src/templates/config_bundle/steps/table_formset.html
@@ -0,0 +1,64 @@
+{% extends "workflow/viewport-element.html" %}
+{% load staticfiles %}
+
+{% load bootstrap4 %}
+
+{% block extrahead %}
+ <!-- DataTables CSS -->
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
+ rel="stylesheet">
+
+ <!-- DataTables Responsive CSS -->
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
+ rel="stylesheet">
+{% endblock extrahead %}
+
+{% block content %}
+{% if error %}
+ <h1 style="text-align:center;">{{ error }}</h1>
+{% else %}
+<div style="padding: 5%;">
+ <form method="post" action="" class="form" id="table_formset">
+ {% csrf_token %}
+
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover" id="table" cellspacing="0" width="100%">
+
+ {% block table %}
+ {% endblock table %}
+
+ </table>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
+
+{% endif %}
+{% endblock content %}
+
+{% block extrajs %}
+ {{ block.super }}
+ <!-- DataTables JavaScript -->
+
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
+
+ <script src={% static "js/dataTables-sort.js" %}></script>
+
+ {% block tablejs %}
+ {% endblock tablejs %}
+{% endblock extrajs %}
+
+
+{% block onleave %}
+var form = $("#table_formset");
+var formData = form.serialize();
+var req = new XMLHttpRequest();
+req.open("POST", "/wf/workflow/", false);
+req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+req.onerror = function() { alert("problem with form submission"); }
+req.send(formData);
+{% endblock %}
diff --git a/dashboard/src/templates/dashboard/genericselect.html b/dashboard/src/templates/dashboard/genericselect.html
new file mode 100644
index 0000000..441d8dc
--- /dev/null
+++ b/dashboard/src/templates/dashboard/genericselect.html
@@ -0,0 +1,104 @@
+{% extends "workflow/viewport-element.html" %}
+{% load staticfiles %}
+
+{% load bootstrap4 %}
+
+{% block content %}
+
+<style>
+ #page-wrapper {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #{{select_type}}_form_div div {
+ }
+
+ #{{select_type}}_form_div > * {
+ margin-left: 10px;
+ margin-right: 10px;
+ margin-bottom: 20px;
+ }
+
+ #{{select_type}}_form_div div * {
+ }
+
+ #{{select_type}}_form_div {
+ flex: 1;
+ margin: 30px;
+ display: flex;
+ flex-direction: column;
+ }
+
+ #select_section {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ }
+
+ #{{select_type}}_select_form {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .autocomplete {
+ flex: 1;
+ }
+
+ #create_section {
+ }
+
+ #select_header_section {
+ }
+
+ h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ vertical-align: middle;
+ }
+
+ .divider {
+ border-top: 1px solid #ccc;
+ }
+
+
+</style>
+
+<div id="{{select_type}}_form_div">
+ <h3 id="create_section">Create a Resource
+ <button class="btn btn-primary {% if disabled %} disabled {% endif %}"
+ {% if not disabled %}onclick="parent.add_wf({{addable_type_num}})"
+ {% endif %}>Here
+ </button>
+ </h3>
+ <div class="divider"></div>
+ <h3 id="select_header_section">Or select from the list below:</h3>
+ <div id="select_section">
+ <form id="{{select_type}}_select_form" method="post" action="" class="form" id="{{select_type}}selectorform">
+ {% csrf_token %}
+ {{ form|default:"<p>no form loaded</p>" }}
+ {% buttons %}
+
+ {% endbuttons %}
+ </form>
+ </div>
+</div>
+
+<script>
+ {% if disabled %}
+ disable();
+ {% endif %}
+</script>
+
+{% endblock content %}
+{% block onleave %}
+var form = $("#{{select_type}}_select_form");
+var formData = form.serialize();
+var req = new XMLHttpRequest();
+req.open("POST", "/wf/workflow/", false);
+req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+req.onerror = function() { alert("problem with form submission"); }
+req.send(formData);
+{% endblock %}
+
diff --git a/dashboard/src/templates/dashboard/idf.yaml b/dashboard/src/templates/dashboard/idf.yaml
new file mode 100644
index 0000000..9e0cc26
--- /dev/null
+++ b/dashboard/src/templates/dashboard/idf.yaml
@@ -0,0 +1,46 @@
+---
+idf:
+ version: {{version|default:"0.1"}}
+ net_config:
+ oob:
+ ip-range: {{net_config.oob.ip_range}}
+ vlan: {{net_config.oob.vlan}}
+ admin:
+ interface: {{net_config.admin.interface}}
+ vlan: {{net_config.admin.vlan}}
+ network: {{net_config.admin.network}}
+ mask: {{net_config.admin.mask}}
+ mgmt:
+ interface: {{net_config.mgmt.interface}}
+ vlan: {{net_config.mgmt.vlan}}
+ network: {{net_config.mgmt.network}}
+ mask: {{net_config.mgmt.mask}}
+ private:
+ interface: {{net_config.private.interface}}
+ vlan: {{net_config.private.vlan}}
+ network: {{net_config.private.network}}
+ mask: {{net_config.private.mask}}
+ public:
+ interface: {{net_config.public.interface}}
+ vlan: {{net_config.public.vlan}}
+ network: {{net_config.public.network}}
+ mask: {{net_config.public.mask}}
+ ip-range: {{net_config.public.ip_range}}
+ mask: {{net_config.public.mask}}
+ gateway: {{net_config.public.gateway}}
+ dns: {% for serv in net_config.public.dns %}
+ - {{serv}}{% endfor %}
+ fuel:
+ jumphost:
+ bridges:
+ admin: {{fuel.jumphost.bridges.admin}}
+ mgmt: {{fuel.jumphost.bridges.mgmt}}
+ private: {{fuel.jumphost.bridges.private}}
+ public: {{fuel.jumphost.bridges.public}}
+ network: {% for node in fuel.network.nodes %}
+ node:
+ - interfaces: {% for iface in node.interfaces %}
+ - {{ iface }}{% endfor %}
+ - busaddr: {% for addr in node.bus_addrs %}
+ - {{addr}}{% endfor %}
+ {% endfor %}
diff --git a/dashboard/src/templates/dashboard/lab_detail.html b/dashboard/src/templates/dashboard/lab_detail.html
index 4c06245..336b32e 100644
--- a/dashboard/src/templates/dashboard/lab_detail.html
+++ b/dashboard/src/templates/dashboard/lab_detail.html
@@ -9,12 +9,12 @@
{% block content %}
<div class="row">
<div class="col-lg-4">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">Lab Profile</h4>
- <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <div class="card my-2">
+ <div class="card-header d-flex">
+ <h4>Lab Profile</h4>
+ <button class="btn btn-outline-secondary ml-auto" data-toggle="collapse" data-target="#panel_overview">Expand</button>
</div>
- <div class="panel-body" id="panel_overview">
+ <div id="panel_overview" class="card-body collapse show">
<table class="table">
<tr>
<td>Lab Name: </td><td>{{lab.name}}</td>
@@ -50,19 +50,18 @@
</table>
</div>
</div>
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">Host Profiles</h4>
-
- <a data-toggle="collapse" data-target="#profile_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <div class="card my-2">
+ <div class="card-header d-flex">
+ <h4 class="d-inline-block">Host Profiles</h4>
+ <button data-toggle="collapse" data-target="#profile_panel" class="btn btn-outline-secondary ml-auto" style="line-height: 1;" >Expand</button>
</div>
- <div class="panel-body pod_panel" id="profile_panel">
+ <div id="profile_panel" class="card-body collapse show">
<table class="table">
{% for profile in hostprofiles %}
<tr>
<td>{{profile.name}}</td>
<td>{{profile.description}}</td>
- <td>{{profile.labs}}</td>
+ <td><a href="/resource/profiles/{{ profile.id }}" class="btn btn-info">Profile</a></td>
</tr>
{% endfor %}
</table>
@@ -70,31 +69,30 @@
</div>
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="card my-2">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Networking Capabilities</h4>
- <a data-toggle="collapse" data-target="#network_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#network_panel" class="btn btn-outline-secondary ml-auto" style="line-height: 1;" >Expand</button>
</div>
- <div class="panel-body" id="network_panel">
-
- <table class="table">
- <tr>
- <td>Block Size: (number of VLANs allowed per deployment)</td><td>{{lab.vlan_manager.block_size}}</td>
- </tr>
- <tr>
- <td>Overlapping Vlans Allowed (user can pick which VLANs they wish to use): </td>
- <td>{{lab.vlan_manager.allow_overlapping}}</td>
- </tr>
- </table>
+ <div class="card-body collapse show" id="network_panel">
+ <table class="table">
+ <tr>
+ <td>Block Size: (number of VLANs allowed per deployment)</td><td>{{lab.vlan_manager.block_size}}</td>
+ </tr>
+ <tr>
+ <td>Overlapping Vlans Allowed (user can pick which VLANs they wish to use): </td>
+ <td>{{lab.vlan_manager.allow_overlapping}}</td>
+ </tr>
+ </table>
</div>
</div>
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">Images</h4>
- <a data-toggle="collapse" data-target="#image_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <div class="card my-2">
+ <div class="card-header d-flex">
+ <h4>Images</h4>
+ <button data-toggle="collapse" data-target="#image_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body" id="image_panel">
+ <div class="card-body collapse show" id="image_panel">
<table class="table">
<tr>
<th>Name</th>
@@ -116,14 +114,13 @@
</div>
<div class="col-lg-8">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">Lab Hosts</h4>
- <p style="display: inline; margin-left: 10px;"></p>
- <a data-toggle="collapse" data-target="#lab_hosts_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <div class="card my-2">
+ <div class="card-header d-flex">
+ <h4>Lab Hosts</h4>
+ <button data-toggle="collapse" data-target="#lab_hosts_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body" id="lab_hosts_panel">
+ <div class="card-body collapse show" id="lab_hosts_panel">
<table class="table">
<tr>
<th>Name</th>
diff --git a/dashboard/src/templates/dashboard/lab_list.html b/dashboard/src/templates/dashboard/lab_list.html
index a86f7f4..9cde80c 100644
--- a/dashboard/src/templates/dashboard/lab_list.html
+++ b/dashboard/src/templates/dashboard/lab_list.html
@@ -1,87 +1,28 @@
{% extends "base.html" %}
-{% load staticfiles %}
-
-{% block extrahead %}
- {{block.super}}
- <script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js?lang=yaml"></script>
-{% endblock %}
-
{% block content %}
- <style>
- .grid-item-container {
- padding: 10px;
- }
-
- .grid-item {
- cursor: pointer;
- border:2px;
- border-style:none;
- border-color:black;
- border-radius: 5px;
- padding: 7px;
- color: inherit;
-
- box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75);
- transition-property: box-shadow, background-color;
- transition-duration: .2s;
- }
-
- .grid-item-text
- {
- color: inherit;
- text-decoration: none;
- }
- .grid-item-text:hover
- {
- color: #121212;
- text-decoration: none;
- }
-
- .grid-item:hover {
- box-shadow: 0px 0px 14px 0px rgba(0,0,0,0.75);
- transition-property: box-shadow;
- transition-duration: .2s;
-
- }
-
- .selected_node {
- box-shadow: 0px 0px 14px 0px rgba(0,0,0,0.75);
- background-color: #CCECD7;
- transition-property: background-color;
- transition-duration: .2s;
- }
-
- .disabled_node {
- cursor: not-allowed;
- background-color: #EFEFEF;
- box-shadow: 0px 0px 1px 0px rgba(0,0,0,0.75);
- transition-property: box-shadow;
- transition-duration: .2s;
- }
-
- .disabled_node:hover {
- box-shadow: 0px 0px 1px 0px rgba(0,0,0,0.75);
- }
-
- </style>
- <div class="container-fluid">
- <div class="row">
-
- <div class="listgrid">
- {% for lab in labs %}
- <div class="grid-item-container col-lg-2 col-mid-4 col-sm-6">
-
- <a href="{{ lab.name }}" class="grid-item-text">
-
- <div class="grid-item">
- <h4 class="grid-item-header">{{ lab.name }}</h4>
- <p class="grid-item-description">{{ lab.description }}</p>
- </div>
- </a>
+<h2>Labs</h2>
+<div class="card_container">
+ {% for lab in labs %}
+ <div class="card">
+ <div class="card-header">
+ <h3 class="mt-2">{{lab.name}}</h3>
+ </div>
+ <div class="p-4">
+ <ul class="list-group">
+ <li class="list-group-item">name: {{lab.name}}</li>
+ <li class="list-group-item">description: {{lab.description}}</li>
+ <li class="list-group-item">location: {{lab.location}}</li>
+ {% if lab.status == 0 %}
+ <li class="list-group-item">status: Up</li>
+ {% elif lab.status == 100 %}
+ <li class="list-group-item">status: Down for Maintenance</li>
+ {% elif lab.status == 200 %}
+ <li class="list-group-item">status: Down</li>
+ {% endif %}
+ </ul>
+ <a class="btn btn-primary mt-4 w-100" href="/lab/{{lab.name}}/">Details</a>
</div>
- {% endfor %}
- </div>
- </div>
</div>
-
-{% endblock content %}
+ {% endfor %}
+</div>
+{% endblock %} \ No newline at end of file
diff --git a/dashboard/src/templates/dashboard/landing.html b/dashboard/src/templates/dashboard/landing.html
index fb75d5f..e6a235f 100644
--- a/dashboard/src/templates/dashboard/landing.html
+++ b/dashboard/src/templates/dashboard/landing.html
@@ -2,27 +2,30 @@
{% load staticfiles %}
{% block content %}
- <div class="" style="text-align: center;">
- {% if not request.user.is_anonymous %}
- {% if not request.user.userprofile.ssh_public_key %}
- <h4 style="display: inline; text-align: center; border: 3px solid red; padding: 10px; border-radius: 10000px; height: 40px;">
- Warning: you need to upload an ssh key under <a href="/accounts/settings">account settings</a> if you wish to log into the servers you book
- </h4>
- {% endif %}
- {% else %}
- {% endif %}
+<div class="" style="text-align: center;">
+ {% if not request.user.is_anonymous %}
+ {% if not request.user.userprofile.ssh_public_key %}
+ <div class="alert alert-danger" role="alert">
+ Warning: you need to upload an ssh key under <a href="/accounts/settings">account settings</a> if you wish to
+ log into the servers you book
</div>
+ {% endif %}
+ {% else %}
+ {% endif %}
+</div>
{% csrf_token %}
<style>
- .wf_create{
+ .wf_create {
display: inline-block;
text-align: center;
}
- .wf_create_div{
+
+ .wf_create_div {
text-align: center;
}
- .hidden_form{
+
+ .hidden_form {
display: none;
}
@@ -39,81 +42,117 @@
display: grid;
grid-template-columns: 1fr 30px 1fr;
}
+
.grid_panel {
padding: 30px;
}
+
.btn-primary {
margin: 10px;
}
+
h2 {
border-bottom: 1px solid #cccccc;
}
- h1 {
- }
-</style>
-<div class="landing_container">
- <div class="info_panel grid_panel">
- <h2>About Us:</h2>
- <p>The Lab as a Service (LaaS) project aims to help in the development and testing of LFN projects such as OPNFV by hosting hardware and providing access to the community. Currently, the only participating lab is the University of New Hampshire Interoperability Lab (UNH-IOL).</p>
- <p>To get started, you can request access to a server at the right. PTL's have the ability to design and book a whole block of servers with customized layer2 networks (e.g. a Pharos Pod). Read more here: <a href="https://wiki.opnfv.org/display/INF/Lab+as+a+Service+2.0">LaaS Wiki</a></p>
+ h1 {}
+</style>
+<div class="container-fluid">
+ <div class="row">
+ <!-- About us -->
+ <div class="col-12 col-lg-6 mb-4">
+ <h2>About Us:</h2>
+ <p>The Lab as a Service (LaaS) project aims to help in the development and testing of LFN projects such as
+ OPNFV
+ by hosting hardware and providing access to the community. Currently, the only participating lab is the
+ University of New Hampshire Interoperability Lab (UNH-IOL).</p>
+ <p>To get started, you can request access to a server at the right. PTL's have the ability to design and
+ book a
+ whole block of servers with customized layer2 networks (e.g. a Pharos Pod). Read more here: <a
+ href="https://wiki.opnfv.org/display/INF/Lab+as+a+Service+2.0">LaaS Wiki</a></p>
+ </div>
+ <!-- Get started -->
+ <div class="col-12 col-lg-6 mb-4">
+ <h2>Get Started:</h2>
+ {% if request.user.is_anonymous %}
+ <h4 style="text-align:center;">To get started, please log in with your <a href="/accounts/login">Linux
+ Foundation Jira account</a></h4>
+ {% else %}
+ <p>To get started, book a server below:</p>
+ <a class="wf_create btn btn-primary"
+ style="display: flex; flex-direction: column; justify-content: center; margin: 20px; height: 100pt; vertical-align: middle; text-align: center; color: #FFF;"
+ href="/booking/quick/">
+ <p style="font-size: xx-large">Book a Server</p>
+ </a>
+ <p>PTLs can use our advanced options to book multi-node pods. If you are a PTL, you may use the options
+ below:
+ </p>
+ <div class='container'>
+ <div class="row">
+ <div class="col-12 col-xl-4">
+ <button class="wf_create btn btn-primary w-100" onclick="cwf(0)">Book a Pod</button>
+ </div>
+ <div class="col-12 col-xl-4">
+ <button class="wf_create btn btn-primary w-100" onclick="cwf(1)">Design a Pod</button>
+ </div>
+ <div class="col-12 col-xl-4">
+ <button class="wf_create btn btn-primary w-100" onclick="cwf(2)">Configure a Pod</button>
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ <!-- Returning users -->
{% if not request.user.is_anonymous %}
- <h2 style="margin-top: 50px;">Returning Users:</h2>
- <p>If you're a returning user, some of the following options may be of interest:</p>
- <button class="wf_create btn btn-primary" onclick="cwf(3)">Snapshot a Host</button>
- <a class="wf_create btn btn-primary" href="{% url 'account:my-bookings' %}">My Bookings</a>
- {% if manager == True %}
- <button class="wf_continue btn btn-primary" onclick="continue_wf()">Continue Unfinished Workflow</button>
- {% endif %}
- {% endif %}
- </div>
- <div class="">
- </div>
- <div class="actions_panel grid_panel">
- <h2>Get Started:</h2>
- {% if request.user.is_anonymous %}
- <h4 style="text-align:center;">To get started, please log in with your <a href="/accounts/login">Linux Foundation Jira account</a></h4>
- {% else %}
- <p>To get started, book a server below:</p>
- <a class="wf_create btn btn-primary" style="display: flex; flex-direction: column; justify-content: center; margin: 20px; height: 100pt; vertical-align: middle; text-align: center; color: #FFF;" href="/booking/quick/"><p style="font-size: xx-large">Book a Server</p></a>
- <p>PTLs can use our advanced options to book multi-node pods. If you are a PTL, you may use the options below:</p>
- <div class='wf_create_div'>
-
- <button class="wf_create btn btn-primary" onclick="cwf(0)">Book a Pod</button>
- <button class="wf_create btn btn-primary" onclick="cwf(1)">Design a Pod</button>
- <button class="wf_create btn btn-primary" onclick="cwf(2)">Configure a Pod</button>
- {% endif %}
+ <div class="col-12 col-lg-6 offset-lg-6 mb-4 mt-lg-4">
+ <h2 class="ht-4">Returning Users:</h2>
+ <p>If you're a returning user, some of the following options may be of interest:</p>
+ <div class="container">
+ <div class="row">
+ <div class="col-12 col-xl-4">
+ <button class="wf_create btn btn-primary w-100" onclick="cwf(3)">Snapshot a Host</button>
+ </div>
+ <div class="col-12 col-xl-4">
+ <a class="wf_create btn btn-primary w-100" href="{% url 'account:my-bookings' %}">My
+ Bookings</a>
+ </div>
+ {% if manager == True %}
+ <div class="col-12 col-xl-4">
+ <button class="wf_continue btn btn-primary w-100" onclick="continue_wf()">Resume
+ Workflow</button>
+ </div>
+ {% endif %}
+ </div>
+ </div>
</div>
+ {% endif %}
</div>
</div>
-
-
-
-
<script type="text/javascript">
- function cwf(type)
- {
+ function cwf(type) {
$.ajax({
type: "POST",
url: "/",
- data: {"create":type},
- beforeSend: function(request) {
+ data: {
+ "create": type
+ },
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val()
+ $('input[name="csrfmiddlewaretoken"]').val()
);
}
}).done(function (data) {
window.location.replace("/wf/");
- }).fail(function(jqxHR, textstatus) {
- alert("Something went wrong...");});
+ }).fail(function (jqxHR, textstatus) {
+ alert("Something went wrong...");
+ });
}
- function continue_wf()
- {
+
+ function continue_wf() {
window.location.replace("/wf/");
}
-
</script>
<div class="hidden_form" id="form_div">
@@ -130,4 +169,4 @@
{% block vport_comm %}
{% endblock %}
-{% endblock content %}
+{% endblock content %} \ No newline at end of file
diff --git a/dashboard/src/templates/dashboard/multiple_select_filter_widget.html b/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
index 4e47ce0..4302543 100644
--- a/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
+++ b/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
@@ -1,405 +1,149 @@
+<script src="/static/js/dashboard.js">
+</script>
+
<style>
.object_class_wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
border: 0px;
}
+
.class_grid_wrapper {
border: 0px;
- border-left: 1px;
+ text-align: center;
border-right: 1px;
border-style: solid;
border-color: grey;
- text-align: center;
}
+
+.class_grid_wrapper:last-child {
+ border-right: none;
+}
+
.grid_wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
}
+
.grid-item {
cursor: pointer;
- border:2px;
- border-style:none;
- border-color:black;
+ border: 1px solid #cccccc;
border-radius: 5px;
- margin:20px;
+ margin: 20px;
height: 200px;
padding: 7px;
- box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75);
- transition-property: box-shadow, background-color;
- transition-duration: .2s;
+ transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s;
+ box-shadow: 0 1px 1px rgba(0,0,0,.075);
+
+ display: flex;
+ flex-direction: column;
}
-.grid-item:hover {
- box-shadow: 0px 0px 14px 0px rgba(0,0,0,0.75);
- transition-property: box-shadow;
- transition-duration: .2s;
+.grid-item > .btn:active, .grid-item > .btn:focus {
+ outline: none; !important;
+ box-shadow: none;
+}
+.grid-item-description {
+ flex: 1;
}
.selected_node {
- box-shadow: 0px 0px 14px 0px rgba(0,0,0,0.75);
- background-color: #CCECD7;
- transition-property: background-color;
- transition-duration: .2s;
+ border-color: #40c640;
+ box-shadow: 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(109, 243, 76, 0.6);
+ transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s;
+}
+
+.grid-item:hover:not(.selected_node):not(.disabled_node) {
+ box-shadow: 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(100, 100, 100, 0.3);
+ transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s;
}
.disabled_node {
cursor: not-allowed;
background-color: #EFEFEF;
- box-shadow: 0px 0px 1px 0px rgba(0,0,0,0.75);
- transition-property: box-shadow;
- transition-duration: .2s;
}
-.disabled_node:hover {
- box-shadow: 0px 0px 1px 0px rgba(0,0,0,0.75);
-}
+.disabled_node:hover {}
.cleared_node {
background-color: #FFFFFF;
}
-.grid-item-header
-{
+.grid-item-header {
font-weight: bold;
font-size: 20px;
margin-top: 10px;
}
-</style>
-<input name="filter_field" id="filter_field" type="hidden"/>
-<div id="grid_wrapper" class="grid_wrapper">
-{% for object_class, object_list in filter_objects %}
-<div class="class_grid_wrapper">
- <div style="display:inline-block;margin:auto">
- <h4>{{object_class}}</h4>
- </div>
- <div id="{{object_class}}" class="object_class_wrapper">
- {% for obj in object_list %}
- <div id="object_parent">
- <div id="{{ obj.id|default:'not_provided' }}" class="grid-item">
- <p class="grid-item-header">{{obj.name}}</p>
- <p class="grid-item-description">{{obj.description}}</p>
- <button type="button" class="btn btn-success grid-item-select-btn" onclick="processClick('{{obj.id}}', {% if obj.multiple %}true{% else %}false{% endif %});">{% if obj.multiple %}Add{% else %}Select{% endif %}</button>
- </div>
- <input type="hidden" name="{{obj.id}}_selected" value="false"/>
- </div>
- {% endfor %}
- </div>
- </div>
-{% endfor %}
-</div>
-
-<div id="dropdown_wrapper">
-</div>
-
-<script>
-var initialized = false;
-var mapping = {{ mapping|safe }};
-var filter_items = {{ filter_items|safe }};
-var result = {};
-var selection = {{selection_data|default_if_none:"null"|safe}};
-var dropdown_count = 0;
-
-{% if selection_data %}
-make_selection({{selection_data|safe}});
-{% endif %}
-
-function make_selection( selection_data ){
- if(!initialized) {
- filter_field_init();
- }
- for(var k in selection_data) {
- selected_items = selection_data[k];
- for( var selected_item in selected_items ){
- var node = filter_items[selected_item];
- if(!node['multiple']){
- var input_value = selected_items[selected_item];
- if( input_value != 'false' ) {
- select(node);
- markAndSweep(node);
- }
- var div = document.getElementById(selected_item)
- var inputs = div.parentNode.getElementsByTagName("input")
- var input = div.parentNode.getElementsByTagName("input")[0]
- input.value = input_value;
- updateResult(selected_item);
- } else {
- make_multiple_selection(selected_items, selected_item);
- }
- }
- }
-}
-
-function make_multiple_selection(data, item_class){
- var node = filter_items[item_class];
- select(node);
- markAndSweep(node);
- prepop_data = data[item_class];
- for(var i=0; i<prepop_data.length; i++){
- var div = add_item_prepopulate(node, prepop_data[i]);
- updateObjectResult(div);
- }
-}
-
-function markAndSweep(root){
- for(var nodeId in filter_items) {
- node = filter_items[nodeId];
- node['marked'] = true; //mark all nodes
- //clears grey background of everything
- }
-
- toCheck = [];
- toCheck.push(root);
-
- while(toCheck.length > 0){
- node = toCheck.pop();
- if(!node['marked']) {
- //already visited, just continue
- continue;
- }
- node['marked'] = false; //mark as visited
- if(node['follow'] || node == root){ //add neighbors if we want to follow this node (labs)
- var mappingId = node.id
- var neighbors = mapping[mappingId];
- for(var neighId in neighbors) {
- neighId = neighbors[neighId];
- var neighbor = filter_items[neighId];
- toCheck.push(neighbor);
- }
- }
- }
-
- //now remove all nodes still marked
- for(var nodeId in filter_items){
- node = filter_items[nodeId];
- if(node['marked']){
- disable_node(node);
- }
- }
-}
-
-function process(node) {
- if(node['selected']) {
- markAndSweep(node);
- }
- else {
- var selected = []
- //remember the currently selected, then reset everything and reselect one at a time
- for(var nodeId in filter_items) {
- node = filter_items[nodeId];
- if(node['selected']) {
- selected.push(node);
- }
- clear(node);
-
- }
- for(var i=0; i<selected.length; i++) {
- node = selected[i];
- select(node);
- markAndSweep(selected[i]);
- }
- }
-}
-
-function select(node) {
- elem = document.getElementById(node['id']);
- node['selected'] = true;
- elem.classList.remove('cleared_node');
- elem.classList.remove('disabled_node');
- elem.classList.add('selected_node');
- var input = elem.parentNode.getElementsByTagName("input")[0];
- input.disabled = false;
- input.value = true;
-}
-
-function clear(node) {
- elem = document.getElementById(node['id']);
- node['selected'] = false;
- node['selectable'] = true;
- elem.classList.add('cleared_node')
- elem.classList.remove('disabled_node');
- elem.classList.remove('selected_node');
- elem.parentNode.getElementsByTagName("input")[0].disabled = true;
-}
-
-function disable_node(node) {
- elem = document.getElementById(node['id']);
- node['selected'] = false;
- node['selectable'] = false;
- elem.classList.remove('cleared_node');
- elem.classList.add('disabled_node');
- elem.classList.remove('selected_node');
- elem.parentNode.getElementsByTagName("input")[0].disabled = true;
-}
-
-function processClick(id, multiple){
- if(!initialized){
- filter_field_init();
- }
- var element = document.getElementById(id);
- var node = filter_items[id];
- if(!node['selectable']){
- return;
- }
- if(multiple){
- return processClickMultipleObject(node);
- }
- node['selected'] = !node['selected']; //toggle on click
-
- if(node['selected']) {
- select(node);
- }
- else {
- clear(node);
- }
- process(node);
- updateResult(id);
+.dropdown_item {
+ border: 1px;
+ border-style: solid;
+ border-color: lightgray;
+ border-radius: 5px;
+ margin: 20px;
+ padding: 2px;
+ grid-column: 1;
+ display: grid;
+ grid-template-columns: 1fr 3fr 1fr;
+ justify-items: center;
}
-function processClickMultipleObject(node){
- select(node);
- add_node(node);
- process(node);
+.dropdown_item > button {
+ margin: 2px;
+ justify-self: end;
}
-function add_node(node){
- return add_item_prepopulate(node, {});
+.dropdown_item > h5 {
+ margin: auto;
}
-inputs = []
-
-function restrictchars(input)
-{
- if( input.validity.patternMismatch )
- {
- input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
- input.reportValidity();
- }
-
- input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
-
- checkunique(input);
-}
-
-function checkunique(tocheck)
-{
- val = tocheck.value;
- for( var i = 0; i < inputs.length; i++ )
- {
- if( inputs[i].value == val && inputs[i] != tocheck)
- {
- tocheck.setCustomValidity("All hostnames must be unique");
- tocheck.reportValidity();
- return;
- }
- }
- tocheck.setCustomValidity("");
-}
-
-function add_item_prepopulate(node, prepopulate){
- inputs = [];
- var div = document.createElement("DIV");
- div.class = node['id'];
- div.id = "dropdown_" + dropdown_count;
- dropdown_count++;
- var label = document.createElement("H5");
- label.style['display'] = 'inline';
- label.appendChild(document.createTextNode(node['name']));
- div.appendChild(label);
- for(var i=0; i<node['forms'].length; i++){
- form = node['forms'][i];
- var input = document.createElement("INPUT");
- input.type = form['type'];
- input.name = form['name'];
- input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
- input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
- input.placeholder = form['placeholder'];
- inputs.push(input);
- input.onchange = function() { updateObjectResult(div); restrictchars(this); };
- input.oninput = function() { restrictchars(this); };
- if(form['name'] in prepopulate){
- input.value = prepopulate[form['name']];
- }
- div.appendChild(input);
- }
- //add class id to dropdown object
- var hiddenInput = document.createElement("INPUT");
- hiddenInput.type = "hidden";
- hiddenInput.name = "class";
- hiddenInput.value = node['id'];
- div.appendChild(hiddenInput);
- button = document.createElement("BUTTON");
- button.onclick = function(){
- remove_dropdown(div.id);
- }
- button.type = "button";
- button.appendChild(document.createTextNode("Remove"));
- div.appendChild(button);
- document.getElementById("dropdown_wrapper").appendChild(div);
- updateObjectResult(div);
- return div;
+.dropdown_item > input {
+ padding: 7px;
+ margin: 2px;
+ width: 90%;
}
-function remove_dropdown(id){
- var div = document.getElementById(id);
- var parent = div.parentNode;
- div.parentNode.removeChild(div);
- //checks if we have removed last item in class
- var deselect_class = true;
- var div_inputs = div.getElementsByTagName("input");
- var div_class = div_inputs[div_inputs.length-1].value;
- var result_class = document.getElementById(div_class).parentNode.parentNode.id;
- delete result[result_class][div.id];
- for(var i=0; i<parent.children.length; i++){
- var inputs = parent.children[i].getElementsByTagName("input");
- var object_class = "";
- for(var k=0; k<inputs.length; k++){
- if(inputs[k].name == "class"){
- object_class = inputs[k].value;
- }
- }
- if(object_class == div_class){
- deselect_class = false;
- }
- }
- if(deselect_class){
- clear(filter_items[div_class]);
- }
+#dropdown_wrapper {
+ display: grid;
+ grid-template-columns: 4fr 5fr;
}
+</style>
-function updateResult(nodeId){
- if(!initialized){
- filter_field_init();
- }
- if(!filter_items[nodeId]['multiple']){
- var node = document.getElementById(nodeId);
- var value = {}
- value[nodeId] = node.parentNode.getElementsByTagName("input")[0].value;
- result[node.parentNode.parentNode.id] = {};
- result[node.parentNode.parentNode.id][nodeId] = value;
- }
-}
+<input name="filter_field" id="filter_field" type="hidden"/>
+<div id="grid_wrapper" class="grid_wrapper">
+{% for object_class, object_list in display_objects %}
+ <div class="class_grid_wrapper">
+ <div style="display:inline-block;margin:auto">
+ <h4>{{object_class}}</h4>
+ </div>
+ <div id="{{object_class}}" class="object_class_wrapper">
+ {% for obj in object_list %}
+ <div id="{{ obj.id|default:'not_provided' }}" class="grid-item" onclick="multi_filter_widget.processClick(
+ '{{obj.id}}');">
+ <p class="grid-item-header">{{obj.name}}</p>
+ <p class="grid-item-description">{{obj.description}}</p>
+ <button type="button" class="btn btn-success grid-item-select-btn">
+ {% if obj.multiple %}Add{% else %}Select{% endif %}
+ </button>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+{% endfor %}
+</div>
-function updateObjectResult(parentElem){
- node_type = document.getElementById(parentElem.class).parentNode.parentNode.id;
- input = {};
- inputs = parentElem.getElementsByTagName("input");
- for(var i in inputs){
- var e = inputs[i];
- input[e.name] = e.value;
- }
- result[node_type][parentElem.id] = input;
-}
+<div id="dropdown_wrapper">
+</div>
+<script>
+function multipleSelectFilterWidgetEntry() {
+ const graph_neighbors = {{ neighbors|safe }};
+ const filter_items = {{ filter_items|safe }};
+ const initial_value = {{ initial_value|default_if_none:"{}"|safe }};
-function filter_field_init() {
- for(nodeId in filter_items) {
- element = document.getElementById(nodeId);
- node = filter_items[nodeId];
- result[element.parentNode.parentNode.id] = {}
- }
- initialized = true;
+ //global variable
+ multi_filter_widget = new MultipleSelectFilterWidget(graph_neighbors, filter_items, initial_value);
}
+multipleSelectFilterWidgetEntry();
</script>
diff --git a/dashboard/src/templates/dashboard/pdf.yaml b/dashboard/src/templates/dashboard/pdf.yaml
index 297e04b..c893919 100644
--- a/dashboard/src/templates/dashboard/pdf.yaml
+++ b/dashboard/src/templates/dashboard/pdf.yaml
@@ -1,95 +1,92 @@
---
version: {{version|default:"1.0"}}
details:
- pod_owner: {{details.owner}}
- contact: {{details.contact}}
- lab: {{details.lab}}
- location: {{details.location}}
- type: {{details.type}}
- link: {{details.link}}
-
+ contact: {{details.contact}}
+ lab: {{details.lab}}
+ link: {{details.link}}
+ location: {{details.location}}
+ pod_owner: {{details.owner}}
+ type: {{details.type}}
jumphost:
- name: {{jumphost.name}}
- node:
- type: {{jumphost.node.type}}
- vendor: {{jumphost.node.vendor}}
- model: {{jumphost.node.model}}
- arch: {{jumphost.node.arch}}
- cpus: {{jumphost.node.cpus}}
- cpu_cflags: {{jumphost.node.cpu_cflags}}
- cores: {{jumphost.node.cores}}
- memory: {{jumphost.node.memory}}
- disks:
- {% for disk in jumphost.disks %}
- - name: {{disk.name}}
- disk_capacity: {{disk.capacity}}
- disk_type: {{disk.type}}
- disk_interface: {{disk.interface}}
- disk_rotation: {{disk.rotation}}
-
- {% endfor %}
- os: {{jumphost.os}}
- remote_params:
- type: {{jumphost.remote.type}}
- versions:
- {% for version in jumphost.remote.versions %}
- - {{version}}
- {% endfor %}
- user: {{jumphost.remote.user}}
- pass: {{jumphost.remote.pass}}
- remote_management:
- type: {{jumphost.remote.type}}
- versions:
- {% for version in jumphost.remote.versions %}
- - {{version}}
- {% endfor %}
- user: {{jumphost.remote.user}}
- pass: {{jumphost.remote.pass}}
- address: {{jumphost.remote.address}}
- mac_address: {{jumphost.remote.mac_address}}
- interfaces:
- {% for interface in jumphost.interfaces %}
- - name: {{interface.name}}
- address: {{interface.address}}
- mac_address: {{interface.mac_address}}
- vlan: {{interface.vlan}}
- {% endfor %}
+ disks:
+ {% for disk in jumphost.disks %}
+ - disk_capacity: {{disk.capacity}}
+ disk_interface: {{disk.interface}}
+ disk_rotation: {{disk.rotation}}
+ disk_type: {{disk.type}}
+ name: {{disk.name}}
+ {% endfor %}
+ interfaces:
+ {% for interface in jumphost.interfaces %}
+ - features: {{interface.features}}
+ mac_address: {{interface.mac_address}}
+ name: {{interface.name}}
+ speed: {{interface.speed}}
+ {% endfor %}
+ name: {{jumphost.name}}
+ node:
+ arch: {{jumphost.node.arch}}
+ cores: {{jumphost.node.cores}}
+ cpu_cflags: {{jumphost.node.cpu_cflags}}
+ cpus: {{jumphost.node.cpus}}
+ memory: {{jumphost.node.memory}}
+ model: {{jumphost.node.model}}
+ type: {{jumphost.node.type}}
+ vendor: {{jumphost.node.vendor}}
+ os: {{jumphost.os}}
+ remote_management:
+ address: {{jumphost.remote.address}}
+ mac_address: {{jumphost.remote.mac_address}}
+ pass: {{jumphost.remote.pass}}
+ type: {{jumphost.remote.type}}
+ user: {{jumphost.remote.user}}
+ versions:
+ {% for version in jumphost.remote.versions %}
+ - {{version}}
+ {% endfor %}
+ remote_params:
+ pass: {{jumphost.remote.pass}}
+ type: {{jumphost.remote.type}}
+ user: {{jumphost.remote.user}}
+ versions:
+ {% for version in jumphost.remote.versions %}
+ - {{version}}
+ {% endfor %}
nodes:
- {% for node in nodes %}
- - name: {{node.name}}
- node:
- type: {{node.node.type}}
- vendor: {{node.node.vendor}}
- model: {{node.node.model}}
- arch: {{node.node.arch}}
- cpus: {{node.node.cpus}}
- cpu_cflags: {{node.node.cpu_cflags}}
- cores: {{node.node.cores}}
- memory: {{node.node.memory}}
- disks:
- {% for disk in node.disks %}
- - name: {{disk.name}}
- disk_capacity: {{disk.capacity}}
- disk_type: {{disk.type}}
- disk_interface: {{disk.interface}}
- disk_rotation: {{disk.rotation}}
-
- {% endfor %}
- remote_management:
- type: {{node.remote.type}}
- versions:
- {% for version in node.remote.versions %}
- - {{version}}
- {% endfor %}
- user: {{node.remote.user}}
- pass: {{node.remote.pass}}
- address: {{node.remote.address}}
- mac_address: {{node.remote.mac_address}}
- interfaces:
- {% for interface in node.interfaces %}
- - name: {{interface.name}}
- address: {{interface.address}}
- mac_address: {{interface.mac_address}}
- vlan: {{interface.vlan}}
- {% endfor %}
+{% for node in nodes %}
+- disks:
+ {% for disk in node.disks %}
+ - disk_capacity: {{disk.capacity}}
+ disk_interface: {{disk.interface}}
+ disk_rotation: {{disk.rotation}}
+ disk_type: {{disk.type}}
+ name: {{disk.name}}
+ {% endfor %}
+ interfaces:
+ {% for interface in node.interfaces %}
+ - features: {{interface.features}}
+ mac_address: {{interface.mac_address}}
+ name: {{interface.name}}
+ speed: {{interface.speed}}
{% endfor %}
+ name: {{node.name}}
+ node:
+ arch: {{node.node.arch}}
+ cores: {{node.node.cores}}
+ cpu_cflags: {{node.node.cpu_cflags}}
+ cpus: {{node.node.cpus}}
+ memory: {{node.node.memory}}
+ model: {{node.node.model}}
+ type: {{node.node.type}}
+ vendor: {{node.node.vendor}}
+ remote_management:
+ address: {{node.remote.address}}
+ mac_address: {{node.remote.mac_address}}
+ pass: {{node.remote.pass}}
+ type: {{node.remote.type}}
+ user: {{node.remote.user}}
+ versions:
+ {% for version in node.remote.versions %}
+ - {{version}}
+ {% endfor %}
+{% endfor %}
diff --git a/dashboard/src/templates/dashboard/resource.html b/dashboard/src/templates/dashboard/resource.html
index 28e7998..f36ee7b 100644
--- a/dashboard/src/templates/dashboard/resource.html
+++ b/dashboard/src/templates/dashboard/resource.html
@@ -7,11 +7,11 @@
<link href="{% static "bower_components/morrisjs/morris.css" %}" rel="stylesheet">
<!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
rel="stylesheet">
<!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
rel="stylesheet">
{% endblock extrahead %}
@@ -23,11 +23,11 @@
{% block extrajs %}
<!-- DataTables JavaScript -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables/media/css/dataTables.bootstrap.css" %}"
rel="stylesheet">
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
diff --git a/dashboard/src/templates/dashboard/resource_all.html b/dashboard/src/templates/dashboard/resource_all.html
index 0b0d0d4..fb8cc7e 100644
--- a/dashboard/src/templates/dashboard/resource_all.html
+++ b/dashboard/src/templates/dashboard/resource_all.html
@@ -7,11 +7,11 @@
<link href="{% static "bower_components/morrisjs/morris.css" %}" rel="stylesheet">
<!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
rel="stylesheet">
<!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
rel="stylesheet">
{% endblock extrahead %}
@@ -36,11 +36,11 @@
{% block extrajs %}
<!-- DataTables JavaScript -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables/media/css/dataTables.bootstrap.css" %}"
rel="stylesheet">
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
diff --git a/dashboard/src/templates/dashboard/searchable_select_multiple.html b/dashboard/src/templates/dashboard/searchable_select_multiple.html
index c08fbe5..8bcf890 100644
--- a/dashboard/src/templates/dashboard/searchable_select_multiple.html
+++ b/dashboard/src/templates/dashboard/searchable_select_multiple.html
@@ -1,41 +1,58 @@
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
+<script src="/static/js/dashboard.js"></script>
-<div class="autocomplete" style="width:400px;">
+<div id="search_select_outer" class="autocomplete">
<div id="warning_pane" style="background: #FFFFFF; color: #CC0000;">
{% if incompatible == "true" %}
<h3>Warning: Incompatible Configuration</h3>
<p>Please make a different selection, as the current config conflicts with the selected pod</p>
{% endif %}
</div>
- <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="{{initial.name}}" oninput="search(this.value)"
+ <div id="added_counter">
+ <p id="added_number">0</p>
+ <p id="addable_limit">/ {% if selectable_limit > -1 %} {{ selectable_limit }} {% else %} &infin; {% endif %}added</p>
+ </div>
+
+ <div id="added_list">
+
+ </div>
+
+ <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="" oninput="searchable_select_multiple_widget.search(this.value)"
{% if disabled %} disabled {% endif %}
>
+ </input>
<input type="hidden" id="selector" name="{{ name }}" class="form-control" style="display: none;"
{% if disabled %} disabled {% endif %}
>
</input>
- <ul id="drop_results"></ul>
-
-
- <div id="default_entry_wrap" style="display: none;">
- <div class="list_entry unremovable_list_entry">
- <p id="default_text" class="full_name"></p>
- <button class="btn-remove btn disabled">remove</button>
- </div>
+ <div id="scroll_restrictor">
+ <ul id="drop_results"></ul>
</div>
+ <style>
+ #scroll_restrictor {
+ flex: 1;
+ position: relative;
+ overflow-y: auto;
+ padding-bottom: 10px;
+ }
- <div id="added_list">
+ #added_list {
+ margin-bottom: 5px;
+ }
- </div>
- <div id="added_counter" style="text-align: center; margin: 10px;"><p id="added_number" style="display: inline;">0</p><p style="display: inline;">/
- {% if selectable_limit > -1 %} {{ selectable_limit }} {% else %} &infin; {% endif %}added</p></div>
- <style>
+ .autocomplete {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
#user_field {
font-size: 14pt;
- width: 400px;
padding: 5px;
+ height: 40px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
}
@@ -43,371 +60,150 @@
list-style-type: none;
padding: 0;
margin: 0;
- max-height: 300px;
min-height: 0;
- overflow-y: scroll;
- overflow-x: hidden;
border: solid 1px #ddd;
- display: none;
+ border-top: none;
+ border-bottom: none;
+ visibility: inherit;
+ flex: 1;
+
+ position: absolute;
+ width: 100%;
}
#drop_results li a{
font-size: 14pt;
- border: 1px solid #ddd;
background-color: #f6f6f6;
- padding: 12px;
+ padding: 7px;
text-decoration: none;
display: block;
- width: 400px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
- .btn-remove {
- float: right;
- height: 30px;
- margin: 4px;
+ #drop_results li a {
+ border-bottom: 1px solid #ddd;
}
.list_entry {
- width: 400px;
- border: 1px solid #ddd;
- border-radius: 3px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
margin-top: 5px;
vertical-align: middle;
line-height: 40px;
height: 40px;
padding-left: 12px;
+ width: 100%;
+ display: flex;
}
#drop_results li a:hover{
background-color: #ffffff;
}
- .small_name {
- display: inline-block;
+ .added_entry_text {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline;
+ width: 100%;
}
- .full_name {
- display: inline-block;
+ .btn-remove {
+ float: right;
+ height: 30px;
+ margin: 4px;
+ padding: 1px;
+ max-width: 20%;
+ width: 15%;
+ min-width: 70px;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
- </style>
-</div>
-
-<script type="text/javascript">
- //flags
- var show_from_noentry = {{show_from_noentry|default:"false"}};
- var show_x_results = {{show_x_results|default:-1}};
- var results_scrollable = {{results_scrollable|default:"false"}};
- var selectable_limit = {{selectable_limit|default:-1}};
- var field_name = "{{name|default:"users"}}";
- var placeholder = "{{placeholder|default:"begin typing"}}";
- var default_entry = "{{default_entry}}";
-
- //needed info
- var items = {{items|safe}}
-
- //tries
- var expanded_name_trie = {}
- expanded_name_trie.isComplete = false;
- var small_name_trie = {}
- small_name_trie.isComplete = false;
- var string_trie = {}
- string_trie.isComplete = false;
-
- var added_items = [];
- var initial_log = {{ initial|safe }};
-
- var added_template = {{ added_list|default:"{}" }};
-
- if( default_entry )
- {
- var default_entry_div = document.getElementById("default_entry_wrap");
- default_entry_div.style.display = "inherit";
-
- var entry_p = document.getElementById("default_text");
- entry_p.innerText = default_entry;
- }
-
- search_field_init();
-
- if( show_from_noentry )
- {
- search("");
- }
-
- function disable() {
- var textfield = document.getElementById("user_field");
- var drop = document.getElementById("drop_results");
-
- textfield.disabled = "True";
- drop.style.display = "none";
-
- var btns = document.getElementsByClassName("btn-remove");
- for( var i = 0; i < btns.length; i++ )
- {
- btns[i].classList.add("disabled");
+ .entry_tooltip {
+ display: none;
}
- }
- function search_field_init() {
- build_all_tries(items);
-
- var initial = {{ initial|safe }};
+ #drop_results li a:hover .entry_tooltip {
+ position: absolute;
+ background: #444;
+ color: #ddd;
+ text-align: center;
+ font-size: 12pt;
+ border-radius: 3px;
- for( var i = 0; i < initial.length; i++)
- {
- select_item(String(initial[i]));
}
- if(initial.length == 1)
- {
- search(items[initial[0]]["small_name"]);
- document.getElementById("user_field").value = items[initial[0]]["small_name"];
- }
- }
- function build_all_tries(dict)
- {
- for( var i in dict )
- {
- add_item(dict[i]);
+ #drop_results {
+ max-width: 100%;
+ display: inline-block;
+ list-style-type: none;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
}
- }
-
- function add_item(item)
- {
- var id = item['id'];
- add_to_tree(item['expanded_name'], id, expanded_name_trie);
- add_to_tree(item['small_name'], id, small_name_trie);
- add_to_tree(item['string'], id, string_trie);
- }
-
- function add_to_tree(str, id, trie)
- {
- inner_trie = trie;
- while( str )
- {
- if( !inner_trie[str.charAt(0)] )
- {
- new_trie = {};
- inner_trie[str.charAt(0)] = new_trie;
- }
- else
- {
- new_trie = inner_trie[str.charAt(0)];
- }
- if( str.length == 1 )
- {
- new_trie.isComplete = true;
- new_trie.itemID = id;
- }
- inner_trie = new_trie;
- str = str.substring(1);
+ #drop_results li {
+ overflow: hidden;
+ text-overflow: ellipsis;
}
- }
- function search(input)
- {
- if( input.length == 0 && !show_from_noentry){
- dropdown([]);
- return;
- }
- else if( input.length == 0 && show_from_noentry)
- {
- dropdown(items); //show all items
- }
- else
- {
- var trees = []
- var tr1 = getSubtree(input, expanded_name_trie);
- trees.push(tr1);
- var tr2 = getSubtree(input, small_name_trie);
- trees.push(tr2);
- var tr3 = getSubtree(input, string_trie);
- trees.push(tr3);
- var results = collate(trees);
- dropdown(results);
+ #added_counter {
+ text-align: center;
}
- }
-
- function getSubtree(input, given_trie)
- {
- /*
- recursive function to return the trie accessed at input
- */
- if( input.length == 0 ){
- return given_trie;
+ #added_number, #addable_limit {
+ display: inline;
}
+ </style>
+</div>
- else{
- var substr = input.substring(0, input.length - 1);
- var last_char = input.charAt(input.length-1);
- var subtrie = getSubtree(substr, given_trie);
- if( !subtrie ) //substr not in the trie
- {
- return {};
- }
- var indexed_trie = subtrie[last_char];
- return indexed_trie;
- }
- }
+<script type="text/javascript">
+ function searchableSelectMultipleWidgetEntry() {
+ let format_vars = {
+ "show_from_noentry": {{show_from_noentry|yesno:"true,false"}},
+ "show_x_results": {{show_x_results|default:-1}},
+ "results_scrollable": {{results_scrollable|yesno:"true,false"}},
+ "selectable_limit": {{selectable_limit|default:-1}},
+ "placeholder": "{{placeholder|default:"begin typing"}}"
+ };
- function serialize(trie)
- {
- /*
- takes in a trie and returns a list of its item id's
- */
- var itemIDs = [];
- if ( !trie )
- {
- return itemIDs; //empty, base case
- }
- for( var key in trie )
- {
- if(key.length > 1)
- {
- continue;
- }
- itemIDs = itemIDs.concat(serialize(trie[key]));
- }
- if ( trie.isComplete )
- {
- itemIDs.push( trie.itemID );
- }
+ let field_dataset = {{items|safe}};
- return itemIDs;
- }
+ let field_initial = {{ initial|safe }};
- function collate(trees)
- {
- /*
- takes a list of tries
- returns a list of ids of objects that are available
- */
- results = [];
- for( var i in trees )
- {
- var available_IDs = serialize(trees[i]);
- for( var j=0; j<available_IDs.length; j++){
- var itemID = available_IDs[j];
- results[itemID] = items[itemID];
- }
- }
- return results;
+ //global
+ searchable_select_multiple_widget = new SearchableSelectMultipleWidget(format_vars, field_dataset, field_initial);
}
- function dropdown(ids)
- {
- /*
- takes in a mapping of ids to objects in items
- and displays them in the dropdown
- */
- var drop = document.getElementById("drop_results");
- while(drop.firstChild)
- {
- drop.removeChild(drop.firstChild);
- }
-
- for( var id in ids )
- {
- var result_entry = document.createElement("li");
- var result_button = document.createElement("a");
- var obj = items[id];
- var result_text = document.createTextNode(obj['small_name'] + " : " + obj['expanded_name']);
- result_button.appendChild(result_text);
- result_button.setAttribute('onclick', 'select_item("' + obj['id'] + '")');
- result_entry.appendChild(result_button);
- drop.appendChild(result_entry);
- }
+ searchableSelectMultipleWidgetEntry();
- if( !drop.firstChild )
- {
- drop.style.display = 'none';
- }
- else
- {
- drop.style.display = 'inherit';
- }
- }
+ /*
+ var show_from_noentry = context(show_from_noentry|yesno:"true,false") // whether to show any results before user starts typing
+ var show_x_results = context(show_x_results|default:-1) // how many results to show at a time, -1 shows all results
+ var results_scrollable = {{results_scrollable|yesno:"true,false") // whether list should be scrollable
+ var selectable_limit = {{selectable_limit|default:-1) // how many selections can be made, -1 allows infinitely many
+ var placeholder = "context(placeholder|default:"begin typing")" // placeholder that goes in text box
- function select_item(item_id)
- {
- //TODO make faster
- var item = items[item_id];
- if( (selectable_limit > -1 && added_items.length < selectable_limit) || selectable_limit < 0 )
+ needed info
+ var items = context(items|safe) // items to add to trie. Type is a dictionary of dictionaries with structure:
{
- if( added_items.indexOf(item) == -1 )
- {
- added_items.push(item);
+ id# : {
+ "id": any, identifiable on backend
+ "small_name": string, displayed first (before separator), searchable (use for e.g. username)
+ "expanded_name": string, displayed second (after separator), searchable (use for e.g. email address)
+ "string": string, not displayed, still searchable
}
}
- update_selected_list();
- document.getElementById("user_field").focus();
- }
-
- function remove_item(item_ref)
- {
- item = Object.values(items)[item_ref];
- var index = added_items.indexOf(item);
- added_items.splice(index, 1);
-
- update_selected_list()
- document.getElementById("user_field").focus();
- }
-
- function edit_item(item_id){
- var wf_type = "{{wf_type}}";
- parent.add_edit_wf(wf_type, item_id);
- }
-
- function update_selected_list()
- {
- document.getElementById("added_number").innerText = added_items.length;
- selector = document.getElementById('selector');
- selector.value = JSON.stringify(added_items);
- added_list = document.getElementById('added_list');
-
- while(selector.firstChild)
- {
- selector.removeChild(selector.firstChild);
- }
- while(added_list.firstChild)
- {
- added_list.removeChild(added_list.firstChild);
- }
-
- list_html = "";
-
- for( var key in added_items )
- {
- item = added_items[key];
-
- list_html += '<div class="list_entry"><p class="full_name">'
- + item["expanded_name"]
- + '</p><p class="small_name">, '
- + item["small_name"]
- + '</p><button onclick="remove_item('
- + Object.values(items).indexOf(item)
- + ')" class="btn-remove btn">remove</button>';
- {% if edit %}
- list_html += '<button onclick="edit_item('
- + item['id']
- + ')" class="btn-remove btn">edit</button>';
- {% endif %}
- list_html += '</div>';
- }
-
- added_list.innerHTML = list_html;
- }
+ used later:
+ context(selectable_limit): changes what number displays for field
+ context(name): form identifiable name, relevant for backend
+ // when submitted, form will contain field data in post with name as the key
+ context(placeholder): "greyed out" contents put into search field initially to guide user as to what they're searching for
+ context(initial): in search_field_init(), marked safe, an array of id's each referring to an id from items
+ */
</script>
-<style>
- .full_name {
- display: inline-block;
- }
- .small_name {
- display: inline-block;
- }
-</style>
diff --git a/dashboard/src/templates/dashboard/table.html b/dashboard/src/templates/dashboard/table.html
index b3f4b5f..0a37ded 100644
--- a/dashboard/src/templates/dashboard/table.html
+++ b/dashboard/src/templates/dashboard/table.html
@@ -4,11 +4,12 @@
{% block extrahead %}
{{ block.super }}
<!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
rel="stylesheet">
<!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}" rel="stylesheet">
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
+ rel="stylesheet">
{% endblock extrahead %}
{% block content %}
@@ -34,8 +35,8 @@
{% block extrajs %}
<!-- DataTables JavaScript -->
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
<script src={% static "js/dataTables-sort.js" %}></script>
diff --git a/dashboard/src/templates/layout.html b/dashboard/src/templates/layout.html
index 13f182b..d37d4f5 100644
--- a/dashboard/src/templates/layout.html
+++ b/dashboard/src/templates/layout.html
@@ -20,7 +20,7 @@
<link href="{% static "bower_components/metisMenu/dist/metisMenu.min.css" %}" rel="stylesheet">
<!-- Custom Fonts -->
- <link href="{% static "bower_components/font-awesome/css/font-awesome.min.css" %}"
+ <link href="{% static "bower_components/Font-Awesome/css/all.min.css" %}"
rel="stylesheet" type="text/css">
<!-- Favicon -->
@@ -50,17 +50,12 @@
{#<!-- jQuery -->#}
{#<script src="{% static "bower_components/jquery/dist/jquery.min.js" %}"></script>#}
{#<script src="{% static "bower_components/jquery-migrate/jquery-migrate.min.js" %}"></script>#}
-
+<!-- Popper.js -->
+<script src="{% static "bower_components/popper.js/dist/umd/popper.min.js" %}"></script>
{#<script src="https://code.jquery.com/jquery-2.2.0.min.js"></script>#}
<!-- Bootstrap Core JavaScript -->
<script src="{% static "bower_components/bootstrap/dist/js/bootstrap.min.js" %}"></script>
-<!-- Metis Menu Plugin JavaScript -->
-<script src="{% static "bower_components/metisMenu/dist/metisMenu.min.js" %}"></script>
-
-<!-- Custom Theme JavaScript -->
-<script src="{% static "bower_components/startbootstrap-sb-admin-2-blackrockdigital/dist/js/sb-admin-2.min.js" %}"></script>
-
{% block extrajs %}
{% endblock extrajs %}
</body>
diff --git a/dashboard/src/templates/notifier/inbox.html b/dashboard/src/templates/notifier/inbox.html
index 471eae4..72207ed 100644
--- a/dashboard/src/templates/notifier/inbox.html
+++ b/dashboard/src/templates/notifier/inbox.html
@@ -6,81 +6,124 @@
{% block content %}
<style media="screen">
-
- .inbox-panel {
- display: grid;
- grid-template-columns: 30% 70%;
- }
-
- .section-panel {
- padding: 10px;
- }
-
- .iframe-panel {
- padding: 0px;
- margin-top: 0px;
- }
-
- .card-container {
- box-shadow: 0 0 5px 2px #cccccc;
- }
- .card {
- height: 50px;
- position: relative;
- border-bottom: 1px solid #cccccc;
- padding: 10px;
- width: 100%;
- background-color: #ffffff;
- z-index: 5;
- }
- .selected-card {
- background-color: #f3f3f3;
- }
-
- .card:hover {
- box-shadow: 0px 0 5px 2px #cccccc;
- z-index: 6;
- }
-
- #inbox-iframe {
- height: calc(100vh - 130px);
- }
-
- .half_width {
- width: 50%;
- }
- .card-wrapper {
- }
+ .inbox-panel {
+ display: grid;
+ grid-template-columns: 30% 5% 65%;
+ }
+
+ .section-panel {
+ padding: 10px;
+ }
+
+ .iframe-panel {
+ padding: 0px;
+ margin-top: 0px;
+ }
+
+ .card-container {
+ border: 1px solid #cccccc;
+ border-bottom: 0px;
+ }
+
+ .card {
+ height: 50px;
+ position: relative;
+ border-bottom: 1px solid #cccccc;
+ padding: 10px;
+ width: 100%;
+ background-color: #ffffff;
+ z-index: 5;
+ }
+
+ .selected-card {
+ background-color: #f3f3f3;
+ }
+
+ .card:hover {
+ box-shadow: 0px 0 5px 2px #cccccc;
+ z-index: 6;
+ }
+
+ .half_width {
+ width: 50%;
+ }
+
+ #page-wrapper {
+ padding: 0px;
+ }
+
+ .read_notification {
+ background-color: #efefef;
+ }
+
+ .scrollable {
+ overflow-y: auto;
+ }
</style>
-
-<div class="inbox-panel">
- <div class="section-panel">
- <div class="card-container">
- {% for notification in notifications %}
- <div class="inbox-entry card" onclick="showmessage({{notification.id}}); setactive(this);">
- {{ notification }}
+<div class="container-fluid d-flex flex-grow-1 flex-column">
+ <div class="row mt-3 mb-2">
+ <div class="col-2 px-0">
+ <div class="btn-group w-100" id="filterGroup">
+ <button class="btn btn-secondary active" data-read="-1">All</button>
+ <button class="btn btn-secondary" data-read="0">Unread</button>
+ <button class="btn btn-secondary" data-read="1">Read</button>
+ </div>
+ </div>
+ </div>
+ <div class="row flex-grow-1" id="fixHeight">
+ <!-- Notification list && Controls -->
+ <div class="mb-2 mb-lg-0 col-lg-2 px-0 mh-100">
+ <div class="list-group rounded-0 rounded-left scrollable mh-100 notifications" id="unreadNotifications" data-read="0">
+ {% for notification in unread_notifications %}
+ <a
+ href="#"
+ onclick="showmessage({{notification.id}}); setactive(this);"
+ class="list-group-item list-group-item-action notification">
+ {{ notification }}
+ </a>
+ {% endfor %}
+ </div>
+ <div class="list-group rounded-0 rounded-left scrollable mh-100 notifications" id="readNotifications" data-read="1">
+ {% for notification in read_notifications %}
+ <a
+ href="#"
+ onclick="showmessage({{notification.id}}); setactive(this);"
+ class="list-group-item list-group-item-action list-group-item-secondary notification">
+ {{ notification }}
+ </a>
+ {% endfor %}
+ </div>
+ </div>
+ <!-- Content -->
+ <div class="col ml-lg-2 border mh-100 p-4">
+ <iframe class="w-100 h-100" id="inbox-iframe" frameBorder="0" scrolling="yes">Please select a notification</iframe>
</div>
- {% endfor %}
</div>
- </div>
- <div class="iframe-panel inbox-expanded-view">
- <div class="inbox-iframe-div">
- <iframe id="inbox-iframe" frameBorder="0" width="100%" height="100vh" scrolling="yes" onload="sizetoiframe(this);">Please select a notification</iframe>
- </div>
- </div>
</div>
<script type="text/javascript">
- $('#inbox-iframe').load(function() {
- sizetoiframe(this);
- })
-
- function showmessage(msg_id)
- {
- iframe = document.getElementById("inbox-iframe");
- iframe.src = "notification/" + msg_id;
- }
-
+ function showmessage(msg_id) {
+ iframe = document.getElementById("inbox-iframe");
+ iframe.src = "notification/" + msg_id;
+ }
+
+ function setactive(obj) {
+ $(".notification").removeClass("active");
+ $(obj).addClass("active");
+ }
+
+ $(document).ready(function(){
+ // For all / unread / read
+ $("#filterGroup button").click(function(){
+ let read = $(this).attr("data-read");
+ $(this).siblings().removeClass("active");
+ $(".notifications").addClass("d-none");
+ $(this).addClass("active");
+ if (read === "-1") {
+ return $(".notifications").removeClass("d-none");
+ }
+ $(`.notifications[data-read="${read}"]`).removeClass("d-none");
+ });
+ });
</script>
-
-{% endblock %}
+{% endblock %} \ No newline at end of file
diff --git a/dashboard/src/templates/notifier/notification.html b/dashboard/src/templates/notifier/notification.html
index 65d26c9..0eafa60 100644
--- a/dashboard/src/templates/notifier/notification.html
+++ b/dashboard/src/templates/notifier/notification.html
@@ -2,19 +2,55 @@
{% block extrahead %}
<base target="_parent">
{% endblock %}
+
{% block basecontent %}
-<div class="card-container">
-<h3 class="msg_header">{{notification.title}}</h3>
-<p class="content"></p>
-<pre>
-{{notification.content|safe}}
-</pre>
+<script>
+ function send_request(post_data){
+ var form = $("#notification_action_form");
+ var formData = form.serialize() + '&' + post_data + '=true';
+ var req = new XMLHttpRequest();
+ req.open("POST", ".", false);
+ req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ req.onerror = function() { alert("problem occurred while trying to cancel current workflow"); }
+ req.onreadystatechange = function() { if(req.readyState === 4){
+ window.top.location.href += '';
+ }};
+ req.send(formData);
+ }
+ function delete_notification()
+ {
+ send_request("delete");
+ }
+ function mark_unread()
+ {
+ send_request("unread");
+ }
+</script>
+<div>
+ <h3 class="msg_header">{{notification.title}}
+ <div class="btn_group">
+ <button class="btn btn-primary inbox-btn" onclick="mark_unread()">Mark Unread</button>
+ <button class="btn btn-danger inbox-btn" onclick="delete_notification()">Delete</button>
+ </div>
+ </h3>
</div>
+<p class="content-divider"></p>
+
+{% if not notification.is_html %}
+<pre>
+{% endif %}
+ {{notification.content|safe}}
+{% if not notification.is_html %}
+</pre>
+{% endif %}
+<form id="notification_action_form" action="." method="post">
+ {% csrf_token %}
+</form>
+
<style media="screen">
.card-container {
- box-shadow: 0 0 5px 2px #cccccc;
border: 1px solid #ffffff;
margin-top: 11px;
}
@@ -28,11 +64,20 @@
background-color: #ffffff;
z-index: 5;
}
-
.sender {
color: #636363;
}
-
-
+ .content-divider {
+ border-bottom: 1px solid #cccccc;
+ padding-bottom: 15px;
+ clear: right;
+ }
+ .inbox-btn{
+ display: inline;
+ margin: 3px;
+ }
+ .btn_group{
+ float: right;
+ }
</style>
{% endblock %}
diff --git a/dashboard/src/templates/resource/hostprofile_detail.html b/dashboard/src/templates/resource/hostprofile_detail.html
new file mode 100644
index 0000000..dc20600
--- /dev/null
+++ b/dashboard/src/templates/resource/hostprofile_detail.html
@@ -0,0 +1,102 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+
+{% block content %}
+<div class="row">
+ <div class="col-lg-6">
+ <div class="card mb-4">
+ <div class="card-header d-flex">
+ <h4 style="display: inline;">Available at</h4>
+ <button data-toggle="collapse" data-target="#avilableAt" class="btn ml-auto btn-outline-secondary">Expand</button>
+ </div>
+ <div class="card-body collapse show" id="avilableAt">
+ <ul class="list-group">
+ {% for lab in hostprofile.labs.all %}
+ <li class="list-group-item">{{lab.name}}</li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+ <div class="card mb-4">
+ <div class="card-header d-flex">
+ <h4 style="display: inline;">RAM</h4>
+ <button data-toggle="collapse" data-target="#ramPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
+ </div>
+ <div class="card-body collapse show" id="ramPanel">
+ {{hostprofile.ramprofile.first.amount}}G,
+ {{hostprofile.ramprofile.first.channels}} channels
+ </div>
+ </div>
+ <div class="card mb-4">
+ <div class="card-header d-flex">
+ <h4 style="display: inline;">CPU</h4>
+ <button data-toggle="collapse" data-target="#cpuPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
+ </div>
+ <div class="card-body collapse show" id="cpuPanel">
+ <table class="table">
+ <tr>
+ <td>Arch:</td>
+ <td>{{hostprofile.cpuprofile.first.architecture}}</td>
+ </tr>
+ <tr>
+ <td>Cores:</td>
+ <td>{{hostprofile.cpuprofile.first.cores}}</td>
+ </tr>
+ <tr>
+ <td>Sockets:</td>
+ <td>{{hostprofile.cpuprofile.first.cpus}}</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <div class="card mb-4">
+ <div class="card-header d-flex">
+ <h4 style="display: inline;">Disk</h4>
+ <button data-toggle="collapse" data-target="#diskPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
+ </div>
+ <div class="card-body collapse show" id="diskPanel">
+ <table class="table">
+ <tr>
+ <td>Size:</td>
+ <td>{{hostprofile.storageprofile.first.size}} GiB</td>
+ </tr>
+ <tr>
+ <td>Type:</td>
+ <td>{{hostprofile.storageprofile.first.media_type}}</td>
+ </tr>
+ <tr>
+ <td>Mount Point:</td>
+ <td>{{hostprofile.storageprofile.first.name}}</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-6">
+ <div class="card">
+ <div class="card-header d-flex">
+ <h4 style="display: inline;">Interfaces</h4>
+ <button data-toggle="collapse" data-target="#interfacePanel" class="btn ml-auto btn-outline-secondary">Expand</button>
+ </div>
+ <div class="card-body collapse show" id="interfacePanel">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Speed</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for intprof in hostprofile.interfaceprofile.all %}
+ <tr>
+ <td>{{intprof.name}}</td>
+ <td>{{intprof.speed}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+</div>
+{% endblock content %}
diff --git a/dashboard/src/templates/resource/hosts.html b/dashboard/src/templates/resource/hosts.html
index 4a53b45..69b7231 100644
--- a/dashboard/src/templates/resource/hosts.html
+++ b/dashboard/src/templates/resource/hosts.html
@@ -17,7 +17,7 @@
{{ host.name }}
</td>
<td>
- {{ host.profile }}
+ <a href="profiles/{{ host.profile.id }}">{{ host.profile }}</a>
</td>
<td>
{{ host.booked }}
@@ -35,10 +35,7 @@
$(document).ready(function () {
$('#table').DataTable({
scrollX: true,
- columnDefs: [
- {type: 'status', targets: 6}
- ],
- "order": [[6, "asc"]]
+ "order": [[0, "asc"]]
});
});
</script>
diff --git a/dashboard/src/templates/resource/steps/define_hardware.html b/dashboard/src/templates/resource/steps/define_hardware.html
index 933b4ab..57078e9 100644
--- a/dashboard/src/templates/resource/steps/define_hardware.html
+++ b/dashboard/src/templates/resource/steps/define_hardware.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
<p>Note that not all labs host every kind of machine.
@@ -15,20 +15,7 @@ with your current configuration will become unavailable.</p>
</form>
{% endblock content %}
{% block onleave %}
-var normalize = function(data){
- //converts the top level keys in data to map to lists
- var normalized = {}
- for( var key in data ){
- normalized[key] = [];
- for( var subkey in data[key] ){
- normalized[key].push(data[key][subkey]);
- }
- }
- return normalized;
-}
-var data = normalize(result);
-data = JSON.stringify(data);
-document.getElementById("filter_field").value = data;
+multi_filter_widget.finish();
var formData = $("#define_hardware_form").serialize();
req = new XMLHttpRequest();
req.open('POST', '/wf/workflow/', false);
diff --git a/dashboard/src/templates/resource/steps/host_info.html b/dashboard/src/templates/resource/steps/host_info.html
index 0275727..bbbafdc 100644
--- a/dashboard/src/templates/resource/steps/host_info.html
+++ b/dashboard/src/templates/resource/steps/host_info.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/resource/steps/meta_info.html b/dashboard/src/templates/resource/steps/meta_info.html
index b458842..cebd343 100644
--- a/dashboard/src/templates/resource/steps/meta_info.html
+++ b/dashboard/src/templates/resource/steps/meta_info.html
@@ -1,10 +1,32 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
+<style>
+#resource_meta_form {
+ padding: 80px;
+ display: grid;
+}
+
+#resource_meta_form td > * {
+ width: 100%;
+ margin-bottom: 20px;
+ margin-top: 20px;
+}
+
+#resource_meta_form > table > tbody > tr {
+ border-bottom: 1px solid #cccccc;
+}
+
+#resource_meta_form > table > tbody > tr:last-child {
+ border-bottom: none;
+}
+
+</style>
+
<form id="resource_meta_form" method="post" action="/wf/workflow/">
{% csrf_token %}
<table>
diff --git a/dashboard/src/templates/resource/steps/pod_definition.html b/dashboard/src/templates/resource/steps/pod_definition.html
index 8599bb0..5826ccb 100644
--- a/dashboard/src/templates/resource/steps/pod_definition.html
+++ b/dashboard/src/templates/resource/steps/pod_definition.html
@@ -8,629 +8,7 @@
var mxLoadStylesheets = false;
</script>
<script type="text/javascript" src="/static/js/mxClient.min.js" ></script>
-<style>
-p {
- word-break: normal;
- white-space: normal;
-}
-</style>
-<script type="text/javascript">
-var currentWindow;
-var currentGraph;
-var netCount = 0;
-var netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
-var hostCount = 0;
-var lastHostBottom = 100;
-var networks = new Set([]);
-var network_names = new Set([]);
-var has_public_net = false;
-var vlans = {{vlans|default:'null'}};
-var vlan_string = "";
-
-function main(graphContainer, overviewContainer, toolbarContainer) {
- if(vlans){
- for(var i=0; i<vlans.length-1; i++){
- vlan_string += vlans[i] + ", ";
- }
- if(vlans.length > 0){
- vlan_string += vlans[vlans.length-1];
- }
-
- var str = "Available vlans for your POD: " + vlan_string;
- document.getElementById("vlan_notice").innerHTML = str;
- }
- //check if the browser is supported
- if (!mxClient.isBrowserSupported()) {
- mxUtils.error('Browser is not supported', 200, false);
- return null;
- }
-
- // Workaround for Internet Explorer ignoring certain styles
- if (mxClient.IS_QUIRKS) {
- document.body.style.overflow = 'hidden';
- new mxDivResizer(graphContainer);
- }
- var editor = new mxEditor();
- var graph = editor.graph;
- var model = graph.getModel();
- editor.setGraphContainer(graphContainer);
-
- {% if debug %}
- editor.addAction('printXML', function(editor, cell) {
- mxLog.write(encodeGraph(graph));
- mxLog.show();
- });
- {% endif %}
-
-
- doGlobalConfig(graph);
- currentGraph = graph;
-
- {% if xml %}
- restoreFromXml('{{xml|safe}}', editor);
- {% elif hosts %}
- {% for host in hosts %}
-
- var host = {{host|safe}};
- makeHost(host);
- {% endfor %}
- {% endif %}
- {% if added_hosts %}
- {% for host in added_hosts %}
- var host = {{host|safe}}
- makeHost(host);
- {% endfor %}
- updateHosts([]);
- {% endif %}
-
- addToolbarButton(editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
- addToolbarButton(editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
-
- {% if debug %}
- addToolbarButton(editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
- {% endif %}
-
- var outline = new mxOutline(graph, overviewContainer);
-
-
- var checkAllowed = function(edge, terminal, source) {
- //check if other terminal is null, and that they are different
- otherTerminal = edge.getTerminal(!source);
- if(terminal != null && otherTerminal != null) {
- if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
- otherTerminal.getParent().getId().split('_')[0] ) {
- //not allowed
- graph.removeCells([edge]);
- return false;
- }
- }
- return true;
- };
-
- var colorEdge = function(edge, terminal, source) {
- if(terminal.getParent().getId().indexOf('network') >= 0) {
- styles = terminal.getParent().getStyle().split(';');
- color = 'black';
- for(var i=0; i<styles.length; i++){
- kvp = styles[i].split('=');
- if(kvp[0] == "fillColor"){
- color = kvp[1];
- }
- }
- edge.setStyle('strokeColor=' + color);
- }
- };
-
- var alertVlan = function(edge, terminal, source) {
- if( terminal == null || edge.getTerminal(!source) == null) {
- return;
- }
- var vlanHTML = '<form> <input type="radio" name="tagged" value="True" checked> Tagged<br>'
- vlanHTML += '<input type="radio" name="tagged" value="False"> Untagged </form>'
- vlanHTML += '<button onclick=parseVlanWindow(' + edge.getId() + ');>Okay</button>'
- vlanHTML += '<button onclick=deleteVlanWindow(' + edge.getId() + ');>Cancel</button>'
- content = document.createElement('div');
- content.innerHTML = vlanHTML;
- showWindow(graph, "Vlan Selection", content, 200, 200);
- }
-
- //sets the edge color to be the same as the network
- graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event){
- edge = event.getProperty('edge');
- terminal = event.getProperty('terminal')
- source = event.getProperty('source');
- if(checkAllowed(edge, terminal, source)) {
- colorEdge(edge, terminal, source);
- alertVlan(edge, terminal, source);
- }
- });
-
- createDeleteDialog = function(id)
- {
- var content = document.createElement('div');
- var innerHTML = "<button style='width: 46%;' onclick=deleteCell('" + id + "');>Remove</button>"
- innerHTML += "<button style='width: 46%;' onclick='currentWindow.destroy();'>Cancel</button>"
- content.innerHTML = innerHTML;
- showWindow(currentGraph, 'Do you want to delete this network?', content, 200, 62);
- }
- graph.dblClick = function(evt, cell) {
-
- if( cell != null ){
- if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
- cell = cell.getParent();
- }
- if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
- createDeleteDialog(cell.getId());
- }
- else {
- showDetailWindow(cell);
- }
- }
- };
- graph.setCellsSelectable(false);
- graph.setCellsMovable(false);
-
- updateHosts({{ removed_hosts|default:"[]"|safe }});
- if(!has_public_net){
- addPublicNetwork();
- }
-}
-
-function showDetailWindow(cell) {
- var info = JSON.parse(cell.getValue());
- var content = document.createElement("div");
- var inner = "<pre>Name: " + info.name + "\n";
- inner += "Description:\n" + info.description + "</pre>";
- inner += '<button onclick="currentWindow.destroy();">Okay</button>'
- content.innerHTML = inner
- showWindow(currentGraph, 'Details', content, 400, 400);
-}
-
-function restoreFromXml(xml, editor) {
- var doc = mxUtils.parseXml(xml);
- var node = doc.documentElement;
- editor.readGraphModel(node);
-
- //Iterate over all children, and parse the networks to add them to the sidebar
- var root = currentGraph.getDefaultParent();
- for( var i=0; i<root.getChildCount(); i++) {
- var cell = root.getChildAt(i);
- if(cell.getId().indexOf("network") > -1) {
- var info = JSON.parse(cell.getValue());
- var vlan_id = info['vlan_id'];
- networks.add(vlan_id);
- var name = info['name'];
- network_names.add(name);
- var styles = cell.getStyle().split(";");
- var color = null;
- for(var j=0; j< styles.length; j++){
- var kvp = styles[j].split('=');
- if(kvp[0] == "fillColor") {
- color = kvp[1];
- break;
- }
- }
- if(info.public){
- vlan_id = "";
- has_public_net = true;
- }
- netCount++;
- makeSidebarNetwork(name, vlan_id, color, cell.getId());
- }
- }
-}
-
-function deleteCell(cellId) {
- var cell = currentGraph.getModel().getCell(cellId);
- if( cellId.indexOf("network") > -1 ) {
- elem = document.getElementById(cellId);
- elem.parentElement.removeChild(elem);
- }
- currentGraph.removeCells([cell]);
- currentWindow.destroy();
-
-}
-
-function newNetworkWindow() {
- var innerHtml = 'Name: <input type="text" name="net_name" id="net_name_input" style="margin:5px;"><br>';
- innerHtml += 'Vlan: <input type="number" step="1" name="vlan_id" id="vlan_id_input" style="margin:5px;"><br>';
- innerHtml += '<button style="width: 46%;" onclick="parseNetworkWindow()">Okay</button>';
- innerHtml += '<button style="width: 46%;" onclick="currentWindow.destroy();">Cancel</button><br>';
- innerHtml += '<div id="current_window_vlans"/>';
- innerHtml += '<div id="current_window_errors"/>';
- var content = document.createElement("div");
- content.innerHTML = innerHtml;
-
- showWindow(currentGraph, "Network Creation", content, 300, 300);
-
- if(vlans){
- vlan_notice = document.getElementById("current_window_vlans");
- vlan_notice.appendChild(document.createTextNode("Available Vlans: " + vlan_string));
- }
-}
-
-function parseNetworkWindow() {
- var net_name = document.getElementById("net_name_input").value
- var vlan_id = document.getElementById("vlan_id_input").value
- var error_div = document.getElementById("current_window_errors");
- var vlan_valid = Number.isInteger(Number(vlan_id)) && (vlan_id < 4095) && (vlan_id > 1)
- if(vlans){
- vlan_valid = vlan_valid & vlans.indexOf(Number(vlan_id)) >= 0;
- }
- if( !vlan_valid)
- {
- error_div.innerHTML = "Please only enter an integer in the valid range (default 1-4095) for the VLAN ID";
- return;
- }
- if( networks.has(vlan_id))
- {
- error_div.innerHTML = "All VLAN IDs must be unique";
- return;
- }
- if( network_names.has(net_name) ){
- error_div.innerHTML = "All network names must be unique";
- return;
- }
- addNetwork(net_name, vlan_id);
- currentWindow.destroy();
-}
-
-function addToolbarButton(editor, toolbar, action, label, image, isTransparent)
-{
- var button = document.createElement('button');
- button.style.fontSize = '10';
- if (image != null)
- {
- var img = document.createElement('img');
- img.setAttribute('src', image);
- img.style.width = '16px';
- img.style.height = '16px';
- img.style.verticalAlign = 'middle';
- img.style.marginRight = '2px';
- button.appendChild(img);
- }
- if (isTransparent)
- {
- button.style.background = 'transparent';
- button.style.color = '#FFFFFF';
- button.style.border = 'none';
- }
- mxEvent.addListener(button, 'click', function(evt)
- {
- editor.execute(action);
- });
- mxUtils.write(button, label);
- toolbar.appendChild(button);
-};
-
-function encodeGraph(graph) {
- var encoder = new mxCodec();
- var xml = encoder.encode(graph.getModel());
- return mxUtils.getXml(xml);
-}
-
-function doGlobalConfig(graph) {
- //general graph stuff
- graph.setMultigraph(false);
-
- //edge behavior
- graph.setConnectable(true);
- graph.setAllowDanglingEdges(false);
- mxEdgeHandler.prototype.snapToTerminals = true;
- mxConstants.MIN_HOTSPOT_SIZE = 16;
- mxConstants.DEFAULT_HOTSPOT = 1;
- //edge 'style' (still affects behavior greatly)
- style = graph.getStylesheet().getDefaultEdgeStyle();
- style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
- style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
- style[mxConstants.STYLE_ROUNDED] = true;
- style[mxConstants.STYLE_FONTCOLOR] = 'black';
- style[mxConstants.STYLE_STROKECOLOR] = 'red';
-
- style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
- style[mxConstants.STYLE_STROKEWIDTH] = '3';
- style[mxConstants.STYLE_ROUNDED] = true;
- style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
-
- // TODO: Proper override
- graph.convertValueToString = function(cell) {
- try{
- //changes value for edges with xml value
- if(cell.isEdge()) {
- if(JSON.parse(cell.getValue())["tagged"]) {
- return "tagged";
- }
- return "untagged";
- } else{
- return JSON.parse(cell.getValue())['name'];
- }
- }
- catch(e){
- return cell.getValue();
- }
- };
-}
-
-function showWindow(graph, title, content, width, height) {
- //create transparent black background
- var background = document.createElement('div');
- background.style.position = 'absolute';
- background.style.left = '0px';
- background.style.top = '0px';
- background.style.right = '0px';
- background.style.bottom = '0px';
- background.style.background = 'black';
- mxUtils.setOpacity(background, 50);
- document.body.appendChild(background);
-
- //deal with IE quirk
- if (mxClient.IS_IE) {
- new mxDivResizer(background);
- }
-
- var x = Math.max(0, document.body.scrollWidth/2-width/2);
- var y = Math.max(10, (document.body.scrollHeight ||
- document.documentElement.scrollHeight)/2-height*2/3);
-
- var wnd = new mxWindow(title, content, x, y, width, height, false, true);
- wnd.setClosable(false);
-
- wnd.addListener(mxEvent.DESTROY, function(evt) {
- graph.setEnabled(true);
- mxEffects.fadeOut(background, 50, true, 10, 30, true);
- });
- currentWindow = wnd;
-
- graph.setEnabled(false);
- wnd.setVisible(true);
-};
-
-function closeWindow() {
- //allows the current window to be destroyed
- currentWindow.destroy();
-};
-
-function othersUntagged(edgeID) {
- var edge = currentGraph.getModel().getCell(edgeID);
- var end1 = edge.getTerminal(true);
- var end2 = edge.getTerminal(false);
-
- if( end1.getParent().getId().split('_')[0] == 'host' )
- {
- var netint = end1;
- }
- else
- {
- var netint = end2;
- }
-
- var edges = netint.edges;
-
- for( var i=0; i < edges.length; i++ )
- {
- if( edges[i].getValue() )
- {
- var tagged = JSON.parse(edges[i].getValue()).tagged;
- }
- else
- {
- var tagged = true;
- }
- if( !tagged )
- {
- return true;
- }
- }
- return false;
-};
-
-
-function deleteVlanWindow(edgeID) {
- var cell = currentGraph.getModel().getCell(edgeID);
- currentGraph.removeCells([cell]);
- currentWindow.destroy();
-}
-
-function parseVlanWindow(edgeID) {
- //do parsing and data manipulation
- var radios = document.getElementsByName("tagged");
- edge = currentGraph.getModel().getCell(edgeID);
-
- for(var i=0; i<radios.length; i++) {
- if(radios[i].checked) {
- //set edge to be tagged or untagged
- //cellValue.setAttribute("tagged", radios[i].value);
- if( radios[i].value == "False")
- {
- if( othersUntagged(edgeID) )
- {
- alert("Only one untagged VLAN is allowed per interface");
- return;
- }
- }
- edgeVal = Object();
- edgeVal['tagged'] = radios[i].value == "True";
- edge.setValue(JSON.stringify(edgeVal));
- break;
- }
- }
- //edge.setValue(cellValue);
- currentGraph.refresh(edge);
- closeWindow();
-}
-
-function makeMxNetwork(vlan_id, net_name) {
- model = currentGraph.getModel();
- width = 10;
- height = 1700;
- xoff = 400 + (30 * netCount);
- yoff = -10;
- var color = netColors[netCount];
- if( netCount > (netColors.length - 1)) {
- color = Math.floor(Math.random() * 16777215); //int in possible color space
- color = '#' + color.toString(16).toUpperCase(); //convert to hex
- //alert(color);
- }
- var net_val = Object();
- net_val['vlan_id'] = vlan_id;
- net_val['name'] = net_name;
- net_val['public'] = vlan_id < 0;
- net = currentGraph.insertVertex(
- currentGraph.getDefaultParent(),
- 'network_' + netCount,
- JSON.stringify(net_val),
- xoff,
- yoff,
- width,
- height,
- 'fillColor=' + color,
- false
- );
- var num_ports = 45;
- for(var i=0; i<num_ports; i++){
- port = currentGraph.insertVertex(
- net,
- null,
- '',
- 0,
- (1/num_ports) * i,
- 10,
- height / num_ports,
- 'fillColor=black;opacity=0',
- true
- );
- }
-
- var retVal = Object();
- retVal['color'] = color;
- retVal['element_id'] = "network_" + netCount;
-
- netCount++;
- return retVal;
-}
-
-function addPublicNetwork() {
- var net = makeMxNetwork(-1, "public");
- network_names.add("public");
- makeSidebarNetwork("public", "", net['color'], net['element_id']);
-}
-
-function addNetwork(net_name, vlan_id) {
- var ret = makeMxNetwork(vlan_id, net_name);
- var color = ret['color'];
- var net_id = ret['element_id'];
- networks.add(vlan_id);
- network_names.add(net_name);
- makeSidebarNetwork(net_name, vlan_id, color, net_id);
-}
-
-function updateHosts(removed) {
- for(var i=0; i < removed.length; i++)
- {
- var hoststring = removed[i];
- var hostid = "host_" + hoststring.split("*")[0];
- var cell = currentGraph.getModel().getCell(hostid);
- currentGraph.removeCells([cell]);
- }
-
- var hosts = currentGraph.getChildVertices(currentGraph.getDefaultParent());
- var topdist = 100;
- for(var i=0; i<hosts.length; i++)
- {
- var host = hosts[i];
- if(!host.id.startsWith("host_"))
- {
- continue;
- }
- var geometry = host.getGeometry();
- geometry.y = topdist + 50;
- topdist = geometry.y + geometry.height;
- host.setGeometry(geometry);
- }
-}
-
-function makeSidebarNetwork(net_name, vlan_id, color, net_id){
- var newNet = document.createElement("li");
- var colorBlob = document.createElement("div");
- colorBlob.className = "colorblob";
- var textContainer = document.createElement("p");
- textContainer.className = "network_innertext";
- newNet.id = net_id;
- var deletebutton = document.createElement("button");
- deletebutton.className = "btn btn-danger";
- deletebutton.style = "float: right; height: 20px; line-height: 8px; vertical-align: middle; width: 20px; padding-left: 5px;";
- deleteButtonText = document.createTextNode("X");
- deletebutton.appendChild(deleteButtonText);
- deletebutton.addEventListener("click", function() {
- createDeleteDialog(net_id);
- }, false);
- var text = net_name;
- if(vlan_id){
- text += " : " + vlan_id;
- }
- var newNetValue = document.createTextNode(text);
- textContainer.appendChild(newNetValue);
- colorBlob.style['background'] = color;
- newNet.appendChild(colorBlob);
- newNet.appendChild(textContainer);
- if( net_name != "public" )
- {
- newNet.appendChild(deletebutton);
- }
- document.getElementById("network_list").appendChild(newNet);
-}
-
-function makeHost(hostInfo) {
- value = JSON.stringify(hostInfo['value']);
- interfaces = hostInfo['interfaces'];
- graph = currentGraph;
- width = 100;
- height = (25 * interfaces.length) + 10;
- xoff = 75;
- yoff = lastHostBottom + 50;
- lastHostBottom = yoff + height;
- host = graph.insertVertex(
- graph.getDefaultParent(),
- 'host_' + hostInfo['id'],
- value,
- xoff,
- yoff,
- width,
- height,
- 'editable=0',
- false
- );
- host.setConnectable(false);
- hostCount++;
-
- for(var i=0; i<interfaces.length; i++) {
- port = graph.insertVertex(
- host,
- null,
- JSON.stringify(interfaces[i]),
- 90,
- (i * 25) + 5,
- 20,
- 20,
- 'fillColor=blue;editable=0',
- false
- );
- }
-}
-
-function submitForm() {
- var form = document.getElementById("xml_form");
- var input_elem = document.getElementById("hidden_xml_input");
- var s = encodeGraph(currentGraph);
- input_elem.value = s;
- req = new XMLHttpRequest();
- req.open("POST", "/wf/workflow/", false);
- req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
- req.onerror = function() { alert("problem with form submission"); }
- var formData = $("#xml_form").serialize();
- req.send(formData);
-}
-</script>
+<script type="text/javascript" src="/static/js/dashboard.js" ></script>
{% endblock extrahead %}
<!-- Calls the main function after the page has loaded. Container is dynamically created. -->
@@ -654,6 +32,10 @@ function submitForm() {
</div>
<style>
+ p {
+ word-break: normal;
+ white-space: normal;
+ }
#network_select {
background: inherit;
padding: 0px;
@@ -705,12 +87,11 @@ function submitForm() {
<div id="network_select" style="position:absolute;top:0px;bottom:0px;width:25%;right:0px;left:auto;">
<div id="toolbar_extension">
- <button id="btn_add_network" type="button" class="btn btn-primary" onclick="newNetworkWindow();">Add Network</button>
+ <button id="btn_add_network" type="button" class="btn btn-primary" onclick="network_step.newNetworkWindow();">Add Network</button>
</div>
<ul id="network_list">
</ul>
- <p id="vlan_notice"></p>
- <button type="button" style="display: none" onclick="submitForm();">Submit</button>
+ <button type="button" style="display: none" onclick="network_step.submitForm();">Submit</button>
</div>
<form id="xml_form" method="post" action="/wf/workflow/">
{% csrf_token %}
@@ -718,14 +99,42 @@ function submitForm() {
</form>
<script>
- main(
+ //gather context data
+ let debug = false;
+ {% if debug %}
+ debug = true;
+ {% endif %}
+
+ let xml = '';
+ {% if xml %}
+ xml = '{{xml|safe}}';
+ {% endif %}
+
+ let hosts = [];
+ {% for host in hosts %}
+ hosts.push({{host|safe}});
+ {% endfor %}
+
+ let added_hosts = [];
+ {% for host in added_hosts %}
+ added_hosts.push({{host|safe}});
+ {% endfor %}
+
+ let removed_host_ids = {{removed_hosts|safe}};
+
+ network_step = new NetworkStep(
+ debug,
+ xml,
+ hosts,
+ added_hosts,
+ removed_host_ids,
document.getElementById('graphContainer'),
document.getElementById('outlineContainer'),
document.getElementById('toolbarContainer'),
document.getElementById('sidebarContainer')
- )
+ );
</script>
{% endblock content %}
{% block onleave %}
-submitForm();
+network_step.submitForm();
{% endblock %}
diff --git a/dashboard/src/templates/snapshot_workflow/steps/meta.html b/dashboard/src/templates/snapshot_workflow/steps/meta.html
index cc49691..bea475d 100644
--- a/dashboard/src/templates/snapshot_workflow/steps/meta.html
+++ b/dashboard/src/templates/snapshot_workflow/steps/meta.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
<style>
diff --git a/dashboard/src/templates/snapshot_workflow/steps/select_host.html b/dashboard/src/templates/snapshot_workflow/steps/select_host.html
index 27a9238..f438bac 100644
--- a/dashboard/src/templates/snapshot_workflow/steps/select_host.html
+++ b/dashboard/src/templates/snapshot_workflow/steps/select_host.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/workflow/confirm.html b/dashboard/src/templates/workflow/confirm.html
index 2510204..c1f3440 100644
--- a/dashboard/src/templates/workflow/confirm.html
+++ b/dashboard/src/templates/workflow/confirm.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
@@ -58,6 +58,17 @@
<script>
var select = document.getElementById("id_confirm");
+ function processResponseText(json)
+ {
+ var dict = JSON.parse(json);
+
+ if( !dict["redir_url"] ) {
+ window.top.refresh_iframe();
+ } else {
+ top.window.location.href = dict["redir_url"];
+ }
+ }
+
function delete_manager()
{
var form = $("#manager_delete_form");
@@ -67,7 +78,7 @@
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
req.onerror = function() { alert("problem with cleaning up session"); }
req.onreadystatechange = function() { if(req.readyState === 4 ) {
- window.top.refresh_iframe();
+ processResponseText(req.responseText);
}}
req.send(formData);
}
diff --git a/dashboard/src/templates/workflow/exit_redirect.html b/dashboard/src/templates/workflow/exit_redirect.html
deleted file mode 100644
index b08df78..0000000
--- a/dashboard/src/templates/workflow/exit_redirect.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<!DOCTYPE html>
-<html>
- <script>
- top.window.location.href='/';
- </script>
-</html>
diff --git a/dashboard/src/templates/workflow/resource_select.html b/dashboard/src/templates/workflow/resource_select.html
index c319ff5..cd04137 100644
--- a/dashboard/src/templates/workflow/resource_select.html
+++ b/dashboard/src/templates/workflow/resource_select.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/workflow/viewport-base.html b/dashboard/src/templates/workflow/viewport-base.html
index 9ddb4b8..aa01d7e 100644
--- a/dashboard/src/templates/workflow/viewport-base.html
+++ b/dashboard/src/templates/workflow/viewport-base.html
@@ -1,12 +1,12 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
<style>
- .go_btn{
+ .go_btn {
position: absolute;
width: 100px;
@@ -14,120 +14,256 @@
height: calc(100% - 170px);
}
- .go_btn_disabled{
- background-color: #ffffff;
+
+ .go_btn_disabled {
+ background-color: #ffffff;
}
- .go_forward{
+
+ .go_forward {
right: 0px;
border-left: none;
}
- .go_back{
+ .go_back {
left: 251px;
border-right: none;
}
- .btn_wrapper{
+ .btn_wrapper {
text-align: center;
margin-bottom: 5px;
}
{% if DEBUG %}
+ .add_btn_wrapper {
+ right: 130px;
+ top: 10px;
+ position: absolute;
+ }
+ {% endif %}
- .add_btn_wrapper{
- right: 130px;
- top: 10px;
- position: absolute;
+ #breadcrumbs {
+ margin-bottom: 0;
}
- {% endif %}
+ .btn_wrapper {
+ margin: 0;
+ }
+ .step {
+ display: inline;
+ padding: 7px;
+ margin: 1px;
+ font-size: 14pt;
+ cursor: default;
+ }
- .options{
- position: absolute;
- top: 60px;
- right: 20px;
+ .step:active {
+ -webkit-box-shadow: inherit;
+ box-shadow: inherit;
}
- #breadcrumbs {
- padding: 4px;
+ .step_active:active {
+ -webkit-box-shadow: inherit;
+ box-shadow: inherit;
}
- .step{
- background: #DEEED3;
+
+ .step_active {
display: inline;
- padding: 5px;
+ padding: 7px;
margin: 1px;
+ cursor: default;
+ font-size: 14pt;
+ padding-bottom: 4px !important;
+ border-bottom: 4px solid #41ba78 !important;
}
- .step_active{
- background: #5EC392;
- display: inline;
- padding: 5px;
- margin: 1px;
- font-weight: bold;
+ .step_hidden {
+ background: #EFEFEF;
+ color: #999999;
+ }
+
+ .step_invalid::after {
+ content: " \2612";
+ color: #CC3300;
+ }
+
+ .step_valid::after {
+ content: " \2611";
+ color: #41ba78;
}
- .step_untouched
- {
- background: #DDDDDD;
+ .step_untouched::after {
+ content: " \2610";
}
- .step_invalid
- {
- background: #CC3300;
+ .iframe_div {
+ width: calc(100% - 450px);
+ margin-left: 70px;
+ height: calc(100vh - 155px);
+ position: absolute;
+ border: none;
}
- .step_valid
- {
- background: #0FD57D;
+ .iframe_elem {
+ width: 100%;
+ height: calc(100vh - 155px);
+ border: none;
}
- #viewport-iframe
- {
- height: calc(100vh - 450);
- }
+ #breadcrumbs {
+ background-color: inherit;
+ }
+ #breadcrumbs.breadcrumb>li {
+ border: 1px solid #cccccc;
+ border-left: none;
+ }
-</style>
+ #breadcrumbs.breadcrumb>li:first-child {
+ border-left: 1px solid #cccccc;
+ }
-<button id="gof" onclick="go(step+1)" class="btn go_btn go_forward">Go Forward</button>
-<button id="gob" onclick="go(step-1)" class="btn go_btn go_back">Go Back</button>
+ #breadcrumbs.breadcrumb>li+li:before {
+ content: "";
+ width: 0;
+ margin: 0;
+ padding: 0;
+ }
-<div class="options">
- <button id="cancel_btn" class="btn btn-primary" onclick="cancel_wf()">Cancel</button>
+ #topPagination .topcrumb {
+ flex: 1 1 0;
+ display: flex;
+ align-content: center;
+ justify-content: center;
+ border: 1px solid #dee2e6;
+ border-left: none;
+ }
+
+ .topcrumb > span {
+ color: #343a40;
+ cursor: default;
+ }
+
+ .topcrumb.active > span {
+ background: #007bff;
+ color: white;
+ }
+
+ .topcrumb.disabled > span {
+ color: #6c757d;
+ background: #f8f9fa;
+ }
+</style>
+<!-- Pagination -->
+<div class="row mt-3">
+ <div class="col">
+ <nav>
+ <ul class="pagination d-flex flex-row" id="topPagination">
+ <li class="page-item flex-shrink-1 page-control">
+ <a class="page-link" href="#" id="gob" onclick="go('prev')">
+ <i class="fas fa-backward"></i> Back
+ </a>
+ </li>
+ <li class="page-item flex-grow-1 active">
+ <a class="page-link disabled" href="#">
+ Select <i class="far fa-check-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-grow-1">
+ <a class="page-link disabled" href="#">
+ Configure <i class="far fa-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-grow-1">
+ <a class="page-link disabled" href="#">
+ Information <i class="far fa-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-grow-1">
+ <a class="page-link disabled" href="#">
+ OPNFV <i class="far fa-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-grow-1">
+ <a class="page-link disabled" href="#">
+ Confirm <i class="far fa-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-shrink-1 page-control">
+ <a class="page-link text-right" href="#" id="gof" onclick="go('next')">
+ Next <i class="fas fa-forward"></i>
+ </a>
+ </li>
+ </ul>
+ </nav>
+ </div>
</div>
-<div class="btn_wrapper">
-<div id="breadcrumbs" class="btn-group">
- <div class="btn-group" id="breadcrumb-wrapper">
+<!-- Top header -->
+<div class="row px-4">
+ <div class="col">
+ <div id="iframe_header" class="row view-header">
+ <div class="col-lg-12 step_header">
+ <h1 class="step_title d-inline-block" id="view_title"></h1>
+ <span class="description text-muted" id="view_desc"></span>
+ <p class="step_message" id="view_message"></p>
+ </div>
+ <script>
+ function update_description(title, desc) {
+ document.getElementById("view_title").innerText = title;
+ document.getElementById("view_desc").innerText = desc;
+ }
+
+ function update_message(message, stepstatus) {
+ document.getElementById("view_message").innerText = message;
+ document.getElementById("view_message").className = "step_message";
+ document.getElementById("view_message").classList.add("message_" + stepstatus);
+ }
+ </script>
+ <!-- /.col-lg-12 -->
+ </div>
+ </div>
+ <div class="col-auto align-self-center d-flex">
+ <button id="cancel_btn" class="btn btn-danger ml-auto" onclick="cancel_wf()">Cancel</button>
</div>
</div>
+<!-- Content here -->
+<div class="row d-flex flex-column flex-grow-1">
+ <div class="container-fluid d-flex flex-column h-100">
+ <div class="row d-flex flex-grow-1 p-4">
+ <!-- iframe workflow -->
+ <div class="col-12 d-flex border flex-grow-1">
+ <!-- This was where the iframe went -->
+ <iframe src="/wf/workflow" class="w-100 h-100" scrolling="yes" id="viewport-iframe"
+ frameBorder="0"></iframe>
+ </div>
+ </div>
+ </div>
+</div>
+<div class="btn_wrapper">
</div>
{% csrf_token %}
<script type="text/javascript">
-
-
update_context();
var step = 0;
var page_count = 0;
var context_data = false;
- function go(to)
- {
+ function go(to) {
step_on_leave();
request_leave(to);
}
- function request_leave(to)
- {
+ function request_leave(to) {
$.ajax({
type: "GET",
url: "/wf/manager/",
- beforeSend: function(request) {
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val());
+ $('input[name="csrfmiddlewaretoken"]').val());
},
success: function (data) {
confirm_permission(to, data);
@@ -136,56 +272,44 @@
});
}
- function confirm_permission(to, data)
- {
- if( errors_exist(data) )
- {
- var continueanyway = confirm("The current step has errors that will prevent it from saving. Continue anyway?");
- if( !continueanyway )
- {
+ function confirm_permission(to, data) {
+ if (errors_exist(data)) {
+ if (to != "prev") {
return;
}
}
- if( to >= page_count )
- {
- to = page_count-1;
- }
- else if( to < 0 )
- {
- to = 0;
- }
- var problem = function() {
+
+ var problem = function () {
alert("There was a problem");
}
//makes an asynch request
req = new XMLHttpRequest();
url = "/wf/workflow/?step=" + to;
req.open("GET", url, true);
- req.onload = function(e) {
- if(req.readyState === 4){
- if(req.status < 300){
+ req.onload = function (e) {
+ if (req.readyState === 4) {
+ if (req.status < 300) {
document.getElementById("viewport-iframe").srcdoc = this.responseText;
- } else { problem(); }
- } else { problem(); }
+ } else {
+ problem();
+ }
+ } else {
+ problem();
+ }
}
req.onerror = problem;
req.send();
}
- function step_on_leave()
- {
+ function step_on_leave() {
document.getElementById("viewport-iframe").contentWindow.step_on_leave();
}
- function errors_exist(data)
- {
+ function errors_exist(data) {
var stat = data['steps'][data['active']]['valid'];
- if( stat >= 100 && stat < 200 )
- {
+ if (stat >= 100 && stat < 200) {
return true;
- }
- else
- {
+ } else {
return false;
}
}
@@ -194,9 +318,9 @@
$.ajax({
type: "GET",
url: "/wf/manager/",
- beforeSend: function(request) {
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val());
+ $('input[name="csrfmiddlewaretoken"]').val());
},
success: function (data) {
update_page(data);
@@ -204,117 +328,100 @@
});
}
- function update_page(data)
- {
+ function update_page(data) {
context_data = data;
update_breadcrumbs(data);
- if(data["workflow_count"] == 1)
- {
- document.getElementById("cancel_btn").innerText = "Exit Workflow";
- }
- else
- {
- document.getElementById("cancel_btn").innerText = "Return to Parent";
+ if (data["workflow_count"] == 1) {
+ document.getElementById("cancel_btn").innerText = "Exit Workflow";
+ } else {
+ document.getElementById("cancel_btn").innerText = "Return to Parent";
}
}
function update_breadcrumbs(meta_json) {
step = meta_json['active'];
page_count = meta_json['steps'].length;
- if( step == 0 )
- {
- var btn = document.getElementById("gob");
- btn.classList.add("go_btn_disabled");
- btn.disabled = true;
- }
- else
- {
- var btn = document.getElementById("gob");
- btn.classList.remove("go_btn_disabled");
- btn.disabled = false;
- }
- if( step == page_count - 1 )
- {
- var btn = document.getElementById("gof");
- btn.classList.add("go_btn_disabled");
- btn.disabled = true;
+ if (step == 0) {
+ var btn = document.getElementById("gob");
+ btn.classList.add("invisible");
+ btn.disabled = true;
+ } else {
+ var btn = document.getElementById("gob");
+ btn.classList.remove("invisible");
+ btn.disabled = false;
}
- else
- {
- var btn = document.getElementById("gof");
- btn.classList.remove("go_btn_disabled");
- btn.disabled = false;
+ if (step == page_count - 1) {
+ var btn = document.getElementById("gof");
+ btn.classList.add("invisible");
+ btn.disabled = true;
+ } else {
+ var btn = document.getElementById("gof");
+ btn.classList.remove("invisible");
+ btn.disabled = false;
}
//remove all children of breadcrumbs so we can redraw
- var container = document.getElementById("breadcrumbs");
- while(container.firstChild){
- container.removeChild(container.firstChild);
- }
-
+ $("#topPagination").children().not(".page-control").remove();
draw_steps(meta_json);
}
- function draw_steps(meta_json){
- for( var i = 0; i < meta_json["steps"].length; i++ )
- {
+ function draw_steps(meta_json) {
+ for (var i = 0; i < meta_json["steps"].length; i++) {
meta_json["steps"][i]["index"] = i;
var step_btn = create_step(meta_json["steps"][i], i == meta_json["active"]);
- document.getElementById("breadcrumbs").appendChild(step_btn);
+ $("#topPagination li:last-child").before(step_btn);
}
}
- function create_step(step_json, active){
- var step_dom = document.createElement("DIV");
- if(active){
- step_dom.className = "step_active";
-
- } else{
- step_dom.className = "step";
+ function create_step(step_json, active) {
+ var step_dom = document.createElement("li");
+ // First create the dom object depending on active or not
+ if (active) {
+ step_dom.className = "topcrumb active";
+ } else {
+ step_dom.className = "topcrumb";
}
- step_dom.appendChild(document.createTextNode(step_json['title']));
+ $(step_dom).html(`<span class="d-flex align-items-center justify-content-center text-capitalize w-100">${step_json['title']}</span>`)
var code = step_json['valid'];
stat = "";
msg = "";
- if( code < 100 )
- {
- step_dom.classList.add("step_untouched");
-
+ if (code < 100) {
+ $(step_dom).children().first().append("<i class='ml-2 far fa-square'></i>")
stat = "";
msg = "";
- }
- else if( code < 200 )
- {
- step_dom.classList.add("step_invalid");
+ } else if (code < 200) {
+ $(step_dom).children().first().append("<i class='ml-2 fas fa-minus-square'></i>")
stat = "invalid";
msg = step_json['message'];
- }
- else if( code < 300 )
- {
- step_dom.classList.add("step_valid");
+ } else if (code < 300) {
+ $(step_dom).children().first().append("<i class='ml-2 far fa-check-square'></i>")
stat = "valid";
msg = step_json['message'];
}
- if(active)
- {
+ if (step_json['enabled'] == false) {
+ step_dom.classList.add("disabled");
+ }
+ if (active) {
update_message(msg, stat);
}
- step_dom.classList.add("btn");
var step_number = step_json['index'];
- step_dom.onclick = function(){ go(step_number); }
return step_dom;
}
- function cancel_wf(){
+ function cancel_wf() {
var form = $("#workflow_pop_form");
var formData = form.serialize();
var req = new XMLHttpRequest();
req.open("POST", "/wf/workflow/finish/", false);
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
- req.onerror = function() { alert("problem occurred while trying to cancel current workflow"); }
- req.onreadystatechange = function() { if(req.readyState === 4){
- refresh_iframe();
- }};
+ req.onerror = function () {
+ alert("problem occurred while trying to cancel current workflow");
+ }
+ req.onreadystatechange = function () {
+ if (req.readyState === 4) {
+ refresh_iframe();
+ }
+ };
req.send(formData);
}
@@ -322,43 +429,45 @@
req = new XMLHttpRequest();
url = "/wf/workflow/";
req.open("GET", url, true);
- req.onload = function(e) {
+ req.onload = function (e) {
var doc = document.getElementById("viewport-iframe").contentWindow.document;
- doc.open(); doc.write(this.responseText); doc.close();
+ doc.open();
+ doc.write(this.responseText);
+ doc.close();
}
req.send();
}
- function write_iframe(contents)
- {
- document.getElementById("viewport-iframe").contentWindow.document.innerHTML= contents;
+ function write_iframe(contents) {
+ document.getElementById("viewport-iframe").contentWindow.document.innerHTML = contents;
}
- function redirect_root()
- {
+ function redirect_root() {
window.location.replace('/wf/');
}
- function add_wf(type){
+ function add_wf(type) {
add_wf_internal(type, false);
}
- function add_edit_wf(type, target){
+ function add_edit_wf(type, target) {
add_wf_internal(type, target);
}
- function add_wf_internal(type, itemid){
- data = {"add": type};
- if(itemid){
+ function add_wf_internal(type, itemid) {
+ data = {
+ "add": type
+ };
+ if (itemid) {
data['target'] = itemid;
}
$.ajax({
type: "POST",
url: "/wf/manager/",
data: data,
- beforeSend: function(request) {
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val()
+ $('input[name="csrfmiddlewaretoken"]').val()
);
},
success: refresh_wf_iframe()
@@ -366,73 +475,12 @@
}
function refresh_wf_iframe() {
- window.location=window.location;
+ window.location = window.location;
}
</script>
-<div id="iframe_header" class="row view-header">
- <div class="col-lg-12 step_header">
- <h1 class="step_title" id="view_title"></h1>
- <p class="description" id="view_desc"></p>
- <p class="step_message" id="view_message"></p>
- </div>
- <style>
- #view_desc{
- margin-bottom: 15px;
- margin-top: 5px;
- margin-left: 30px;
- display: inline;
- }
- #view_title{
- margin-top: 5px;
- margin-bottom: 0px;
- display: inline;
- }
- #view_message{
- margin-top: 10px;
- margin-bottom: 5px;
- float: right;
- }
- .message_invalid{
- color: #ff4400;
- }
- .message_valid{
- color: #44cc00;
- }
- .step_header{
- border-bottom: 1px solid #eee;
- border-top: 1px solid #eee;
- left: 101px;
- width: calc(100% - 202px);
- }
- </style>
- <script>
- function update_description(title, desc){
- document.getElementById("view_title").innerText = title;
- document.getElementById("view_desc").innerText = desc;
- }
- function update_message(message, stepstatus){
- document.getElementById("view_message").innerText = message;
- document.getElementById("view_message").className = "step_message";
- document.getElementById("view_message").classList.add("message_" + stepstatus);
- }
- function resize_iframe(){
- var page_rect = document.getElementById("wrapper").getBoundingClientRect();
- var title_rect = document.getElementById("iframe_header").getBoundingClientRect();
- var iframe_height = page_rect.bottom - title_rect.bottom;
- console.log("setting height to " + iframe_height);
- document.getElementById("viewport-iframe").height = iframe_height;
-
- }
- window.addEventListener('load', resize_iframe);
- window.addEventListener('resize', resize_iframe);
- </script>
- <!-- /.col-lg-12 -->
-</div>
<div style="display: none;" id="workflow_pop_form_div">
-<form id="workflow_pop_form" action="/wf/workflow/finish/" method="post">
- {% csrf_token %}
-</form>
+ <form id="workflow_pop_form" action="/wf/workflow/finish/" method="post">
+ {% csrf_token %}
+ </form>
</div>
-
-<iframe src="/wf/workflow" style="position: absolute; left: 351px; right: 105px; width: calc(100% - 450px); border-style: none; border-width: 1px; border-color: #888888;" scrolling="yes" id="viewport-iframe" onload="resize_iframe();"></iframe>
-{% endblock content %}
+{% endblock content %} \ No newline at end of file
diff --git a/dashboard/src/templates/workflow/viewport-element.html b/dashboard/src/templates/workflow/viewport-element.html
index f25e644..7a7165a 100644
--- a/dashboard/src/templates/workflow/viewport-element.html
+++ b/dashboard/src/templates/workflow/viewport-element.html
@@ -1,5 +1,5 @@
{% extends "layout.html" %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% load staticfiles %}
{% block basecontent %}
diff --git a/dashboard/src/workflow/booking_workflow.py b/dashboard/src/workflow/booking_workflow.py
index 76950b8..42372ce 100644
--- a/dashboard/src/workflow/booking_workflow.py
+++ b/dashboard/src/workflow/booking_workflow.py
@@ -8,188 +8,136 @@
##############################################################################
from django.contrib import messages
-from django.shortcuts import render
-from django.contrib.auth.models import User
from django.utils import timezone
-import json
from datetime import timedelta
from booking.models import Booking
-from workflow.models import WorkflowStep
-from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm
-from resource_inventory.models import GenericResourceBundle, ResourceBundle, ConfigBundle
+from workflow.models import WorkflowStep, AbstractSelectOrCreate
+from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm, OPNFVSelectForm
+from resource_inventory.models import GenericResourceBundle, ConfigBundle, OPNFVConfig
-class Resource_Select(WorkflowStep):
- template = 'booking/steps/resource_select.html'
+"""
+subclassing notes:
+ subclasses have to define the following class attributes:
+ self.repo_key: main output of step, where the selected/created single selector
+ result is placed at the end
+ self.confirm_key:
+"""
+
+
+class Abstract_Resource_Select(AbstractSelectOrCreate):
+ form = ResourceSelectorForm
+ template = 'dashboard/genericselect.html'
title = "Select Resource"
description = "Select a resource template to use for your deployment"
short_title = "pod select"
def __init__(self, *args, **kwargs):
- super(Resource_Select, self).__init__(*args, **kwargs)
- self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
- self.repo_check_key = False
- self.confirm_key = "booking"
+ super().__init__(*args, **kwargs)
+ self.select_repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
+ self.confirm_key = self.workflow_type
- def get_context(self):
- context = super(Resource_Select, self).get_context()
- default = []
-
- chosen_bundle = self.repo_get(self.repo_key, False)
- if chosen_bundle:
- default.append(chosen_bundle.id)
+ def alert_bundle_missing(self):
+ self.set_invalid("Please select a valid resource bundle")
- bundle = chosen_bundle
- edit = self.repo_get(self.repo.EDIT, False)
+ def get_form_queryset(self):
user = self.repo_get(self.repo.SESSION_USER)
- context['form'] = ResourceSelectorForm(
- data={"user": user},
- chosen_resource=default,
- bundle=bundle,
- edit=edit
- )
- return context
+ qs = GenericResourceBundle.objects.filter(owner=user)
+ return qs
- def post_render(self, request):
- form = ResourceSelectorForm(request.POST)
- context = self.get_context()
- if form.is_valid():
- data = form.cleaned_data['generic_resource_bundle']
- data = data[2:-2]
- if not data:
- self.metastep.set_invalid("Please select a valid bundle")
- return render(request, self.template, context)
- selected_bundle = json.loads(data)
- if len(selected_bundle) < 1:
- self.metastep.set_invalid("Please select a valid bundle")
- return render(request, self.template, context)
- selected_id = selected_bundle[0]['id']
- gresource_bundle = None
- try:
- selected_id = int(selected_id)
- gresource_bundle = GenericResourceBundle.objects.get(id=selected_id)
- except ValueError:
- # we want the bundle in the repo
- gresource_bundle = self.repo_get(
- self.repo.GRESOURCE_BUNDLE_MODELS,
- {}
- ).get("bundle", GenericResourceBundle())
- self.repo_put(
- self.repo_key,
- gresource_bundle
- )
- confirm = self.repo_get(self.repo.CONFIRMATION)
- if self.confirm_key not in confirm:
- confirm[self.confirm_key] = {}
- confirm[self.confirm_key]["resource name"] = gresource_bundle.name
- self.repo_put(self.repo.CONFIRMATION, confirm)
- messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
- self.metastep.set_valid("Step Completed")
- return render(request, self.template, context)
- else:
- messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
- self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
- return render(request, self.template, context)
+ def get_page_context(self):
+ return {
+ 'select_type': 'resource',
+ 'select_type_title': 'Resource Bundle',
+ 'addable_type_num': 1
+ }
+ def put_confirm_info(self, bundle):
+ confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+ if self.confirm_key not in confirm_dict:
+ confirm_dict[self.confirm_key] = {}
+ confirm_dict[self.confirm_key]["Resource Template"] = bundle.name
+ self.repo_put(self.repo.CONFIRMATION, confirm_dict)
-class Booking_Resource_Select(Resource_Select):
- def __init__(self, *args, **kwargs):
- super(Booking_Resource_Select, self).__init__(*args, **kwargs)
- self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
- self.confirm_key = "booking"
+class Booking_Resource_Select(Abstract_Resource_Select):
+ workflow_type = "booking"
- def get_context(self):
- context = super(Booking_Resource_Select, self).get_context()
- return context
- def post_render(self, request):
- response = super(Booking_Resource_Select, self).post_render(request)
- models = self.repo_get(self.repo.BOOKING_MODELS, {})
- if "booking" not in models:
- models['booking'] = Booking()
- booking = models['booking']
- resource = self.repo_get(self.repo_key, False)
- if resource:
- try:
- booking.resource.template = resource
- except:
- booking.resource = ResourceBundle(template=resource)
- models['booking'] = booking
- self.repo_put(self.repo.BOOKING_MODELS, models)
- return response
-
-
-class SWConfig_Select(WorkflowStep):
- template = 'booking/steps/swconfig_select.html'
+class SWConfig_Select(AbstractSelectOrCreate):
title = "Select Software Configuration"
description = "Choose the software and related configurations you want to have used for your deployment"
short_title = "pod config"
+ form = SWConfigSelectorForm
- def post_render(self, request):
- form = SWConfigSelectorForm(request.POST)
- if form.is_valid():
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE
+ self.confirm_key = "booking"
- bundle_json = form.cleaned_data['software_bundle']
- bundle_json = bundle_json[2:-2] # Stupid django string bug
- if not bundle_json:
- self.metastep.set_invalid("Please select a valid config")
- return self.render(request)
- bundle_json = json.loads(bundle_json)
- if len(bundle_json) < 1:
- self.metastep.set_invalid("Please select a valid config")
- return self.render(request)
- bundle = None
- id = int(bundle_json[0]['id'])
- bundle = ConfigBundle.objects.get(id=id)
-
- grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
-
- if grb and bundle.bundle != grb:
- self.metastep.set_invalid("Incompatible config selected for resource bundle")
- return self.render(request)
- if not grb:
- self.repo_set(self.repo.SELECTED_GRESOURCE_BUNDLE, bundle.bundle)
+ def alert_bundle_missing(self):
+ self.set_invalid("Please select a valid pod config")
- models = self.repo_get(self.repo.BOOKING_MODELS, {})
- if "booking" not in models:
- models['booking'] = Booking()
- models['booking'].config_bundle = bundle
- self.repo_put(self.repo.BOOKING_MODELS, models)
- confirm = self.repo_get(self.repo.CONFIRMATION)
- if "booking" not in confirm:
- confirm['booking'] = {}
- confirm['booking']["configuration name"] = bundle.name
- self.repo_put(self.repo.CONFIRMATION, confirm)
- self.metastep.set_valid("Step Completed")
- messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
- else:
- self.metastep.set_invalid("Please select or create a valid config")
- messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
+ def get_form_queryset(self):
+ user = self.repo_get(self.repo.SESSION_USER)
+ grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
+ qs = ConfigBundle.objects.filter(owner=user).filter(bundle=grb)
+ return qs
- return self.render(request)
+ def put_confirm_info(self, bundle):
+ confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+ if self.confirm_key not in confirm_dict:
+ confirm_dict[self.confirm_key] = {}
+ confirm_dict[self.confirm_key]["Software Configuration"] = bundle.name
+ self.repo_put(self.repo.CONFIRMATION, confirm_dict)
+
+ def get_page_context(self):
+ return {
+ 'select_type': 'swconfig',
+ 'select_type_title': 'Software Config',
+ 'addable_type_num': 2
+ }
- def get_context(self):
- context = super(SWConfig_Select, self).get_context()
- default = []
- bundle = None
- chosen_bundle = None
- created_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
- booking = self.repo_get(self.repo.BOOKING_MODELS, {}).get("booking", False)
- try:
- chosen_bundle = booking.config_bundle
- default.append(chosen_bundle.id)
- bundle = chosen_bundle
- except:
- if created_bundle:
- default.append(created_bundle.id)
- bundle = created_bundle
- edit = self.repo_get(self.repo.EDIT, False)
- grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
- context['form'] = SWConfigSelectorForm(chosen_software=default, bundle=bundle, edit=edit, resource=grb)
- return context
+
+class OPNFV_EnablePicker(object):
+ pass
+
+
+class OPNFV_Select(AbstractSelectOrCreate, OPNFV_EnablePicker):
+ title = "Choose an OPNFV Config"
+ description = "Choose or create a description of how you want to deploy OPNFV"
+ short_title = "opnfv config"
+ form = OPNFVSelectForm
+ enabled = False
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.select_repo_key = self.repo.SELECTED_OPNFV_CONFIG
+ self.confirm_key = "booking"
+
+ def alert_bundle_missing(self):
+ self.set_invalid("Please select a valid OPNFV config")
+
+ def get_form_queryset(self):
+ cb = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
+ qs = OPNFVConfig.objects.filter(bundle=cb)
+ return qs
+
+ def put_confirm_info(self, config):
+ confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+ if self.confirm_key not in confirm_dict:
+ confirm_dict[self.confirm_key] = {}
+ confirm_dict[self.confirm_key]["OPNFV Configuration"] = config.name
+ self.repo_put(self.repo.CONFIRMATION, confirm_dict)
+
+ def get_page_context(self):
+ return {
+ 'select_type': 'opnfv',
+ 'select_type_title': 'OPNFV Config',
+ 'addable_type_num': 4
+ }
class Booking_Meta(WorkflowStep):
@@ -214,23 +162,17 @@ class Booking_Meta(WorkflowStep):
initial['info_file'] = info
users = models.get("collaborators", [])
for user in users:
- default.append(user.id)
+ default.append(user.userprofile)
except Exception:
pass
- default_user = self.repo_get(self.repo.SESSION_USER)
- if default_user is None:
- # TODO: error
- default_user = "you"
- else:
- default_user = default_user.username
+ owner = self.repo_get(self.repo.SESSION_USER)
- context['form'] = BookingMetaForm(initial=initial, chosen_users=default, default_user=default_user)
+ context['form'] = BookingMetaForm(initial=initial, user_initial=default, owner=owner)
return context
def post_render(self, request):
- form = BookingMetaForm(data=request.POST)
- context = self.get_context()
+ form = BookingMetaForm(data=request.POST, owner=request.user)
forms = self.repo_get(self.repo.BOOKING_FORMS, {})
@@ -253,15 +195,16 @@ class Booking_Meta(WorkflowStep):
for key in ['length', 'project', 'purpose']:
confirm['booking'][key] = form.cleaned_data[key]
- user_data = form.cleaned_data['users']
+ if form.cleaned_data["deploy_opnfv"]:
+ self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=True)
+ else:
+ self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=False)
+
+ userprofile_list = form.cleaned_data['users']
confirm['booking']['collaborators'] = []
- user_data = user_data[2:-2] # fixes malformed string from querydict
- if user_data:
- form_users = json.loads(user_data)
- for user_json in form_users:
- user = User.objects.get(pk=user_json['id'])
- models['collaborators'].append(user)
- confirm['booking']['collaborators'].append(user.username)
+ for userprofile in userprofile_list:
+ models['collaborators'].append(userprofile.user)
+ confirm['booking']['collaborators'].append(userprofile.user.username)
info_file = form.cleaned_data.get("info_file", False)
if info_file:
@@ -270,9 +213,8 @@ class Booking_Meta(WorkflowStep):
self.repo_put(self.repo.BOOKING_MODELS, models)
self.repo_put(self.repo.CONFIRMATION, confirm)
messages.add_message(request, messages.SUCCESS, 'Form Validated', fail_silently=True)
- self.metastep.set_valid("Step Completed")
+ self.set_valid("Step Completed")
else:
messages.add_message(request, messages.ERROR, "Form didn't validate", fail_silently=True)
- self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
- context['form'] = form # TODO: store this form
- return render(request, self.template, context)
+ self.set_invalid("Please complete the fields highlighted in red to continue")
+ return self.render(request)
diff --git a/dashboard/src/workflow/forms.py b/dashboard/src/workflow/forms.py
index 726e7dd..ee44ecd 100644
--- a/dashboard/src/workflow/forms.py
+++ b/dashboard/src/workflow/forms.py
@@ -9,41 +9,37 @@
import django.forms as forms
-from django.forms import widgets
+from django.forms import widgets, ValidationError
from django.utils.safestring import mark_safe
from django.template.loader import render_to_string
from django.forms.widgets import NumberInput
+import json
+
from account.models import Lab
from account.models import UserProfile
from resource_inventory.models import (
- GenericResourceBundle,
- ConfigBundle,
OPNFVRole,
- Image,
Installer,
- Scenario
+ Scenario,
)
+from booking.lib import get_user_items, get_user_field_opts
class SearchableSelectMultipleWidget(widgets.SelectMultiple):
template_name = 'dashboard/searchable_select_multiple.html'
def __init__(self, attrs=None):
- self.items = attrs['set']
+ self.items = attrs['items']
self.show_from_noentry = attrs['show_from_noentry']
self.show_x_results = attrs['show_x_results']
- self.results_scrollable = attrs['scrollable']
+ self.results_scrollable = attrs['results_scrollable']
self.selectable_limit = attrs['selectable_limit']
self.placeholder = attrs['placeholder']
self.name = attrs['name']
- self.initial = attrs.get("initial", "")
- self.default_entry = attrs.get("default_entry", "")
- self.edit = attrs.get("edit", False)
- self.wf_type = attrs.get("wf_type")
- self.incompatible = attrs.get("incompatible", "false")
+ self.initial = attrs.get("initial", [])
- super(SearchableSelectMultipleWidget, self).__init__(attrs)
+ super(SearchableSelectMultipleWidget, self).__init__()
def render(self, name, value, attrs=None, renderer=None):
@@ -60,124 +56,160 @@ class SearchableSelectMultipleWidget(widgets.SelectMultiple):
'selectable_limit': self.selectable_limit,
'placeholder': self.placeholder,
'initial': self.initial,
- 'default_entry': self.default_entry,
- 'edit': self.edit,
- 'wf_type': self.wf_type,
- 'incompatible': self.incompatible
}
-class ResourceSelectorForm(forms.Form):
+class SearchableSelectMultipleField(forms.Field):
+ def __init__(self, *args, required=True, widget=None, label=None, disabled=False,
+ items=None, queryset=None, show_from_noentry=True, show_x_results=-1,
+ results_scrollable=False, selectable_limit=-1, placeholder="search here",
+ name="searchable_select", initial=[], **kwargs):
+ """from the documentation:
+ # required -- Boolean that specifies whether the field is required.
+ # True by default.
+ # widget -- A Widget class, or instance of a Widget class, that should
+ # be used for this Field when displaying it. Each Field has a
+ # default Widget that it'll use if you don't specify this. In
+ # most cases, the default widget is TextInput.
+ # label -- A verbose name for this field, for use in displaying this
+ # field in a form. By default, Django will use a "pretty"
+ # version of the form field name, if the Field is part of a
+ # Form.
+ # initial -- A value to use in this Field's initial display. This value
+ # is *not* used as a fallback if data isn't given.
+ # help_text -- An optional string to use as "help text" for this Field.
+ # error_messages -- An optional dictionary to override the default
+ # messages that the field will raise.
+ # show_hidden_initial -- Boolean that specifies if it is needed to render a
+ # hidden widget with initial value after widget.
+ # validators -- List of additional validators to use
+ # localize -- Boolean that specifies if the field should be localized.
+ # disabled -- Boolean that specifies whether the field is disabled, that
+ # is its widget is shown in the form but not editable.
+ # label_suffix -- Suffix to be added to the label. Overrides
+ # form's label_suffix.
+ """
+
+ self.widget = widget
+ if self.widget is None:
+ self.widget = SearchableSelectMultipleWidget(
+ attrs={
+ 'items': items,
+ 'initial': [obj.id for obj in initial],
+ 'show_from_noentry': show_from_noentry,
+ 'show_x_results': show_x_results,
+ 'results_scrollable': results_scrollable,
+ 'selectable_limit': selectable_limit,
+ 'placeholder': placeholder,
+ 'name': name,
+ 'disabled': disabled
+ }
+ )
+ self.disabled = disabled
+ self.queryset = queryset
+ self.selectable_limit = selectable_limit
- def __init__(self, data=None, **kwargs):
- chosen_resource = ""
- bundle = None
- edit = False
- if "chosen_resource" in kwargs:
- chosen_resource = kwargs.pop("chosen_resource")
- if "bundle" in kwargs:
- bundle = kwargs.pop("bundle")
- if "edit" in kwargs:
- edit = kwargs.pop("edit")
- super(ResourceSelectorForm, self).__init__(data=data, **kwargs)
- queryset = GenericResourceBundle.objects.select_related("owner").all()
- if data and 'user' in data:
- queryset = queryset.filter(owner=data['user'])
+ super().__init__(disabled=disabled, **kwargs)
- attrs = self.build_search_widget_attrs(chosen_resource, bundle, edit, queryset)
+ self.required = required
- self.fields['generic_resource_bundle'] = forms.CharField(
- widget=SearchableSelectMultipleWidget(attrs=attrs)
+ def clean(self, data):
+ data = data[0]
+ if not data:
+ if self.required:
+ raise ValidationError("Nothing was selected")
+ else:
+ return []
+ data_as_list = json.loads(data)
+ if self.selectable_limit != -1:
+ if len(data_as_list) > self.selectable_limit:
+ raise ValidationError("Too many items were selected")
+
+ items = []
+ for elem in data_as_list:
+ items.append(self.queryset.get(id=elem))
+
+ return items
+
+
+class SearchableSelectAbstractForm(forms.Form):
+ def __init__(self, *args, queryset=None, initial=[], **kwargs):
+ self.queryset = queryset
+ items = self.generate_items(self.queryset)
+ options = self.generate_options()
+
+ super(SearchableSelectAbstractForm, self).__init__(*args, **kwargs)
+ self.fields['searchable_select'] = SearchableSelectMultipleField(
+ initial=initial,
+ items=items,
+ queryset=self.queryset,
+ **options
)
- def build_search_widget_attrs(self, chosen_resource, bundle, edit, queryset):
- resources = {}
- for res in queryset:
- displayable = {}
- displayable['small_name'] = res.name
- if res.owner:
- displayable['expanded_name'] = res.owner.username
- else:
- displayable['expanded_name'] = ""
- displayable['string'] = res.description
- displayable['id'] = res.id
- resources[res.id] = displayable
-
- attrs = {
- 'set': resources,
- 'show_from_noentry': "true",
+ def get_validated_bundle(self):
+ bundles = self.cleaned_data['searchable_select']
+ if len(bundles) < 1: # don't need to check for >1, as field does that for us
+ raise ValidationError("No bundle was selected")
+ return bundles[0]
+
+ def generate_items(self, queryset):
+ raise Exception("SearchableSelectAbstractForm does not implement concrete generate_items()")
+
+ def generate_options(self, disabled=False):
+ return {
+ 'show_from_noentry': True,
'show_x_results': -1,
- 'scrollable': "true",
+ 'results_scrollable': True,
'selectable_limit': 1,
- 'name': "generic_resource_bundle",
- 'placeholder': "resource",
- 'initial': chosen_resource,
- 'edit': edit,
- 'wf_type': 1
+ 'placeholder': 'Search for a Bundle',
+ 'name': 'searchable_select',
+ 'disabled': False
}
- return attrs
-class SWConfigSelectorForm(forms.Form):
+class SWConfigSelectorForm(SearchableSelectAbstractForm):
+ def generate_items(self, queryset):
+ items = {}
- def __init__(self, *args, **kwargs):
- chosen_software = ""
- bundle = None
- edit = False
- resource = None
- if "chosen_software" in kwargs:
- chosen_software = kwargs.pop("chosen_software")
-
- if "bundle" in kwargs:
- bundle = kwargs.pop("bundle")
- if "edit" in kwargs:
- edit = kwargs.pop("edit")
- if "resource" in kwargs:
- resource = kwargs.pop("resource")
- super(SWConfigSelectorForm, self).__init__(*args, **kwargs)
- attrs = self.build_search_widget_attrs(chosen_software, bundle, edit, resource)
- self.fields['software_bundle'] = forms.CharField(
- widget=SearchableSelectMultipleWidget(attrs=attrs)
- )
+ for bundle in queryset:
+ items[bundle.id] = {
+ 'expanded_name': bundle.name,
+ 'small_name': bundle.owner.username,
+ 'string': bundle.description,
+ 'id': bundle.id
+ }
+
+ return items
- def build_search_widget_attrs(self, chosen, bundle, edit, resource):
- configs = {}
- queryset = ConfigBundle.objects.select_related('owner').all()
- if resource:
- queryset = queryset.filter(bundle=resource)
+
+class OPNFVSelectForm(SearchableSelectAbstractForm):
+ def generate_items(self, queryset):
+ items = {}
for config in queryset:
- displayable = {}
- displayable['small_name'] = config.name
- displayable['expanded_name'] = config.owner.username
- displayable['string'] = config.description
- displayable['id'] = config.id
- configs[config.id] = displayable
-
- incompatible_choice = "false"
- if bundle and bundle.id not in configs:
- displayable = {}
- displayable['small_name'] = bundle.name
- displayable['expanded_name'] = bundle.owner.username
- displayable['string'] = bundle.description
- displayable['id'] = bundle.id
- configs[bundle.id] = displayable
- incompatible_choice = "true"
-
- attrs = {
- 'set': configs,
- 'show_from_noentry': "true",
- 'show_x_results': -1,
- 'scrollable': "true",
- 'selectable_limit': 1,
- 'name': "software_bundle",
- 'placeholder': "config",
- 'initial': chosen,
- 'edit': edit,
- 'wf_type': 2,
- 'incompatible': incompatible_choice
- }
- return attrs
+ items[config.id] = {
+ 'expanded_name': config.name,
+ 'small_name': config.bundle.owner.username,
+ 'string': config.description,
+ 'id': config.id
+ }
+
+ return items
+
+
+class ResourceSelectorForm(SearchableSelectAbstractForm):
+ def generate_items(self, queryset):
+ items = {}
+
+ for bundle in queryset:
+ items[bundle.id] = {
+ 'expanded_name': bundle.name,
+ 'small_name': bundle.owner.username,
+ 'string': bundle.description,
+ 'id': bundle.id
+ }
+
+ return items
class BookingMetaForm(forms.Form):
@@ -186,194 +218,125 @@ class BookingMetaForm(forms.Form):
widget=NumberInput(
attrs={
"type": "range",
- 'min': "0",
+ 'min': "1",
"max": "21",
- "value": "0"
+ "value": "1"
}
)
)
purpose = forms.CharField(max_length=1000)
project = forms.CharField(max_length=400)
info_file = forms.CharField(max_length=1000, required=False)
+ deploy_opnfv = forms.BooleanField(required=False)
- def __init__(self, data=None, *args, **kwargs):
- chosen_users = []
- if "default_user" in kwargs:
- default_user = kwargs.pop("default_user")
- else:
- default_user = "you"
- self.default_user = default_user
- if "chosen_users" in kwargs:
- chosen_users = kwargs.pop("chosen_users")
- elif data and "users" in data:
- chosen_users = data.getlist("users")
- else:
- pass
-
- super(BookingMetaForm, self).__init__(data=data, **kwargs)
+ def __init__(self, *args, user_initial=[], owner=None, **kwargs):
+ super(BookingMetaForm, self).__init__(**kwargs)
- self.fields['users'] = forms.CharField(
- widget=SearchableSelectMultipleWidget(
- attrs=self.build_search_widget_attrs(chosen_users, default_user=default_user)
- ),
- required=False
+ self.fields['users'] = SearchableSelectMultipleField(
+ queryset=UserProfile.objects.select_related('user').exclude(user=owner),
+ initial=user_initial,
+ items=get_user_items(exclude=owner),
+ required=False,
+ **get_user_field_opts()
)
- def build_user_list(self):
- """
- returns a mapping of UserProfile ids to displayable objects expected by
- searchable multiple select widget
- """
- try:
- users = {}
- d_qset = UserProfile.objects.select_related('user').all().exclude(user__username=self.default_user)
- for userprofile in d_qset:
- user = {
- 'id': userprofile.user.id,
- 'expanded_name': userprofile.full_name,
- 'small_name': userprofile.user.username,
- 'string': userprofile.email_addr
- }
-
- users[userprofile.user.id] = user
-
- return users
- except Exception:
- pass
-
- def build_search_widget_attrs(self, chosen_users, default_user="you"):
-
- attrs = {
- 'set': self.build_user_list(),
- 'show_from_noentry': "false",
- 'show_x_results': 10,
- 'scrollable': "false",
- 'selectable_limit': -1,
- 'name': "users",
- 'placeholder': "username",
- 'initial': chosen_users,
- 'edit': False
- }
- return attrs
-
class MultipleSelectFilterWidget(forms.Widget):
- def __init__(self, attrs=None):
- super(MultipleSelectFilterWidget, self).__init__(attrs)
- self.attrs = attrs
+ def __init__(self, *args, display_objects=None, filter_items=None, neighbors=None, **kwargs):
+ super(MultipleSelectFilterWidget, self).__init__(*args, **kwargs)
+ self.display_objects = display_objects
+ self.filter_items = filter_items
+ self.neighbors = neighbors
self.template_name = "dashboard/multiple_select_filter_widget.html"
def render(self, name, value, attrs=None, renderer=None):
- attrs = self.attrs
- self.context = self.get_context(name, value, attrs)
- html = render_to_string(self.template_name, context=self.context)
+ context = self.get_context(name, value, attrs)
+ html = render_to_string(self.template_name, context=context)
return mark_safe(html)
def get_context(self, name, value, attrs):
- return attrs
+ return {
+ 'display_objects': self.display_objects,
+ 'neighbors': self.neighbors,
+ 'filter_items': self.filter_items,
+ 'initial_value': value
+ }
class MultipleSelectFilterField(forms.Field):
- def __init__(self, required=True, widget=None, label=None, initial=None,
- help_text='', error_messages=None, show_hidden_initial=False,
- validators=(), localize=False, disabled=False, label_suffix=None):
- """from the documentation:
- # required -- Boolean that specifies whether the field is required.
- # True by default.
- # widget -- A Widget class, or instance of a Widget class, that should
- # be used for this Field when displaying it. Each Field has a
- # default Widget that it'll use if you don't specify this. In
- # most cases, the default widget is TextInput.
- # label -- A verbose name for this field, for use in displaying this
- # field in a form. By default, Django will use a "pretty"
- # version of the form field name, if the Field is part of a
- # Form.
- # initial -- A value to use in this Field's initial display. This value
- # is *not* used as a fallback if data isn't given.
- # help_text -- An optional string to use as "help text" for this Field.
- # error_messages -- An optional dictionary to override the default
- # messages that the field will raise.
- # show_hidden_initial -- Boolean that specifies if it is needed to render a
- # hidden widget with initial value after widget.
- # validators -- List of additional validators to use
- # localize -- Boolean that specifies if the field should be localized.
- # disabled -- Boolean that specifies whether the field is disabled, that
- # is its widget is shown in the form but not editable.
- # label_suffix -- Suffix to be added to the label. Overrides
- # form's label_suffix.
- """
- # this is bad, but django forms are annoying
- self.widget = widget
- if self.widget is None:
- self.widget = MultipleSelectFilterWidget()
- super(MultipleSelectFilterField, self).__init__(
- required=required,
- widget=self.widget,
- label=label,
- initial=None,
- help_text=help_text,
- error_messages=error_messages,
- show_hidden_initial=show_hidden_initial,
- validators=validators,
- localize=localize,
- disabled=disabled,
- label_suffix=label_suffix
- )
+ def __init__(self, **kwargs):
+ self.initial = kwargs.get("initial")
+ super().__init__(**kwargs)
- def clean(data):
- """
- This method will raise a django.forms.ValidationError or return clean data
- """
- return data
+ def to_python(self, value):
+ return json.loads(value)
class FormUtils:
@staticmethod
- def getLabData(multiple_selectable_hosts):
+ def getLabData(multiple_hosts=False):
"""
Gets all labs and thier host profiles and returns a serialized version the form can understand.
Should be rewritten with a related query to make it faster
- Should be moved outside of global scope
"""
+ # javascript truthy variables
+ true = 1
+ false = 0
+ if multiple_hosts:
+ multiple_hosts = true
+ else:
+ multiple_hosts = false
labs = {}
hosts = {}
items = {}
- mapping = {}
+ neighbors = {}
for lab in Lab.objects.all():
- slab = {}
- slab['id'] = "lab_" + str(lab.lab_user.id)
- slab['name'] = lab.name
- slab['description'] = lab.description
- slab['selected'] = 0
- slab['selectable'] = 1
- slab['follow'] = 1
- slab['multiple'] = 0
- items[slab['id']] = slab
- mapping[slab['id']] = []
- labs[slab['id']] = slab
+ lab_node = {
+ 'id': "lab_" + str(lab.lab_user.id),
+ 'model_id': lab.lab_user.id,
+ 'name': lab.name,
+ 'description': lab.description,
+ 'selected': false,
+ 'selectable': true,
+ 'follow': false,
+ 'multiple': false,
+ 'class': 'lab'
+ }
+ if multiple_hosts:
+ # "follow" this lab node to discover more hosts if allowed
+ lab_node['follow'] = true
+ items[lab_node['id']] = lab_node
+ neighbors[lab_node['id']] = []
+ labs[lab_node['id']] = lab_node
+
for host in lab.hostprofiles.all():
- shost = {}
- shost['forms'] = [{"name": "host_name", "type": "text", "placeholder": "hostname"}]
- shost['id'] = "host_" + str(host.id)
- shost['name'] = host.name
- shost['description'] = host.description
- shost['selected'] = 0
- shost['selectable'] = 1
- shost['follow'] = 0
- shost['multiple'] = multiple_selectable_hosts
- items[shost['id']] = shost
- mapping[slab['id']].append(shost['id'])
- if shost['id'] not in mapping:
- mapping[shost['id']] = []
- mapping[shost['id']].append(slab['id'])
- hosts[shost['id']] = shost
-
- filter_objects = [("labs", labs.values()), ("hosts", hosts.values())]
+ host_node = {
+ 'form': {"name": "host_name", "type": "text", "placeholder": "hostname"},
+ 'id': "host_" + str(host.id),
+ 'model_id': host.id,
+ 'name': host.name,
+ 'description': host.description,
+ 'selected': false,
+ 'selectable': true,
+ 'follow': false,
+ 'multiple': multiple_hosts,
+ 'class': 'host'
+ }
+ if multiple_hosts:
+ host_node['values'] = [] # place to store multiple values
+ items[host_node['id']] = host_node
+ neighbors[lab_node['id']].append(host_node['id'])
+ if host_node['id'] not in neighbors:
+ neighbors[host_node['id']] = []
+ neighbors[host_node['id']].append(lab_node['id'])
+ hosts[host_node['id']] = host_node
+
+ display_objects = [("lab", labs.values()), ("host", hosts.values())]
context = {
- 'filter_objects': filter_objects,
- 'mapping': mapping,
+ 'display_objects': display_objects,
+ 'neighbors': neighbors,
'filter_items': items
}
return context
@@ -382,14 +345,10 @@ class FormUtils:
class HardwareDefinitionForm(forms.Form):
def __init__(self, *args, **kwargs):
- selection_data = kwargs.pop("selection_data", False)
super(HardwareDefinitionForm, self).__init__(*args, **kwargs)
- attrs = FormUtils.getLabData(1)
- attrs['selection_data'] = selection_data
+ attrs = FormUtils.getLabData(multiple_hosts=True)
self.fields['filter_field'] = MultipleSelectFilterField(
- widget=MultipleSelectFilterWidget(
- attrs=attrs
- )
+ widget=MultipleSelectFilterWidget(**attrs)
)
@@ -422,20 +381,14 @@ class NetworkConfigurationForm(forms.Form):
class HostSoftwareDefinitionForm(forms.Form):
- fields = ["host_name", "role", "image"]
host_name = forms.CharField(max_length=200, disabled=True, required=False)
- role = forms.ModelChoiceField(queryset=OPNFVRole.objects.all())
- image = forms.ModelChoiceField(queryset=Image.objects.all())
-
+ headnode = forms.BooleanField(required=False, widget=forms.HiddenInput)
-class SoftwareConfigurationForm(forms.Form):
-
- name = forms.CharField(max_length=200)
- description = forms.CharField(widget=forms.Textarea)
- opnfv = forms.BooleanField(disabled=True, required=False)
- installer = forms.ModelChoiceField(queryset=Installer.objects.all(), disabled=True, required=False)
- scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), disabled=True, required=False)
+ def __init__(self, *args, **kwargs):
+ imageQS = kwargs.pop("imageQS")
+ super(HostSoftwareDefinitionForm, self).__init__(*args, **kwargs)
+ self.fields['image'] = forms.ModelChoiceField(queryset=imageQS)
class WorkflowSelectionForm(forms.Form):
@@ -459,7 +412,7 @@ class SnapshotHostSelectForm(forms.Form):
host = forms.CharField()
-class SnapshotMetaForm(forms.Form):
+class BasicMetaForm(forms.Form):
name = forms.CharField()
description = forms.CharField(widget=forms.Textarea)
@@ -473,3 +426,23 @@ class ConfirmationForm(forms.Form):
(False, "Cancel")
)
)
+
+
+class OPNFVSelectionForm(forms.Form):
+ installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=True)
+ scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=True)
+
+
+class OPNFVNetworkRoleForm(forms.Form):
+ role = forms.CharField(max_length=200, disabled=True, required=False)
+
+ def __init__(self, *args, config_bundle, **kwargs):
+ super(OPNFVNetworkRoleForm, self).__init__(*args, **kwargs)
+ self.fields['network'] = forms.ModelChoiceField(
+ queryset=config_bundle.bundle.networks.all()
+ )
+
+
+class OPNFVHostRoleForm(forms.Form):
+ host_name = forms.CharField(max_length=200, disabled=True, required=False)
+ role = forms.ModelChoiceField(queryset=OPNFVRole.objects.all().order_by("name").distinct("name"))
diff --git a/dashboard/src/workflow/models.py b/dashboard/src/workflow/models.py
index beaaee4..6c6bd9a 100644
--- a/dashboard/src/workflow/models.py
+++ b/dashboard/src/workflow/models.py
@@ -19,8 +19,9 @@ import requests
from workflow.forms import ConfirmationForm
from api.models import JobFactory
from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
-from resource_inventory.models import Image, GenericInterface
+from resource_inventory.models import Image, GenericInterface, OPNFVConfig, HostOPNFVConfig, NetworkRole
from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
from notifier.manager import NotificationHandler
from booking.models import Booking
@@ -142,10 +143,12 @@ class BookingAuthManager():
currently checks if the booking uses multiple servers. if it does, then the owner must be a PTL,
which is checked using the provided info file
"""
- if len(booking.resource.template.getHosts()) < 2:
- return True # if they only have one server, we dont care
if booking.owner.userprofile.booking_privledge:
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.getHosts()) < 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
ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE))
@@ -155,12 +158,52 @@ class BookingAuthManager():
return False
+class WorkflowStepStatus(object):
+ UNTOUCHED = 0
+ INVALID = 100
+ VALID = 200
+
+
class WorkflowStep(object):
template = 'bad_request.html'
title = "Generic Step"
description = "You were led here by mistake"
short_title = "error"
metastep = None
+ # phasing out metastep:
+
+ valid = WorkflowStepStatus.UNTOUCHED
+ message = ""
+
+ enabled = True
+
+ def cleanup(self):
+ raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete implemented cleanup() method")
+
+ def enable(self):
+ if not self.enabled:
+ self.enabled = True
+
+ def disable(self):
+ if self.enabled:
+ self.cleanup()
+ self.enabled = False
+
+ def set_invalid(self, message, code=WorkflowStepStatus.INVALID):
+ self.valid = code
+ self.message = message
+
+ def set_valid(self, message, code=WorkflowStepStatus.VALID):
+ self.valid = code
+ self.message = message
+
+ def to_json(self):
+ return {
+ 'title': self.short_title,
+ 'enabled': self.enabled,
+ 'valid': self.valid,
+ 'message': self.message,
+ }
def __init__(self, id, repo=None):
self.repo = repo
@@ -197,31 +240,73 @@ class WorkflowStep(object):
return self.repo.put(key, value, self.id)
+"""
+subclassing notes:
+ subclasses have to define the following class attributes:
+ self.select_repo_key: where the selected "object" or "bundle" is to be placed in the repo
+ self.form: the form to be used
+ alert_bundle_missing(): what message to display if a user does not select/selects an invalid object
+ get_form_queryset(): generate a queryset to be used to filter available items for the field
+ get_page_context(): return simple context such as page header and other info
+"""
+
+
+class AbstractSelectOrCreate(WorkflowStep):
+ template = 'dashboard/genericselect.html'
+ title = "Select a Bundle"
+ short_title = "select"
+ description = "Generic bundle selector step"
+
+ select_repo_key = None
+ form = None # subclasses are expected to use a form that is a subclass of SearchableSelectGenericForm
+
+ def alert_bundle_missing(self): # override in subclasses to change message if field isn't filled out
+ self.set_invalid("Please select a valid bundle")
+
+ def post_render(self, request):
+ context = self.get_context()
+ form = self.form(request.POST, queryset=self.get_form_queryset())
+ if form.is_valid():
+ bundle = form.get_validated_bundle()
+ if not bundle:
+ self.alert_bundle_missing()
+ return render(request, self.template, context)
+ self.repo_put(self.select_repo_key, bundle)
+ self.put_confirm_info(bundle)
+ self.set_valid("Step Completed")
+ else:
+ self.alert_bundle_missing()
+ messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
+
+ return self.render(request)
+
+ def get_context(self):
+ default = []
+
+ bundle = self.repo_get(self.select_repo_key, False)
+ if bundle:
+ default.append(bundle)
+
+ form = self.form(queryset=self.get_form_queryset(), initial=default)
+
+ context = {'form': form, **self.get_page_context()}
+ context.update(super().get_context())
+
+ return context
+
+ def get_page_context():
+ return {
+ 'select_type': 'generic',
+ 'select_type_title': 'Generic Bundle'
+ }
+
+
class Confirmation_Step(WorkflowStep):
template = 'workflow/confirm.html'
title = "Confirm Changes"
description = "Does this all look right?"
- def get_vlan_warning(self):
- grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False)
- if not grb:
- return 0
- if self.repo.BOOKING_MODELS not in self.repo.el:
- return 0
- vlan_manager = grb.lab.vlan_manager
- if vlan_manager is None:
- return 0
- hosts = grb.getHosts()
- for host in hosts:
- for interface in host.generic_interfaces.all():
- for vlan in interface.vlans.all():
- if vlan.public:
- if not vlan_manager.public_vlan_is_available(vlan.vlan_id):
- return 1
- else:
- if not vlan_manager.is_available(vlan.vlan_id):
- return 1 # There is a problem with these vlans
- return 0
+ short_title = "confirm"
def get_context(self):
context = super(Confirmation_Step, self).get_context()
@@ -230,7 +315,6 @@ class Confirmation_Step(WorkflowStep):
self.repo_get(self.repo.CONFIRMATION),
default_flow_style=False
).strip()
- context['vlan_warning'] = self.get_vlan_warning()
return context
@@ -261,39 +345,8 @@ class Confirmation_Step(WorkflowStep):
pass
else:
- if "vlan_input" in request.POST:
- if request.POST.get("vlan_input") == "True":
- self.translate_vlans()
- return self.render(request)
pass
- def translate_vlans(self):
- grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False)
- if not grb:
- return 0
- vlan_manager = grb.lab.vlan_manager
- if vlan_manager is None:
- return 0
- hosts = grb.getHosts()
- for host in hosts:
- for interface in host.generic_interfaces.all():
- for vlan in interface.vlans.all():
- if not vlan.public:
- if not vlan_manager.is_available(vlan.vlan_id):
- vlan.vlan_id = vlan_manager.get_vlan()
- vlan.save()
- else:
- if not vlan_manager.public_vlan_is_available(vlan.vlan_id):
- pub_vlan = vlan_manager.get_public_vlan()
- vlan.vlan_id = pub_vlan.vlan
- vlan.save()
-
-
-class Workflow():
-
- steps = []
- active_index = 0
-
class Repository():
@@ -303,6 +356,7 @@ class Repository():
CONFIRMATION = "confirmation"
SELECTED_GRESOURCE_BUNDLE = "selected generic bundle pk"
SELECTED_CONFIG_BUNDLE = "selected config bundle pk"
+ SELECTED_OPNFV_CONFIG = "selected opnfv deployment config"
GRESOURCE_BUNDLE_MODELS = "generic_resource_bundle_models"
GRESOURCE_BUNDLE_INFO = "generic_resource_bundle_info"
BOOKING = "booking"
@@ -312,7 +366,9 @@ class Repository():
SWCONF_HOSTS = "swconf_hosts"
BOOKING_MODELS = "booking models"
CONFIG_MODELS = "configuration bundle models"
+ OPNFV_MODELS = "opnfv configuration models"
SESSION_USER = "session owner user account"
+ SESSION_MANAGER = "session manager for current session"
VALIDATED_MODEL_GRB = "valid grb config model instance in db"
VALIDATED_MODEL_CONFIG = "valid config model instance in db"
VALIDATED_MODEL_BOOKING = "valid booking model instance in db"
@@ -323,8 +379,8 @@ class Repository():
SNAPSHOT_DESC = "description of the snapshot"
BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking"
- #migratory elements of segmented workflow
- #each of these is the end result of a different workflow.
+ # migratory elements of segmented workflow
+ # each of these is the end result of a different workflow.
HAS_RESULT = "whether or not workflow has a result"
RESULT_KEY = "key for target index that result will be put into in parent"
RESULT = "result object from workflow"
@@ -340,6 +396,7 @@ class Repository():
self.el[key] = value
def get(self, key, default, id):
+
self.add_get_history(key, id)
return self.el.get(key, default)
@@ -364,6 +421,7 @@ class Repository():
errors = self.make_snapshot()
if errors:
return errors
+
# if GRB WF, create it
if self.GRESOURCE_BUNDLE_MODELS in self.el:
errors = self.make_generic_resource_bundle()
@@ -383,6 +441,14 @@ class Repository():
self.el[self.RESULT_KEY] = self.SELECTED_CONFIG_BUNDLE
return
+ if self.OPNFV_MODELS in self.el:
+ errors = self.make_opnfv_config()
+ if errors:
+ return errors
+ else:
+ self.el[self.HAS_RESULT] = True
+ self.el[self.RESULT_KEY] = self.SELECTED_OPNFV_CONFIG
+
if self.BOOKING_MODELS in self.el:
errors = self.make_booking()
if errors:
@@ -424,6 +490,9 @@ class Repository():
pass
JobFactory.makeSnapshotTask(image, booking, host)
+ self.el[self.RESULT] = image
+ self.el[self.HAS_RESULT] = True
+
def make_generic_resource_bundle(self):
owner = self.el[self.SESSION_USER]
if self.GRESOURCE_BUNDLE_MODELS in self.el:
@@ -452,31 +521,37 @@ class Repository():
except Exception as e:
return "GRB, saving hosts generated exception: " + str(e) + " CODE:0x0005"
+ if 'networks' in models:
+ for net in models['networks'].values():
+ net.bundle = bundle
+ net.save()
+
if 'interfaces' in models:
for interface_set in models['interfaces'].values():
for interface in interface_set:
try:
interface.host = interface.host
interface.save()
- except Exception as e:
+ except Exception:
return "GRB, saving interface " + str(interface) + " failed. CODE:0x0019"
else:
return "GRB, no interface set provided. CODE:0x001a"
- if 'vlans' in models:
- for resource_name, mapping in models['vlans'].items():
- for profile_name, vlan_set in mapping.items():
+ if 'connections' in models:
+ for resource_name, mapping in models['connections'].items():
+ for profile_name, connection_set in mapping.items():
interface = GenericInterface.objects.get(
profile__name=profile_name,
host__resource__name=resource_name,
host__resource__bundle=models['bundle']
)
- for vlan in vlan_set:
+ for connection in connection_set:
try:
- vlan.save()
- interface.vlans.add(vlan)
+ connection.network = connection.network
+ connection.save()
+ interface.connections.add(connection)
except Exception as e:
- return "GRB, saving vlan " + str(vlan) + " failed. Exception: " + str(e) + ". CODE:0x0017"
+ return "GRB, saving vlan " + str(connection) + " failed. Exception: " + str(e) + ". CODE:0x0017"
else:
return "GRB, no vlan set provided. CODE:0x0018"
@@ -484,13 +559,14 @@ class Repository():
return "GRB no models given. CODE:0x0001"
self.el[self.RESULT] = bundle
+ self.el[self.HAS_RESULT] = True
return False
def make_software_config_bundle(self):
models = self.el[self.CONFIG_MODELS]
if 'bundle' in models:
bundle = models['bundle']
- bundle.bundle = bundle.bundle
+ bundle.bundle = self.el[self.SELECTED_GRESOURCE_BUNDLE]
try:
bundle.save()
except Exception as e:
@@ -528,18 +604,22 @@ class Repository():
models = self.el[self.BOOKING_MODELS]
owner = self.el[self.SESSION_USER]
+ if 'booking' in models:
+ booking = models['booking']
+ else:
+ return "BOOK, no booking model exists. CODE:0x000f"
+
+ selected_grb = None
+
if self.SELECTED_GRESOURCE_BUNDLE in self.el:
selected_grb = self.el[self.SELECTED_GRESOURCE_BUNDLE]
else:
return "BOOK, no selected resource. CODE:0x000e"
- if not self.reserve_vlans(selected_grb):
- return "BOOK, vlans not available"
+ if self.SELECTED_CONFIG_BUNDLE not in self.el:
+ return "BOOK, no selected config bundle. CODE:0x001f"
- if 'booking' in models:
- booking = models['booking']
- else:
- return "BOOK, no booking model exists. CODE:0x000f"
+ booking.config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
if not booking.start:
return "BOOK, booking has no start. CODE:0x0010"
@@ -561,7 +641,6 @@ class Repository():
booking.resource = resource_bundle
booking.owner = owner
- booking.config_bundle = booking.config_bundle
booking.lab = selected_grb.lab
is_allowed = BookingAuthManager().booking_allowed(booking, self)
@@ -577,7 +656,7 @@ class Repository():
booking.collaborators.add(collaborator)
try:
- booking.pdf = ResourceManager().makePDF(booking.resource)
+ booking.pdf = PDFTemplater.makePDF(booking)
booking.save()
except Exception as e:
return "BOOK, failed to create Pod Desriptor File: " + str(e)
@@ -592,33 +671,62 @@ class Repository():
except Exception as e:
return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016"
- def reserve_vlans(self, grb):
- """
- True is success
- """
- vlans = []
- public_vlan = None
- vlan_manager = grb.lab.vlan_manager
- if vlan_manager is None:
- return True
- for host in grb.getHosts():
- for interface in host.generic_interfaces.all():
- for vlan in interface.vlans.all():
- if vlan.public:
- public_vlan = vlan
- else:
- vlans.append(vlan.vlan_id)
-
- try:
- vlan_manager.reserve_vlans(vlans)
- vlan_manager.reserve_public_vlan(public_vlan.vlan_id)
- return True
- except Exception:
- return False
+ self.el[self.RESULT] = booking
+ self.el[self.HAS_RESULT] = True
+
+ def make_opnfv_config(self):
+ opnfv_models = self.el[self.OPNFV_MODELS]
+ config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
+ if not config_bundle:
+ return "No Configuration bundle selected"
+ info = opnfv_models.get("meta", {})
+ name = info.get("name", False)
+ desc = info.get("description", False)
+ if not (name and desc):
+ return "No name or description given"
+ installer = opnfv_models['installer_chosen']
+ if not installer:
+ return "No OPNFV Installer chosen"
+ scenario = opnfv_models['scenario_chosen']
+ if not scenario:
+ return "No OPNFV Scenario chosen"
+
+ opnfv_config = OPNFVConfig.objects.create(
+ bundle=config_bundle,
+ name=name,
+ description=desc,
+ installer=installer,
+ scenario=scenario
+ )
+
+ network_roles = opnfv_models['network_roles']
+ for net_role in network_roles:
+ opnfv_config.networks.add(
+ NetworkRole.objects.create(
+ name=net_role['role'],
+ network=net_role['network']
+ )
+ )
+
+ host_roles = opnfv_models['host_roles']
+ for host_role in host_roles:
+ config = config_bundle.hostConfigurations.get(
+ host__resource__name=host_role['host_name']
+ )
+ HostOPNFVConfig.objects.create(
+ role=host_role['role'],
+ host_config=config,
+ opnfv_config=opnfv_config
+ )
+
+ self.el[self.RESULT] = opnfv_config
+ self.el[self.HAS_RESULT] = True
def __init__(self):
self.el = {}
self.el[self.CONFIRMATION] = {}
self.el["active_step"] = 0
+ self.el[self.HAS_RESULT] = False
+ self.el[self.RESULT] = None
self.get_history = {}
self.put_history = {}
diff --git a/dashboard/src/workflow/opnfv_workflow.py b/dashboard/src/workflow/opnfv_workflow.py
new file mode 100644
index 0000000..7d499ec
--- /dev/null
+++ b/dashboard/src/workflow/opnfv_workflow.py
@@ -0,0 +1,299 @@
+##############################################################################
+# 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 django.forms import formset_factory
+
+from workflow.models import WorkflowStep, AbstractSelectOrCreate
+from resource_inventory.models import ConfigBundle, OPNFV_SETTINGS
+from workflow.forms import OPNFVSelectionForm, OPNFVNetworkRoleForm, OPNFVHostRoleForm, SWConfigSelectorForm, BasicMetaForm
+
+
+class OPNFV_Resource_Select(AbstractSelectOrCreate):
+ title = "Select Software Configuration"
+ description = "Choose the software bundle you wish to use as a base for your OPNFV configuration"
+ short_title = "software config"
+ form = SWConfigSelectorForm
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE
+
+ def get_form_queryset(self):
+ user = self.repo_get(self.repo.SESSION_USER)
+ qs = ConfigBundle.objects.filter(owner=user)
+ return qs
+
+ def put_confirm_info(self, bundle):
+ confirm_dict = self.repo_get(self.repo.CONFIRMATION)
+ confirm_dict['software bundle'] = bundle.name
+ confirm_dict['hardware POD'] = bundle.bundle.name
+ self.repo_put(self.repo.CONFIRMATION, confirm_dict)
+
+ def get_page_context(self):
+ return {
+ 'select_type': 'swconfig',
+ 'select_type_title': 'Software Config',
+ 'addable_type_num': 2
+ }
+
+
+class Pick_Installer(WorkflowStep):
+ template = 'config_bundle/steps/pick_installer.html'
+ title = 'Pick OPNFV Installer'
+ description = 'Choose which OPNFV installer to use'
+ short_title = "opnfv installer"
+ modified_key = "installer_step"
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ installer = models.get("installer_chosen")
+ scenario = models.get("scenario_chosen")
+ if not (installer and scenario):
+ return
+ confirm['installer'] = installer.name
+ confirm['scenario'] = scenario.name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def get_context(self):
+ context = super(Pick_Installer, self).get_context()
+
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ initial = {
+ "installer": models.get("installer_chosen"),
+ "scenario": models.get("scenario_chosen")
+ }
+
+ context["form"] = OPNFVSelectionForm(initial=initial)
+ return context
+
+ def post_render(self, request):
+ form = OPNFVSelectionForm(request.POST)
+ if form.is_valid():
+ installer = form.cleaned_data['installer']
+ scenario = form.cleaned_data['scenario']
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ models['installer_chosen'] = installer
+ models['scenario_chosen'] = scenario
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+ self.set_valid("Step Completed")
+ else:
+ self.set_invalid("Please select an Installer and Scenario")
+
+ return self.render(request)
+
+
+class Assign_Network_Roles(WorkflowStep):
+ template = 'config_bundle/steps/assign_network_roles.html'
+ title = 'Pick Network Roles'
+ description = 'Choose what role each network should get'
+ short_title = "network roles"
+ modified_key = "net_roles_step"
+
+ """
+ to do initial filling, repo should have a "network_roles" array with the following structure for each element:
+ {
+ "role": <NetworkRole object ref>,
+ "network": <Network object ref>
+ }
+ """
+ def create_netformset(self, roles, config_bundle, data=None):
+ roles_initial = []
+ set_roles = self.repo_get(self.repo.OPNFV_MODELS, {}).get("network_roles")
+ if set_roles:
+ roles_initial = set_roles
+ else:
+ for role in OPNFV_SETTINGS.NETWORK_ROLES:
+ roles_initial.append({"role": role})
+
+ Formset = formset_factory(OPNFVNetworkRoleForm, extra=0)
+ kwargs = {
+ "initial": roles_initial,
+ "form_kwargs": {"config_bundle": config_bundle}
+ }
+ formset = None
+ if data:
+ formset = Formset(data, **kwargs)
+ else:
+ formset = Formset(**kwargs)
+ return formset
+
+ def get_context(self):
+ context = super(Assign_Network_Roles, self).get_context()
+ config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
+ if config_bundle is None:
+ context["unavailable"] = True
+ return context
+
+ roles = OPNFV_SETTINGS.NETWORK_ROLES
+ formset = self.create_netformset(roles, config_bundle)
+ context['formset'] = formset
+
+ return context
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ roles = models.get("network_roles")
+ if not roles:
+ return
+ confirm['network roles'] = {}
+ for role in roles:
+ confirm['network roles'][role['role']] = role['network'].name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
+ roles = OPNFV_SETTINGS.NETWORK_ROLES
+ net_role_formset = self.create_netformset(roles, config_bundle, data=request.POST)
+ if net_role_formset.is_valid():
+ results = []
+ for form in net_role_formset:
+ results.append({
+ "role": form.cleaned_data['role'],
+ "network": form.cleaned_data['network']
+ })
+ models['network_roles'] = results
+ self.set_valid("Completed")
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+ else:
+ self.set_invalid("Please complete all fields")
+ return self.render(request)
+
+
+class Assign_Host_Roles(WorkflowStep): # taken verbatim from Define_Software in sw workflow, merge the two?
+ template = 'config_bundle/steps/assign_host_roles.html'
+ title = 'Pick Host Roles'
+ description = "Choose the role each machine will have in your OPNFV pod"
+ short_title = "host roles"
+ modified_key = "host_roles_step"
+
+ def create_host_role_formset(self, hostlist=[], data=None):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ host_roles = models.get("host_roles", [])
+ if not host_roles:
+ for host in hostlist:
+ initial = {"host_name": host.resource.name}
+ host_roles.append(initial)
+ models['host_roles'] = host_roles
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+
+ HostFormset = formset_factory(OPNFVHostRoleForm, extra=0)
+
+ kwargs = {"initial": host_roles}
+ formset = None
+ if data:
+ formset = HostFormset(data, **kwargs)
+ else:
+ formset = HostFormset(**kwargs)
+
+ return formset
+
+ def get_context(self):
+ context = super(Assign_Host_Roles, self).get_context()
+ config = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE)
+ if config is None:
+ context['error'] = "Please select a Configuration on the first step"
+
+ formset = self.create_host_role_formset(hostlist=config.bundle.getHosts())
+ context['formset'] = formset
+
+ return context
+
+ def get_host_role_mapping(self, host_roles, hostname):
+ for obj in host_roles:
+ if hostname == obj['host_name']:
+ return obj
+ return None
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ roles = models.get("host_roles")
+ if not roles:
+ return
+ confirm['host roles'] = {}
+ for role in roles:
+ confirm['host roles'][role['host_name']] = role['role'].name
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ formset = self.create_host_role_formset(data=request.POST)
+
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ host_roles = models.get("host_roles", [])
+
+ has_jumphost = False
+ if formset.is_valid():
+ for form in formset:
+ hostname = form.cleaned_data['host_name']
+ role = form.cleaned_data['role']
+ mapping = self.get_host_role_mapping(host_roles, hostname)
+ mapping['role'] = role
+ if "jumphost" in role.name.lower():
+ has_jumphost = True
+
+ models['host_roles'] = host_roles
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+
+ if not has_jumphost:
+ self.set_invalid('Must have at least one "Jumphost" per POD')
+ else:
+ self.set_valid("Completed")
+ else:
+ self.set_invalid("Please complete all fields")
+
+ return self.render(request)
+
+
+class MetaInfo(WorkflowStep):
+ template = 'config_bundle/steps/config_software.html'
+ title = "Other Info"
+ description = "Give your software config a name, description, and other stuff"
+ short_title = "config info"
+
+ def get_context(self):
+ context = super(MetaInfo, self).get_context()
+
+ initial = self.repo_get(self.repo.OPNFV_MODELS, {}).get("meta", {})
+ context["form"] = BasicMetaForm(initial=initial)
+ return context
+
+ def update_confirmation(self):
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ meta = models.get("meta")
+ if not meta:
+ return
+ confirm['name'] = meta['name']
+ confirm['description'] = meta['description']
+ self.repo_put(self.repo.CONFIRMATION, confirm)
+
+ def post_render(self, request):
+ models = self.repo_get(self.repo.OPNFV_MODELS, {})
+ info = models.get("meta", {})
+
+ form = BasicMetaForm(request.POST)
+ if form.is_valid():
+ info['name'] = form.cleaned_data['name']
+ info['description'] = form.cleaned_data['description']
+ models['meta'] = info
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ self.update_confirmation()
+ self.set_valid("Complete")
+ else:
+ self.set_invalid("Please correct the errors shown below")
+
+ self.repo_put(self.repo.OPNFV_MODELS, models)
+ return self.render(request)
diff --git a/dashboard/src/workflow/resource_bundle_workflow.py b/dashboard/src/workflow/resource_bundle_workflow.py
index 4858ebe..06737d2 100644
--- a/dashboard/src/workflow/resource_bundle_workflow.py
+++ b/dashboard/src/workflow/resource_bundle_workflow.py
@@ -10,6 +10,7 @@
from django.shortcuts import render
from django.forms import formset_factory
+from django.conf import settings
import json
import re
@@ -25,11 +26,12 @@ from workflow.forms import (
)
from resource_inventory.models import (
GenericResourceBundle,
- Vlan,
GenericInterface,
GenericHost,
GenericResource,
- HostProfile
+ HostProfile,
+ Network,
+ NetworkConnection
)
from dashboard.exceptions import (
InvalidVlanConfigurationException,
@@ -50,65 +52,47 @@ class Define_Hardware(WorkflowStep):
description = "Choose the type and amount of machines you want"
short_title = "hosts"
+ def __init__(self, *args, **kwargs):
+ self.form = None
+ super().__init__(*args, **kwargs)
+
def get_context(self):
context = super(Define_Hardware, self).get_context()
- selection_data = {"hosts": {}, "labs": {}}
- models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
- hosts = models.get("hosts", [])
- for host in hosts:
- profile_id = "host_" + str(host.profile.id)
- if profile_id not in selection_data['hosts']:
- selection_data['hosts'][profile_id] = []
- selection_data['hosts'][profile_id].append({"host_name": host.resource.name, "class": profile_id})
-
- if models.get("bundle", GenericResourceBundle()).lab:
- selection_data['labs'] = {"lab_" + str(models.get("bundle").lab.lab_user.id): "true"}
-
- form = HardwareDefinitionForm(
- selection_data=selection_data
- )
- context['form'] = form
+ context['form'] = self.form or HardwareDefinitionForm()
return context
- def render(self, request):
- self.context = self.get_context()
- return render(request, self.template, self.context)
-
def update_models(self, data):
- data = json.loads(data['filter_field'])
+ data = data['filter_field']
models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
models['hosts'] = [] # This will always clear existing data when this step changes
models['interfaces'] = {}
if "bundle" not in models:
models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))
- host_data = data['hosts']
+ host_data = data['host']
names = {}
- for host_dict in host_data:
- id = host_dict['class']
- # bit of formatting
- id = int(id.split("_")[-1])
+ for host_profile_dict in host_data.values():
+ id = host_profile_dict['id']
profile = HostProfile.objects.get(id=id)
# instantiate genericHost and store in repo
- name = host_dict['host_name']
- if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
- raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
- if name in names:
- raise NonUniqueHostnameException("All hosts must have unique names")
- names[name] = True
- genericResource = GenericResource(bundle=models['bundle'], name=name)
- genericHost = GenericHost(profile=profile, resource=genericResource)
- models['hosts'].append(genericHost)
- for interface_profile in profile.interfaceprofile.all():
- genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
- if genericHost.resource.name not in models['interfaces']:
- models['interfaces'][genericHost.resource.name] = []
- models['interfaces'][genericHost.resource.name].append(genericInterface)
+ for name in host_profile_dict['values'].values():
+ if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
+ raise InvalidHostnameException("Invalid hostname: '" + name + "'")
+ if name in names:
+ raise NonUniqueHostnameException("All hosts must have unique names")
+ names[name] = True
+ genericResource = GenericResource(bundle=models['bundle'], name=name)
+ genericHost = GenericHost(profile=profile, resource=genericResource)
+ models['hosts'].append(genericHost)
+ for interface_profile in profile.interfaceprofile.all():
+ genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
+ if genericHost.resource.name not in models['interfaces']:
+ models['interfaces'][genericHost.resource.name] = []
+ models['interfaces'][genericHost.resource.name].append(genericInterface)
# add selected lab to models
- for lab_dict in data['labs']:
- if list(lab_dict.values())[0]: # True for lab the user selected
- lab_user_id = int(list(lab_dict.keys())[0].split("_")[-1])
- models['bundle'].lab = Lab.objects.get(lab_user__id=lab_user_id)
+ for lab_dict in data['lab'].values():
+ if lab_dict['selected']:
+ models['bundle'].lab = Lab.objects.get(lab_user__id=lab_dict['id'])
break # if somehow we get two 'true' labs, we only use one
# return to repo
@@ -131,17 +115,13 @@ class Define_Hardware(WorkflowStep):
try:
self.form = HardwareDefinitionForm(request.POST)
if self.form.is_valid():
- if len(json.loads(self.form.cleaned_data['filter_field'])['labs']) != 1:
- self.metastep.set_invalid("Please select one lab")
- else:
- self.update_models(self.form.cleaned_data)
- self.update_confirmation()
- self.metastep.set_valid("Step Completed")
+ self.update_models(self.form.cleaned_data)
+ self.update_confirmation()
+ self.set_valid("Step Completed")
else:
- self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
- pass
+ self.set_invalid("Please complete the fields highlighted in red to continue")
except Exception as e:
- self.metastep.set_invalid(str(e))
+ self.set_invalid(str(e))
self.context = self.get_context()
return render(request, self.template, self.context)
@@ -171,53 +151,55 @@ class Define_Nets(WorkflowStep):
except Exception:
return None
+ def make_mx_host_dict(self, generic_host):
+ host = {
+ 'id': generic_host.resource.name,
+ 'interfaces': [],
+ 'value': {
+ "name": generic_host.resource.name,
+ "description": generic_host.profile.description
+ }
+ }
+ for iface in generic_host.profile.interfaceprofile.all():
+ host['interfaces'].append({
+ "name": iface.name,
+ "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type
+ })
+ return host
+
def get_context(self):
- # TODO: render *primarily* on hosts in repo models
context = super(Define_Nets, self).get_context()
- context['form'] = NetworkDefinitionForm()
+ context.update({
+ 'form': NetworkDefinitionForm(),
+ 'debug': settings.DEBUG,
+ 'hosts': [],
+ 'added_hosts': [],
+ 'removed_hosts': []
+ })
+ vlans = self.get_vlans()
+ if vlans:
+ context['vlans'] = vlans
try:
- context['hosts'] = []
models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
- vlans = self.get_vlans()
- if vlans:
- context['vlans'] = vlans
hosts = models.get("hosts", [])
- hostlist = self.repo_get(self.repo.GRB_LAST_HOSTLIST, None)
- added_list = []
- added_dict = {}
- context['added_hosts'] = []
- if hostlist is not None:
- new_hostlist = []
- for host in models['hosts']:
- intcount = host.profile.interfaceprofile.count()
- new_hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
- context['removed_hosts'] = list(set(hostlist) - set(new_hostlist))
- added_list = list(set(new_hostlist) - set(hostlist))
- for hoststr in added_list:
- key = hoststr.split("*")[0]
- added_dict[key] = hoststr
+ # calculate if the selected hosts have changed
+ added_hosts = set()
+ host_set = set(self.repo_get(self.repo.GRB_LAST_HOSTLIST, []))
+ if len(host_set):
+ new_host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']])
+ context['removed_hosts'] = [h.split("*")[0] for h in (host_set - new_host_set)]
+ added_hosts.update([h.split("*")[0] for h in (new_host_set - host_set)])
+
+ # add all host info to context
for generic_host in hosts:
- host_profile = generic_host.profile
- host = {}
- host['id'] = generic_host.resource.name
- host['interfaces'] = []
- for iface in host_profile.interfaceprofile.all():
- host['interfaces'].append(
- {
- "name": iface.name,
- "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type
- }
- )
- host['value'] = {"name": generic_host.resource.name}
- host['value']['description'] = generic_host.profile.description
- context['hosts'].append(json.dumps(host))
- if host['id'] in added_dict:
- context['added_hosts'].append(json.dumps(host))
+ host = self.make_mx_host_dict(generic_host)
+ host_serialized = json.dumps(host)
+ context['hosts'].append(host_serialized)
+ if host['id'] in added_hosts:
+ context['added_hosts'].append(host_serialized)
bundle = models.get("bundle", False)
- if bundle and bundle.xml:
- context['xml'] = bundle.xml
- else:
- context['xml'] = False
+ if bundle:
+ context['xml'] = bundle.xml or False
except Exception:
pass
@@ -227,27 +209,24 @@ class Define_Nets(WorkflowStep):
def post_render(self, request):
models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
if 'hosts' in models:
- hostlist = []
- for host in models['hosts']:
- intcount = host.profile.interfaceprofile.count()
- hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
- self.repo_put(self.repo.GRB_LAST_HOSTLIST, hostlist)
+ host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']])
+ self.repo_put(self.repo.GRB_LAST_HOSTLIST, host_set)
try:
xmlData = request.POST.get("xml")
self.updateModels(xmlData)
# update model with xml
- self.metastep.set_valid("Networks applied successfully")
+ self.set_valid("Networks applied successfully")
except ResourceAvailabilityException:
- self.metastep.set_invalid("Public network not availble")
- except Exception:
- self.metastep.set_invalid("An error occurred when applying networks")
+ self.set_invalid("Public network not availble")
+ except Exception as e:
+ self.set_invalid("An error occurred when applying networks: " + str(e))
return self.render(request)
def updateModels(self, xmlData):
models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
- models["vlans"] = {}
- given_hosts, interfaces = self.parseXml(xmlData)
- vlan_manager = models['bundle'].lab.vlan_manager
+ models["connections"] = {}
+ models['networks'] = {}
+ given_hosts, interfaces, networks = self.parseXml(xmlData)
existing_host_list = models.get("hosts", [])
existing_hosts = {} # maps id to host
for host in existing_host_list:
@@ -255,104 +234,133 @@ class Define_Nets(WorkflowStep):
bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
+ for net_id, net in networks.items():
+ network = Network()
+ network.name = net['name']
+ network.bundle = bundle
+ network.is_public = net['public']
+ models['networks'][net_id] = network
+
for hostid, given_host in given_hosts.items():
existing_host = existing_hosts[hostid[5:]]
for ifaceId in given_host['interfaces']:
iface = interfaces[ifaceId]
- if existing_host.resource.name not in models['vlans']:
- models['vlans'][existing_host.resource.name] = {}
- models['vlans'][existing_host.resource.name][iface['profile_name']] = []
- for network in iface['networks']:
- vlan_id = network['network']['vlan']
- is_public = network['network']['public']
- if is_public:
- public_net = vlan_manager.get_public_vlan()
- if public_net is None:
- raise ResourceAvailabilityException("No public networks available")
- vlan_id = vlan_manager.get_public_vlan().vlan
- vlan = Vlan(vlan_id=vlan_id, tagged=network['tagged'], public=is_public)
- models['vlans'][existing_host.resource.name][iface['profile_name']].append(vlan)
+ if existing_host.resource.name not in models['connections']:
+ models['connections'][existing_host.resource.name] = {}
+ models['connections'][existing_host.resource.name][iface['profile_name']] = []
+ for connection in iface['connections']:
+ network_id = connection['network']
+ net = models['networks'][network_id]
+ connection = NetworkConnection(vlan_is_tagged=connection['tagged'], network=net)
+ models['connections'][existing_host.resource.name][iface['profile_name']].append(connection)
bundle.xml = xmlData
self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
- # serialize and deserialize xml from mxGraph
- def parseXml(self, xmlString):
- parent_nets = {} # map network ports to networks
- networks = {} # maps net id to network object
- hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks
- interfaces = {} # maps id -> interface
+ def decomposeXml(self, xmlString):
+ """
+ This function takes in an xml doc from our front end
+ and returns dictionaries that map cellIds to the xml
+ nodes themselves. There is no unpacking of the
+ xml objects, just grouping and organizing
+ """
+
+ connections = {}
+ networks = {}
+ hosts = {}
+ interfaces = {}
+ network_ports = {}
+
xmlDom = minidom.parseString(xmlString)
root = xmlDom.documentElement.firstChild
- netids = {}
- untagged_ints = {}
for cell in root.childNodes:
cellId = cell.getAttribute('id')
+ group = cellId.split("_")[0]
+ parentGroup = cell.getAttribute("parent").split("_")[0]
+ # place cell into correct group
if cell.getAttribute("edge"):
- # cell is a network connection
- escaped_json_str = cell.getAttribute("value")
- json_str = escaped_json_str.replace('&quot;', '"')
- attributes = json.loads(json_str)
- tagged = attributes['tagged']
- interface = None
- network = None
- src = cell.getAttribute("source")
- tgt = cell.getAttribute("target")
- if src in parent_nets:
- # src is a network port
- network = networks[parent_nets[src]]
- if tgt in untagged_ints and not tagged:
- raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
- interface = interfaces[tgt]
- untagged_ints[tgt] = True
- else:
- network = networks[parent_nets[tgt]]
- if src in untagged_ints and not tagged:
- raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
- interface = interfaces[src]
- untagged_ints[src] = True
- interface['networks'].append({"network": network, "tagged": tagged})
-
- elif "network" in cellId: # cell is a network
- escaped_json_str = cell.getAttribute("value")
- json_str = escaped_json_str.replace('&quot;', '"')
- net_info = json.loads(json_str)
- nid = net_info['vlan_id']
- public = net_info['public']
- try:
- int_netid = int(nid)
- assert public or int_netid > 1, "Net id is 1 or lower"
- assert int_netid < 4095, "Net id is 4095 or greater"
- except Exception:
- raise InvalidVlanConfigurationException("VLAN ID is not an integer more than 1 and less than 4095")
- if nid in netids:
- raise NetworkExistsException("Non unique network id found")
- else:
- pass
- network = {"name": net_info['name'], "vlan": net_info['vlan_id'], "public": public}
- netids[net_info['vlan_id']] = True
- networks[cellId] = network
-
- elif "host" in cellId: # cell is a host/machine
- # TODO gather host info
- cell_json_str = cell.getAttribute("value")
- cell_json = json.loads(cell_json_str)
- host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']}
- hosts[cellId] = host
-
- elif cell.hasAttribute("parent"):
- parentId = cell.getAttribute('parent')
- if "network" in parentId:
- parent_nets[cellId] = parentId
- elif "host" in parentId:
- # TODO gather iface info
- cell_json_str = cell.getAttribute("value")
- cell_json = json.loads(cell_json_str)
- iface = {"name": cellId, "networks": [], "profile_name": cell_json['name']}
- hosts[parentId]['interfaces'].append(cellId)
- interfaces[cellId] = iface
- return hosts, interfaces
+ connections[cellId] = cell
+
+ elif "network" in group:
+ networks[cellId] = cell
+
+ elif "host" in group:
+ hosts[cellId] = cell
+
+ elif "host" in parentGroup:
+ interfaces[cellId] = cell
+
+ # make network ports also map to thier network
+ elif "network" in parentGroup:
+ network_ports[cellId] = cell.getAttribute("parent") # maps port ID to net ID
+
+ return connections, networks, hosts, interfaces, network_ports
+
+ # serialize and deserialize xml from mxGraph
+ def parseXml(self, xmlString):
+ networks = {} # maps net name to network object
+ hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks
+ interfaces = {} # maps id -> interface
+ untagged_ifaces = set() # used to check vlan config
+ network_names = set() # used to check network names
+ xml_connections, xml_nets, xml_hosts, xml_ifaces, xml_ports = self.decomposeXml(xmlString)
+
+ # parse Hosts
+ for cellId, cell in xml_hosts.items():
+ cell_json_str = cell.getAttribute("value")
+ cell_json = json.loads(cell_json_str)
+ host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']}
+ hosts[cellId] = host
+
+ # parse networks
+ for cellId, cell in xml_nets.items():
+ escaped_json_str = cell.getAttribute("value")
+ json_str = escaped_json_str.replace('&quot;', '"')
+ net_info = json.loads(json_str)
+ net_name = net_info['name']
+ public = net_info['public']
+ if net_name in network_names:
+ raise NetworkExistsException("Non unique network name found")
+ network = {"name": net_name, "public": public, "id": cellId}
+ networks[cellId] = network
+ network_names.add(net_name)
+
+ # parse interfaces
+ for cellId, cell in xml_ifaces.items():
+ parentId = cell.getAttribute('parent')
+ cell_json_str = cell.getAttribute("value")
+ cell_json = json.loads(cell_json_str)
+ iface = {"name": cellId, "connections": [], "profile_name": cell_json['name']}
+ hosts[parentId]['interfaces'].append(cellId)
+ interfaces[cellId] = iface
+
+ # parse connections
+ for cellId, cell in xml_connections.items():
+ escaped_json_str = cell.getAttribute("value")
+ json_str = escaped_json_str.replace('&quot;', '"')
+ attributes = json.loads(json_str)
+ tagged = attributes['tagged']
+ interface = None
+ network = None
+ src = cell.getAttribute("source")
+ tgt = cell.getAttribute("target")
+ if src in interfaces:
+ interface = interfaces[src]
+ network = networks[xml_ports[tgt]]
+ else:
+ interface = interfaces[tgt]
+ network = networks[xml_ports[src]]
+
+ if not tagged:
+ if interface['name'] in untagged_ifaces:
+ raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
+ untagged_ifaces.add(interface['name'])
+
+ # add connection to interface
+ interface['connections'].append({"tagged": tagged, "network": network['id']})
+
+ return hosts, interfaces, networks
class Resource_Meta_Info(WorkflowStep):
@@ -393,10 +401,10 @@ class Resource_Meta_Info(WorkflowStep):
tmp = tmp[:60] + "..."
confirm_info["description"] = tmp
self.repo_put(self.repo.CONFIRMATION, confirm)
- self.metastep.set_valid("Step Completed")
+ self.set_valid("Step Completed")
else:
- self.metastep.set_invalid("Please correct the fields highlighted in red to continue")
+ self.set_invalid("Please correct the fields highlighted in red to continue")
pass
return self.render(request)
diff --git a/dashboard/src/workflow/snapshot_workflow.py b/dashboard/src/workflow/snapshot_workflow.py
index 002aee5..5414784 100644
--- a/dashboard/src/workflow/snapshot_workflow.py
+++ b/dashboard/src/workflow/snapshot_workflow.py
@@ -14,7 +14,7 @@ import json
from booking.models import Booking
from resource_inventory.models import Host, Image
from workflow.models import WorkflowStep
-from workflow.forms import SnapshotMetaForm, SnapshotHostSelectForm
+from workflow.forms import BasicMetaForm, SnapshotHostSelectForm
class Select_Host_Step(WorkflowStep):
@@ -52,11 +52,11 @@ class Select_Host_Step(WorkflowStep):
def post_render(self, request):
host_data = request.POST.get("host")
if not host_data:
- self.metastep.set_invalid("Please select a host")
+ self.set_invalid("Please select a host")
return self.render(request)
host = json.loads(host_data)
if 'name' not in host or 'booking' not in host:
- self.metastep.set_invalid("Invalid host selected")
+ self.set_invalid("Invalid host selected")
return self.render(request)
name = host['name']
booking_id = host['booking']
@@ -75,7 +75,7 @@ class Select_Host_Step(WorkflowStep):
snap_confirm['host'] = name
confirm['snapshot'] = snap_confirm
self.repo_put(self.repo.CONFIRMATION, confirm)
- self.metastep.set_valid("Success")
+ self.set_valid("Success")
return self.render(request)
@@ -91,14 +91,14 @@ class Image_Meta_Step(WorkflowStep):
desc = self.repo_get(self.repo.SNAPSHOT_DESC, False)
form = None
if name and desc:
- form = SnapshotMetaForm(initial={"name": name, "description": desc})
+ form = BasicMetaForm(initial={"name": name, "description": desc})
else:
- form = SnapshotMetaForm()
+ form = BasicMetaForm()
context['form'] = form
return context
def post_render(self, request):
- form = SnapshotMetaForm(request.POST)
+ form = BasicMetaForm(request.POST)
if form.is_valid():
name = form.cleaned_data['name']
self.repo_put(self.repo.SNAPSHOT_NAME, name)
@@ -112,8 +112,8 @@ class Image_Meta_Step(WorkflowStep):
confirm['snapshot'] = snap_confirm
self.repo_put(self.repo.CONFIRMATION, confirm)
- self.metastep.set_valid("Success")
+ self.set_valid("Success")
else:
- self.metastep.set_invalid("Please Fill out the Form")
+ self.set_invalid("Please Fill out the Form")
return self.render(request)
diff --git a/dashboard/src/workflow/sw_bundle_workflow.py b/dashboard/src/workflow/sw_bundle_workflow.py
index 80d1b3d..0c558fc 100644
--- a/dashboard/src/workflow/sw_bundle_workflow.py
+++ b/dashboard/src/workflow/sw_bundle_workflow.py
@@ -11,26 +11,13 @@
from django.forms import formset_factory
from workflow.models import WorkflowStep
-from workflow.forms import SoftwareConfigurationForm, HostSoftwareDefinitionForm
-from workflow.booking_workflow import Resource_Select
-from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration, Installer, OPNFVConfig
+from workflow.forms import BasicMetaForm, HostSoftwareDefinitionForm
+from workflow.booking_workflow import Abstract_Resource_Select
+from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration
-# resource selection step is reused from Booking workflow
-class SWConf_Resource_Select(Resource_Select):
- def __init__(self, *args, **kwargs):
- super(SWConf_Resource_Select, self).__init__(*args, **kwargs)
- self.repo_key = self.repo.SELECTED_GRESOURCE_BUNDLE
- self.confirm_key = "configuration"
-
- def post_render(self, request):
- response = super(SWConf_Resource_Select, self).post_render(request)
- models = self.repo_get(self.repo.CONFIG_MODELS, {})
- bundle = models.get("bundle", ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER)))
- bundle.bundle = self.repo_get(self.repo_key) # super put grb here
- models['bundle'] = bundle
- self.repo_put(self.repo.CONFIG_MODELS, models)
- return response
+class SWConf_Resource_Select(Abstract_Resource_Select):
+ workflow_type = "configuration"
class Define_Software(WorkflowStep):
@@ -39,48 +26,57 @@ class Define_Software(WorkflowStep):
description = "Choose the opnfv and image of your machines"
short_title = "host config"
- def create_hostformset(self, hostlist):
+ def build_filter_data(self, hosts_data):
+ """
+ returns a 2D array of images to exclude
+ based on the ordering of the passed
+ hosts_data
+ """
+ filter_data = []
+ user = self.repo_get(self.repo.SESSION_USER)
+ lab = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE).lab
+ for i, host_data in enumerate(hosts_data):
+ host = GenericHost.objects.get(pk=host_data['host_id'])
+ wrong_owner = Image.objects.exclude(owner=user).exclude(public=True)
+ wrong_host = Image.objects.exclude(host_type=host.profile)
+ wrong_lab = Image.objects.exclude(from_lab=lab)
+ excluded_images = wrong_owner | wrong_host | wrong_lab
+ filter_data.append([])
+ for image in excluded_images:
+ filter_data[i].append(image.pk)
+ return filter_data
+
+ def create_hostformset(self, hostlist, data=None):
hosts_initial = []
host_configs = self.repo_get(self.repo.CONFIG_MODELS, {}).get("host_configs", False)
if host_configs:
for config in host_configs:
- host_initial = {'host_id': config.host.id, 'host_name': config.host.resource.name}
- host_initial['role'] = config.opnfvRole
- host_initial['image'] = config.image
- hosts_initial.append(host_initial)
-
+ hosts_initial.append({
+ 'host_id': config.host.id,
+ 'host_name': config.host.resource.name,
+ 'headnode': config.is_head_node,
+ 'image': config.image
+ })
else:
for host in hostlist:
- host_initial = {'host_id': host.id, 'host_name': host.resource.name}
-
- hosts_initial.append(host_initial)
+ hosts_initial.append({
+ 'host_id': host.id,
+ 'host_name': host.resource.name
+ })
HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0)
- host_formset = HostFormset(initial=hosts_initial)
+ filter_data = self.build_filter_data(hosts_initial)
- filter_data = {}
- user = self.repo_get(self.repo.SESSION_USER)
- i = 0
- for host_data in hosts_initial:
- host_profile = None
- try:
- host = GenericHost.objects.get(pk=host_data['host_id'])
- host_profile = host.profile
- except Exception:
- for host in hostlist:
- if host.resource.name == host_data['host_name']:
- host_profile = host.profile
- break
- excluded_images = Image.objects.exclude(owner=user).exclude(public=True)
- excluded_images = excluded_images | Image.objects.exclude(host_type=host.profile)
- lab = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE).lab
- excluded_images = excluded_images | Image.objects.exclude(from_lab=lab)
- filter_data["id_form-" + str(i) + "-image"] = []
- for image in excluded_images:
- filter_data["id_form-" + str(i) + "-image"].append(image.name)
- i += 1
+ class SpecialHostFormset(HostFormset):
+ def get_form_kwargs(self, index):
+ kwargs = super(SpecialHostFormset, self).get_form_kwargs(index)
+ if index is not None:
+ kwargs['imageQS'] = Image.objects.exclude(pk__in=filter_data[index])
+ return kwargs
- return host_formset, filter_data
+ if data:
+ return SpecialHostFormset(data, initial=hosts_initial)
+ return SpecialHostFormset(initial=hosts_initial)
def get_host_list(self, grb=None):
if grb is None:
@@ -99,12 +95,12 @@ class Define_Software(WorkflowStep):
if grb:
context["grb"] = grb
- formset, filter_data = self.create_hostformset(self.get_host_list(grb))
+ formset = self.create_hostformset(self.get_host_list(grb))
context["formset"] = formset
- context["filter_data"] = filter_data
+ context['headnode'] = self.repo_get(self.repo.CONFIG_MODELS, {}).get("headnode_index", 1)
else:
context["error"] = "Please select a resource first"
- self.metastep.set_invalid("Step requires information that is not yet provided by previous step")
+ self.set_invalid("Step requires information that is not yet provided by previous step")
return context
@@ -115,47 +111,35 @@ class Define_Software(WorkflowStep):
confirm = self.repo_get(self.repo.CONFIRMATION, {})
- HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0)
- formset = HostFormset(request.POST)
hosts = self.get_host_list()
- has_jumphost = False
+ models['headnode_index'] = request.POST.get("headnode", 1)
+ formset = self.create_hostformset(hosts, data=request.POST)
+ has_headnode = False
if formset.is_valid():
models['host_configs'] = []
- i = 0
confirm_hosts = []
- for form in formset:
+ for i, form in enumerate(formset):
host = hosts[i]
- i += 1
image = form.cleaned_data['image']
- # checks image compatability
- grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
- lab = None
- if grb:
- lab = grb.lab
- try:
- owner = self.repo_get(self.repo.SESSION_USER)
- q = Image.objects.filter(owner=owner) | Image.objects.filter(public=True)
- q.filter(host_type=host.profile)
- q.filter(from_lab=lab)
- q.get(id=image.id) # will throw exception if image is not in q
- except:
- self.metastep.set_invalid("Image " + image.name + " is not compatible with host " + host.resource.name)
- role = form.cleaned_data['role']
- if "jumphost" in role.name.lower():
- has_jumphost = True
+ headnode = form.cleaned_data['headnode']
+ if headnode:
+ has_headnode = True
bundle = models['bundle']
hostConfig = HostConfiguration(
host=host,
image=image,
bundle=bundle,
- opnfvRole=role
+ is_head_node=headnode
)
models['host_configs'].append(hostConfig)
- confirm_host = {"name": host.resource.name, "image": image.name, "role": role.name}
- confirm_hosts.append(confirm_host)
-
- if not has_jumphost:
- self.metastep.set_invalid('Must have at least one "Jumphost" per POD')
+ confirm_hosts.append({
+ "name": host.resource.name,
+ "image": image.name,
+ "headnode": headnode
+ })
+
+ if not has_headnode:
+ self.set_invalid('Must have one "Headnode" per POD')
return self.render(request)
self.repo_put(self.repo.CONFIG_MODELS, models)
@@ -163,17 +147,15 @@ class Define_Software(WorkflowStep):
confirm['configuration'] = {}
confirm['configuration']['hosts'] = confirm_hosts
self.repo_put(self.repo.CONFIRMATION, confirm)
- self.metastep.set_valid("Completed")
+ self.set_valid("Completed")
else:
- self.metastep.set_invalid("Please complete all fields")
+ self.set_invalid("Please complete all fields")
return self.render(request)
class Config_Software(WorkflowStep):
template = 'config_bundle/steps/config_software.html'
- form = SoftwareConfigurationForm
- context = {'workspace_form': form}
title = "Other Info"
description = "Give your software config a name, description, and other stuff"
short_title = "config info"
@@ -187,58 +169,30 @@ class Config_Software(WorkflowStep):
if bundle:
initial['name'] = bundle.name
initial['description'] = bundle.description
- opnfv = models.get("opnfv", False)
- if opnfv:
- initial['installer'] = opnfv.installer
- initial['scenario'] = opnfv.scenario
- else:
- initial['opnfv'] = False
- supported = {}
- for installer in Installer.objects.all():
- supported[str(installer)] = []
- for scenario in installer.sup_scenarios.all():
- supported[str(installer)].append(str(scenario))
-
- context["form"] = SoftwareConfigurationForm(initial=initial)
- context['supported'] = supported
-
+ context["form"] = BasicMetaForm(initial=initial)
return context
def post_render(self, request):
- try:
- models = self.repo_get(self.repo.CONFIG_MODELS, {})
- if "bundle" not in models:
- models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER))
+ models = self.repo_get(self.repo.CONFIG_MODELS, {})
+ if "bundle" not in models:
+ models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER))
- confirm = self.repo_get(self.repo.CONFIRMATION, {})
- if "configuration" not in confirm:
- confirm['configuration'] = {}
+ confirm = self.repo_get(self.repo.CONFIRMATION, {})
+ if "configuration" not in confirm:
+ confirm['configuration'] = {}
- form = self.form(request.POST)
- if form.is_valid():
- models['bundle'].name = form.cleaned_data['name']
- models['bundle'].description = form.cleaned_data['description']
- if form.cleaned_data['opnfv']:
- installer = form.cleaned_data['installer']
- scenario = form.cleaned_data['scenario']
- opnfv = OPNFVConfig(
- bundle=models['bundle'],
- installer=installer,
- scenario=scenario
- )
- models['opnfv'] = opnfv
- confirm['configuration']['installer'] = form.cleaned_data['installer'].name
- confirm['configuration']['scenario'] = form.cleaned_data['scenario'].name
-
- confirm['configuration']['name'] = form.cleaned_data['name']
- confirm['configuration']['description'] = form.cleaned_data['description']
- self.metastep.set_valid("Complete")
- else:
- self.metastep.set_invalid("Please correct the errors shown below")
+ form = BasicMetaForm(request.POST)
+ if form.is_valid():
+ models['bundle'].name = form.cleaned_data['name']
+ models['bundle'].description = form.cleaned_data['description']
- self.repo_put(self.repo.CONFIG_MODELS, models)
- self.repo_put(self.repo.CONFIRMATION, confirm)
+ confirm['configuration']['name'] = form.cleaned_data['name']
+ confirm['configuration']['description'] = form.cleaned_data['description']
+ self.set_valid("Complete")
+ else:
+ self.set_invalid("Please correct the errors shown below")
+
+ self.repo_put(self.repo.CONFIG_MODELS, models)
+ self.repo_put(self.repo.CONFIRMATION, confirm)
- except Exception:
- pass
return self.render(request)
diff --git a/dashboard/src/workflow/urls.py b/dashboard/src/workflow/urls.py
index b131d84..5a97904 100644
--- a/dashboard/src/workflow/urls.py
+++ b/dashboard/src/workflow/urls.py
@@ -14,7 +14,7 @@ from django.conf import settings
from workflow.views import step_view, delete_session, manager_view, viewport_view
from workflow.models import Repository
from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info
-from workflow.booking_workflow import SWConfig_Select, Resource_Select, Booking_Meta
+from workflow.booking_workflow import SWConfig_Select, Booking_Resource_Select, Booking_Meta
app_name = 'workflow'
urlpatterns = [
@@ -31,4 +31,4 @@ if settings.TESTING:
urlpatterns.append(url(r'^workflow/step/resource_meta$', Resource_Meta_Info("", Repository()).test_render))
urlpatterns.append(url(r'^workflow/step/booking_meta$', Booking_Meta("", Repository()).test_render))
urlpatterns.append(url(r'^workflow/step/software_select$', SWConfig_Select("", Repository()).test_render))
- urlpatterns.append(url(r'^workflow/step/resource_select$', Resource_Select("", Repository()).test_render))
+ urlpatterns.append(url(r'^workflow/step/resource_select$', Booking_Resource_Select("", Repository()).test_render))
diff --git a/dashboard/src/workflow/views.py b/dashboard/src/workflow/views.py
index 6d59b1c..7ed9031 100644
--- a/dashboard/src/workflow/views.py
+++ b/dashboard/src/workflow/views.py
@@ -8,12 +8,14 @@
##############################################################################
-from django.http import HttpResponse, HttpResponseGone
+from django.http import HttpResponseGone, JsonResponse
from django.shortcuts import render
+from django.urls import reverse
import uuid
from workflow.workflow_manager import ManagerTracker, SessionManager
+from booking.models import Booking
import logging
logger = logging.getLogger(__name__)
@@ -29,23 +31,33 @@ def attempt_auth(request):
return None
+def get_redirect_response(result):
+ if not result:
+ return {}
+
+ # need to get type of result, and switch on the type
+ # since has_result, result must be populated with a valid object
+ if isinstance(result, Booking):
+ return {
+ 'redir_url': reverse('booking:booking_detail', kwargs={'booking_id': result.id})
+ }
+ else:
+ return {}
+
+
def delete_session(request):
manager = attempt_auth(request)
if not manager:
return HttpResponseGone("No session found that relates to current request")
- if manager.pop_workflow():
- return HttpResponse('')
- else:
- del ManagerTracker.managers[request.session['manager_session']]
- return render(request, 'workflow/exit_redirect.html')
+ not_last_workflow, result = manager.pop_workflow()
- try:
+ if not_last_workflow: # this was not the last workflow, so don't redirect away
+ return JsonResponse({})
+ else:
del ManagerTracker.managers[request.session['manager_session']]
- return HttpResponse('')
- except Exception:
- return None
+ return JsonResponse(get_redirect_response(result))
def step_view(request):
@@ -54,7 +66,12 @@ def step_view(request):
# no manager found, redirect to "lost" page
return no_workflow(request)
if request.GET.get('step') is not None:
- manager.goto(int(request.GET.get('step')))
+ if request.GET.get('step') == 'next':
+ manager.go_next()
+ elif request.GET.get('step') == 'prev':
+ manager.go_prev()
+ else:
+ raise Exception("requested action for new step had malformed contents: " + request.GET.get('step'))
return manager.render(request)
diff --git a/dashboard/src/workflow/workflow_factory.py b/dashboard/src/workflow/workflow_factory.py
index 1f4a28a..03c8126 100644
--- a/dashboard/src/workflow/workflow_factory.py
+++ b/dashboard/src/workflow/workflow_factory.py
@@ -8,11 +8,12 @@
##############################################################################
-from workflow.booking_workflow import Booking_Resource_Select, SWConfig_Select, Booking_Meta
+from workflow.booking_workflow import Booking_Resource_Select, SWConfig_Select, Booking_Meta, OPNFV_Select
from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info
from workflow.sw_bundle_workflow import Config_Software, Define_Software, SWConf_Resource_Select
from workflow.snapshot_workflow import Select_Host_Step, Image_Meta_Step
-from workflow.models import Repository, Confirmation_Step
+from workflow.opnfv_workflow import Pick_Installer, Assign_Network_Roles, Assign_Host_Roles, OPNFV_Resource_Select, MetaInfo
+from workflow.models import Confirmation_Step
import uuid
@@ -20,21 +21,6 @@ import logging
logger = logging.getLogger(__name__)
-class BookingMetaWorkflow(object):
- workflow_type = 0
- color = "#0099ff"
- is_child = False
-
-
-class ResourceMetaWorkflow(object):
- workflow_type = 1
- color = "#ff6600"
-
-
-class ConfigMetaWorkflow(object):
- workflow_type = 2
- color = "#00ffcc"
-
class MetaStep(object):
UNTOUCHED = 0
@@ -53,6 +39,7 @@ class MetaStep(object):
self.short_title = "error"
self.skip_step = 0
self.valid = 0
+ self.hidden = False
self.message = ""
self.id = uuid.uuid4()
@@ -76,18 +63,20 @@ class MetaStep(object):
def __ne__(self, other):
return self.id.int != other.id.int
+
class Workflow(object):
- def __init__(self, steps, metasteps, repository):
+ def __init__(self, steps, repository):
self.repository = repository
self.steps = steps
- self.metasteps = metasteps
self.active_index = 0
+
class WorkflowFactory():
booking_steps = [
Booking_Resource_Select,
SWConfig_Select,
Booking_Meta,
+ OPNFV_Select,
]
resource_steps = [
@@ -107,27 +96,31 @@ class WorkflowFactory():
Image_Meta_Step,
]
+ opnfv_steps = [
+ OPNFV_Resource_Select,
+ Pick_Installer,
+ Assign_Network_Roles,
+ Assign_Host_Roles,
+ MetaInfo
+ ]
+
def conjure(self, workflow_type=None, repo=None):
workflow_types = [
self.booking_steps,
self.resource_steps,
self.config_steps,
self.snapshot_steps,
+ self.opnfv_steps,
]
steps = self.make_steps(workflow_types[workflow_type], repository=repo)
- meta_steps = self.metaize(steps=steps, wf_type=workflow_type)
- return steps, meta_steps
+ return steps
def create_workflow(self, workflow_type=None, repo=None):
- steps, meta_steps = self.conjure(workflow_type, repo)
+ steps = self.conjure(workflow_type, repo)
c_step = self.make_step(Confirmation_Step, repo)
- metaconfirm = MetaStep()
- metaconfirm.short_title = "confirm"
- metaconfirm.index = len(steps)
steps.append(c_step)
- meta_steps.append(metaconfirm)
- return Workflow(steps, meta_steps, repo)
+ return Workflow(steps, repo)
def make_steps(self, step_types, repository):
steps = []
@@ -139,13 +132,3 @@ class WorkflowFactory():
def make_step(self, step_type, repository):
iden = step_type.description + step_type.title + step_type.template
return step_type(iden, repository)
-
- def metaize(self, steps, wf_type):
- meta_dict = []
- for step in steps:
- meta_step = MetaStep()
- meta_step.short_title = step.short_title
- meta_dict.append(meta_step)
- step.metastep = meta_step
-
- return meta_dict
diff --git a/dashboard/src/workflow/workflow_manager.py b/dashboard/src/workflow/workflow_manager.py
index 1d672cf..80b8a67 100644
--- a/dashboard/src/workflow/workflow_manager.py
+++ b/dashboard/src/workflow/workflow_manager.py
@@ -10,10 +10,8 @@
from django.http import JsonResponse
-import random
-
from booking.models import Booking
-from workflow.workflow_factory import WorkflowFactory, MetaStep
+from workflow.workflow_factory import WorkflowFactory
from workflow.models import Repository
from resource_inventory.models import (
GenericResourceBundle,
@@ -37,6 +35,16 @@ class SessionManager():
self.factory = WorkflowFactory()
+ def set_step_statuses(self, superclass_type, desired_enabled=True):
+ workflow = self.active_workflow()
+ steps = workflow.steps
+ for step in steps:
+ if isinstance(step, superclass_type):
+ if desired_enabled:
+ step.enable()
+ else:
+ step.disable()
+
def add_workflow(self, workflow_type=None, target_id=None, **kwargs):
if target_id is not None:
self.prefill_repo(target_id, workflow_type)
@@ -47,30 +55,36 @@ class SessionManager():
repo.set_defaults(defaults)
repo.el[repo.HAS_RESULT] = False
repo.el[repo.SESSION_USER] = self.owner
- self.workflows.append(self.factory.create_workflow(workflow_type=workflow_type, repo = repo))
+ repo.el[repo.SESSION_MANAGER] = self
+ self.workflows.append(
+ self.factory.create_workflow(
+ workflow_type=workflow_type,
+ repo=repo
+ )
+ )
def pop_workflow(self):
- if( len(self.workflows) <= 1 ):
- return False
-
- if self.workflows[-1].repository.el[self.workflows[-1].repository.HAS_RESULT]:
- key = self.workflows[-1].repository.el[self.workflows[-1].repository.RESULT_KEY]
- result = self.workflows[-1].repository.el[self.workflows[-1].repository.RESULT]
- self.workflows[-2].repository.el[key] = result
- self.workflows.pop()
- return True
+ multiple_wfs = len(self.workflows) > 1
+ if multiple_wfs:
+ if self.workflows[-1].repository.el[Repository.RESULT]: # move result
+ key = self.workflows[-1].repository.el[Repository.RESULT_KEY]
+ result = self.workflows[-1].repository.el[Repository.RESULT]
+ self.workflows[-2].repository.el[key] = result
+ self.workflows.pop()
+ current_repo = self.workflows[-1].repository
+ return (multiple_wfs, current_repo.el[current_repo.RESULT])
def status(self, request):
try:
- meta_steps = []
- for step in self.active_workflow().metasteps:
- meta_steps.append(step.to_json())
+ meta_json = []
+ for step in self.active_workflow().steps:
+ meta_json.append(step.to_json())
responsejson = {}
- responsejson["steps"] = meta_steps
+ responsejson["steps"] = meta_json
responsejson["active"] = self.active_workflow().repository.el['active_step']
responsejson["workflow_count"] = len(self.workflows)
return JsonResponse(responsejson, safe=False)
- except Exception as e:
+ except Exception:
pass
def render(self, request, **kwargs):
@@ -83,10 +97,29 @@ class SessionManager():
def post_render(self, request):
return self.active_workflow().steps[self.active_workflow().active_index].post_render(request)
- def goto(self, num, **kwargs):
- self.active_workflow().repository.el['active_step'] = int(num)
- self.active_workflow().active_index = int(num)
- # TODO: change to include some checking
+ def get_active_step(self):
+ return self.active_workflow().steps[self.active_workflow().active_index]
+
+ def go_next(self, **kwargs):
+ # need to verify current step is valid to allow this
+ if self.get_active_step().valid < 200:
+ return
+ next_step = self.active_workflow().active_index + 1
+ if next_step >= len(self.active_workflow().steps):
+ raise Exception("Out of bounds request for step")
+ while not self.active_workflow().steps[next_step].enabled:
+ next_step += 1
+ self.active_workflow().repository.el['active_step'] = next_step
+ self.active_workflow().active_index = next_step
+
+ def go_prev(self, **kwargs):
+ prev_step = self.active_workflow().active_index - 1
+ if prev_step < 0:
+ raise Exception("Out of bounds request for step")
+ while not self.active_workflow().steps[prev_step].enabled:
+ prev_step -= 1
+ self.active_workflow().repository.el['active_step'] = prev_step
+ self.active_workflow().active_index = prev_step
def prefill_repo(self, target_id, workflow_type):
self.repository.el[self.repository.EDIT] = True
diff --git a/dashboard/test.sh b/dashboard/test.sh
index 7931cf0..0fbfd0e 100755
--- a/dashboard/test.sh
+++ b/dashboard/test.sh
@@ -13,4 +13,4 @@ find . -type f -name "*.py" -not -name "manage.py" | xargs flake8 --count --igno
# this file should be executed from the dir it is in
-docker exec -it dg01 python manage.py test -t ../src/
+docker exec -it dg01 python manage.py test