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.py10
-rw-r--r--dashboard/src/account/tests/test_general.py2
-rw-r--r--dashboard/src/account/urls.py19
-rw-r--r--dashboard/src/account/views.py100
-rw-r--r--dashboard/src/api/admin.py2
-rw-r--r--dashboard/src/api/migrations/0003_auto_20190102_1956.py18
-rw-r--r--dashboard/src/api/migrations/0004_snapshotconfig_snapshotrelation.py42
-rw-r--r--dashboard/src/api/migrations/0005_snapshotconfig_delta.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.py342
-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.py104
-rw-r--r--dashboard/src/booking/lib.py36
-rw-r--r--dashboard/src/booking/migrations/0002_booking_pdf.py18
-rw-r--r--dashboard/src/booking/migrations/0003_auto_20190115_1733.py30
-rw-r--r--dashboard/src/booking/migrations/0004_auto_20190124_1700.py20
-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.py33
-rw-r--r--dashboard/src/booking/quick_deployer.py355
-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/urls.py9
-rw-r--r--dashboard/src/booking/views.py114
-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.py18
-rw-r--r--dashboard/src/dashboard/testing_utils.py396
-rw-r--r--dashboard/src/dashboard/views.py4
-rw-r--r--dashboard/src/notifier/manager.py8
-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/0005_image_os.py19
-rw-r--r--dashboard/src/resource_inventory/migrations/0006_auto_20190124_1700.py76
-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.py137
-rw-r--r--dashboard/src/resource_inventory/pdf_templater.py193
-rw-r--r--dashboard/src/resource_inventory/resource_manager.py203
-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.css39
-rw-r--r--dashboard/src/static/css/graph_common.css32
-rw-r--r--dashboard/src/static/js/dashboard.js1134
-rw-r--r--dashboard/src/templates/account/booking_list.html164
-rw-r--r--dashboard/src/templates/account/configuration_list.html88
-rw-r--r--dashboard/src/templates/account/details.html8
-rw-r--r--dashboard/src/templates/account/image_list.html130
-rw-r--r--dashboard/src/templates/account/resource_list.html132
-rw-r--r--dashboard/src/templates/account/userprofile_update_form.html19
-rw-r--r--dashboard/src/templates/base.html302
-rw-r--r--dashboard/src/templates/booking/booking_calendar.html4
-rw-r--r--dashboard/src/templates/booking/booking_delete.html2
-rw-r--r--dashboard/src/templates/booking/booking_detail.html174
-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.html187
-rw-r--r--dashboard/src/templates/booking/stats.html29
-rw-r--r--dashboard/src/templates/booking/steps/booking_confirm.html2
-rw-r--r--dashboard/src/templates/booking/steps/booking_meta.html26
-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.html148
-rw-r--r--dashboard/src/templates/dashboard/multiple_select_filter_widget.html434
-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/resource_detail.html6
-rw-r--r--dashboard/src/templates/dashboard/searchable_select_multiple.html461
-rw-r--r--dashboard/src/templates/dashboard/table.html9
-rw-r--r--dashboard/src/templates/layout.html11
-rw-r--r--dashboard/src/templates/notifier/email_fulfilled.txt6
-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.html32
-rw-r--r--dashboard/src/templates/resource/steps/pod_definition.html699
-rw-r--r--dashboard/src/templates/snapshot_workflow/steps/meta.html21
-rw-r--r--dashboard/src/templates/snapshot_workflow/steps/select_host.html67
-rw-r--r--dashboard/src/templates/workflow/confirm.html50
-rw-r--r--dashboard/src/templates/workflow/no_workflow.html10
-rw-r--r--dashboard/src/templates/workflow/resource_select.html2
-rw-r--r--dashboard/src/templates/workflow/viewport-base.html583
-rw-r--r--dashboard/src/templates/workflow/viewport-element.html2
-rw-r--r--dashboard/src/workflow/booking_workflow.py313
-rw-r--r--dashboard/src/workflow/forms.py541
-rw-r--r--dashboard/src/workflow/models.py457
-rw-r--r--dashboard/src/workflow/opnfv_workflow.py299
-rw-r--r--dashboard/src/workflow/resource_bundle_workflow.py387
-rw-r--r--dashboard/src/workflow/snapshot_workflow.py27
-rw-r--r--dashboard/src/workflow/sw_bundle_workflow.py224
-rw-r--r--dashboard/src/workflow/urls.py4
-rw-r--r--dashboard/src/workflow/views.py43
-rw-r--r--dashboard/src/workflow/workflow_factory.py78
-rw-r--r--dashboard/src/workflow/workflow_manager.py190
-rwxr-xr-xdashboard/test.sh2
-rw-r--r--dashboard/tox.ini21
134 files changed, 9054 insertions, 4122 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 bfeead0..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
@@ -94,7 +94,7 @@ class VlanManager(models.Model):
vlan_master_list = json.loads(self.vlans)
try:
iter(vlans)
- except:
+ except Exception:
vlans = [vlans]
for vlan in vlans:
@@ -112,7 +112,7 @@ class VlanManager(models.Model):
try:
iter(vlans)
- except:
+ except Exception:
vlans = [vlans]
for vlan in vlans:
@@ -125,13 +125,13 @@ class VlanManager(models.Model):
try:
iter(vlans)
- except:
+ except Exception:
vlans = [vlans]
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/urls.py b/dashboard/src/account/urls.py
index 85f0f1a..8aad80c 100644
--- a/dashboard/src/account/urls.py
+++ b/dashboard/src/account/urls.py
@@ -25,6 +25,7 @@ Including another URLconf
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url
+from django.urls import path
from account.views import (
AccountSettingsView,
@@ -36,7 +37,11 @@ from account.views import (
account_booking_view,
account_images_view,
account_configuration_view,
- account_detail_view
+ account_detail_view,
+ resource_delete_view,
+ booking_cancel_view,
+ image_delete_view,
+ configuration_delete_view
)
app_name = "account"
@@ -46,9 +51,13 @@ urlpatterns = [
url(r'^login/$', JiraLoginView.as_view(), name='login'),
url(r'^logout/$', JiraLogoutView.as_view(), name='logout'),
url(r'^users/$', UserListView.as_view(), name='users'),
- url(r'^my/resources', account_resource_view, name="my-resources"),
- url(r'^my/bookings', account_booking_view, name="my-bookings"),
- url(r'^my/images', account_images_view, name="my-images"),
- url(r'^my/configurations', account_configuration_view, name="my-configurations"),
+ url(r'^my/resources/$', account_resource_view, name="my-resources"),
+ path('my/resources/delete/<int:resource_id>', resource_delete_view),
+ url(r'^my/bookings/$', account_booking_view, name="my-bookings"),
+ path('my/bookings/cancel/<int:booking_id>', booking_cancel_view),
+ url(r'^my/images/$', account_images_view, name="my-images"),
+ path('my/images/delete/<int:image_id>', image_delete_view),
+ url(r'^my/configurations/$', account_configuration_view, name="my-configurations"),
+ path('my/configurations/delete/<int:config_id>', configuration_delete_view),
url(r'^my/$', account_detail_view, name="my-account"),
]
diff --git a/dashboard/src/account/views.py b/dashboard/src/account/views.py
index 09c5266..2b4eccb 100644
--- a/dashboard/src/account/views.py
+++ b/dashboard/src/account/views.py
@@ -14,12 +14,15 @@ 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
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.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.generic import RedirectView, TemplateView, UpdateView
from django.shortcuts import render
@@ -30,7 +33,7 @@ from account.forms import AccountSettingsForm
from account.jira_util import SignatureMethod_RSA_SHA1
from account.models import UserProfile
from booking.models import Booking
-from resource_inventory.models import GenericResourceBundle, ConfigBundle, Image
+from resource_inventory.models import GenericResourceBundle, ConfigBundle, Image, Host
@method_decorator(login_required, name='dispatch')
@@ -172,8 +175,22 @@ def account_resource_view(request):
if not request.user.is_authenticated:
return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
template = "account/resource_list.html"
- resources = list(GenericResourceBundle.objects.filter(owner=request.user))
- context = {"resources": resources, "title": "My Resources"}
+ resources = GenericResourceBundle.objects.filter(
+ owner=request.user).prefetch_related("configbundle_set")
+ mapping = {}
+ resource_list = []
+ booking_mapping = {}
+ for grb in resources:
+ resource_list.append(grb)
+ mapping[grb.id] = [{"id": x.id, "name": x.name} for x in grb.configbundle_set.all()]
+ if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists():
+ booking_mapping[grb.id] = "true"
+ context = {
+ "resources": resource_list,
+ "grb_mapping": mapping,
+ "booking_mapping": booking_mapping,
+ "title": "My Resources"
+ }
return render(request, template, context=context)
@@ -181,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))
- collab_bookings = list(request.user.collaborators.all())
- 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)
@@ -202,5 +227,66 @@ def account_images_view(request):
template = "account/image_list.html"
my_images = Image.objects.filter(owner=request.user)
public_images = Image.objects.filter(public=True)
- context = {"title": "Images", "images": my_images, "public_images": public_images}
+ used_images = {}
+ for image in my_images:
+ if Host.objects.filter(booked=True, config__image=image).exists():
+ used_images[image.id] = "true"
+ context = {
+ "title": "Images",
+ "images": my_images,
+ "public_images": public_images,
+ "used_images": used_images
+ }
return render(request, template, context=context)
+
+
+def resource_delete_view(request, resource_id=None):
+ if not request.user.is_authenticated:
+ return HttpResponse('no') # 403?
+ grb = get_object_or_404(GenericResourceBundle, pk=resource_id)
+ if not request.user.id == grb.owner.id:
+ return HttpResponse('no') # 403?
+ if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists():
+ return HttpResponse('no') # 403?
+ grb.delete()
+ return HttpResponse('')
+
+
+def configuration_delete_view(request, config_id=None):
+ if not request.user.is_authenticated:
+ return HttpResponse('no') # 403?
+ config = get_object_or_404(ConfigBundle, pk=config_id)
+ if not request.user.id == config.owner.id:
+ return HttpResponse('no') # 403?
+ if Booking.objects.filter(config_bundle=config, end__gt=timezone.now()).exists():
+ return HttpResponse('no')
+ config.delete()
+ return HttpResponse('')
+
+
+def booking_cancel_view(request, booking_id=None):
+ if not request.user.is_authenticated:
+ return HttpResponse('no') # 403?
+ booking = get_object_or_404(Booking, pk=booking_id)
+ if not request.user.id == booking.owner.id:
+ return HttpResponse('no') # 403?
+
+ if booking.end < timezone.now(): # booking already over
+ return HttpResponse('')
+
+ booking.end = timezone.now()
+ booking.save()
+ return HttpResponse('')
+
+
+def image_delete_view(request, image_id=None):
+ if not request.user.is_authenticated:
+ return HttpResponse('no') # 403?
+ image = get_object_or_404(Image, pk=image_id)
+ if image.public or image.owner.id != request.user.id:
+ return HttpResponse('no') # 403?
+ # check if used in booking
+ if Host.objects.filter(booked=True, config__image=image).exists():
+ return HttpResponse('no') # 403?
+ image.delete()
+ return HttpResponse('')
diff --git a/dashboard/src/api/admin.py b/dashboard/src/api/admin.py
index 3d32c78..8b2fcb3 100644
--- a/dashboard/src/api/admin.py
+++ b/dashboard/src/api/admin.py
@@ -17,6 +17,7 @@ from api.models import (
HardwareConfig,
NetworkConfig,
SoftwareConfig,
+ AccessConfig,
AccessRelation,
SoftwareRelation,
HostHardwareRelation,
@@ -33,6 +34,7 @@ admin.site.register(OpnfvApiConfig)
admin.site.register(HardwareConfig)
admin.site.register(NetworkConfig)
admin.site.register(SoftwareConfig)
+admin.site.register(AccessConfig)
admin.site.register(AccessRelation)
admin.site.register(SoftwareRelation)
admin.site.register(HostHardwareRelation)
diff --git a/dashboard/src/api/migrations/0003_auto_20190102_1956.py b/dashboard/src/api/migrations/0003_auto_20190102_1956.py
new file mode 100644
index 0000000..2ea5d70
--- /dev/null
+++ b/dashboard/src/api/migrations/0003_auto_20190102_1956.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2019-01-02 19:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0002_remove_job_delta'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='accessconfig',
+ name='delta',
+ field=models.TextField(default='{}'),
+ ),
+ ]
diff --git a/dashboard/src/api/migrations/0004_snapshotconfig_snapshotrelation.py b/dashboard/src/api/migrations/0004_snapshotconfig_snapshotrelation.py
new file mode 100644
index 0000000..62bc7af
--- /dev/null
+++ b/dashboard/src/api/migrations/0004_snapshotconfig_snapshotrelation.py
@@ -0,0 +1,42 @@
+# Generated by Django 2.1 on 2019-01-17 15:54
+
+import api.models
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0004_auto_20181017_1532'),
+ ('api', '0003_auto_20190102_1956'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SnapshotConfig',
+ fields=[
+ ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')),
+ ('image', models.IntegerField(null=True)),
+ ('dashboard_id', models.IntegerField()),
+ ('host', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.Host')),
+ ],
+ bases=('api.taskconfig',),
+ ),
+ migrations.CreateModel(
+ name='SnapshotRelation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('status', models.IntegerField(default=0)),
+ ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)),
+ ('lab_token', models.CharField(default='null', max_length=50)),
+ ('message', models.TextField(default='')),
+ ('config', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.SnapshotConfig')),
+ ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job')),
+ ('snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Image')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/dashboard/src/api/migrations/0005_snapshotconfig_delta.py b/dashboard/src/api/migrations/0005_snapshotconfig_delta.py
new file mode 100644
index 0000000..559af90
--- /dev/null
+++ b/dashboard/src/api/migrations/0005_snapshotconfig_delta.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2019-01-17 16:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0004_snapshotconfig_snapshotrelation'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='snapshotconfig',
+ name='delta',
+ field=models.TextField(default='{}'),
+ ),
+ ]
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 78ec920..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}
@@ -214,6 +278,10 @@ class Job(models.Model):
if 'network' not in d:
d['network'] = {}
d['network'][relation.task_id] = relation.config.to_dict()
+ for relation in SnapshotRelation.objects.filter(job=self):
+ if 'snapshot' not in d:
+ d['snapshot'] = {}
+ d['snapshot'][relation.task_id] = relation.config.to_dict()
j['payload'] = d
@@ -221,7 +289,13 @@ class Job(models.Model):
def get_tasklist(self, status="all"):
tasklist = []
- clist = [HostHardwareRelation, AccessRelation, HostNetworkRelation, SoftwareRelation]
+ clist = [
+ HostHardwareRelation,
+ AccessRelation,
+ HostNetworkRelation,
+ SoftwareRelation,
+ SnapshotRelation
+ ]
if status == "all":
for cls in clist:
tasklist += list(cls.objects.filter(job=self))
@@ -261,6 +335,10 @@ class Job(models.Model):
if 'network' not in d:
d['network'] = {}
d['network'][relation.task_id] = relation.config.get_delta()
+ for relation in SnapshotRelation.objects.filter(job=self).filter(status=status):
+ if 'snapshot' not in d:
+ d['snapshot'] = {}
+ d['snapshot'][relation.task_id] = relation.config.get_delta()
j['payload'] = d
return j
@@ -283,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
@@ -320,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)
@@ -343,14 +477,17 @@ class AccessConfig(TaskConfig):
user = models.ForeignKey(User, on_delete=models.CASCADE)
revoke = models.BooleanField(default=False)
context = models.TextField(default="")
- delta = models.TextField()
+ delta = models.TextField(default="{}")
def to_dict(self):
d = {}
d['access_type'] = self.access_type
d['user'] = self.user.id
d['revoke'] = self.revoke
- d['context'] = json.loads(self.context)
+ try:
+ d['context'] = json.loads(self.context)
+ except Exception:
+ pass
return d
def get_delta(self):
@@ -531,8 +668,64 @@ class NetworkConfig(TaskConfig):
self.delta = json.dumps(d)
+class SnapshotConfig(TaskConfig):
+
+ host = models.ForeignKey(Host, null=True, on_delete=models.DO_NOTHING)
+ image = models.IntegerField(null=True)
+ dashboard_id = models.IntegerField()
+ delta = models.TextField(default="{}")
+
+ def to_dict(self):
+ d = {}
+ if self.host:
+ d['host'] = self.host.labid
+ if self.image:
+ d['image'] = self.image
+ d['dashboard_id'] = self.dashboard_id
+ return d
+
+ def to_json(self):
+ return json.dumps(self.to_dict())
+
+ def get_delta(self):
+ if not self.delta:
+ self.delta = self.to_json()
+ self.save()
+
+ d = json.loads(self.delta)
+ return d
+
+ def clear_delta(self):
+ self.delta = json.dumps(self.to_dict())
+ self.save()
+
+ def set_host(self, host):
+ self.host = host
+ d = json.loads(self.delta)
+ d['host'] = host.labid
+ self.delta = json.dumps(d)
+
+ def set_image(self, image):
+ self.image = image
+ d = json.loads(self.delta)
+ d['image'] = self.image
+ self.delta = json.dumps(d)
+
+ def clear_image(self):
+ self.image = None
+ d = json.loads(self.delta)
+ d.pop("image", None)
+ self.delta = json.dumps(d)
+
+ def set_dashboard_id(self, dash):
+ self.dashboard_id = dash
+ d = json.loads(self.delta)
+ d['dashboard_id'] = self.dashboard_id
+ self.delta = json.dumps(d)
+
+
def get_task(task_id):
- for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]:
+ for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]:
try:
ret = taskclass.objects.get(task_id=task_id)
return ret
@@ -614,15 +807,72 @@ class HostNetworkRelation(TaskRelation):
return super(self.__class__, self).delete(*args, **kwargs)
+class SnapshotRelation(TaskRelation):
+ snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
+ config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE)
+
+ def type_str(self):
+ return "Snapshot Task"
+
+ def get_delta(self):
+ return self.config.to_dict()
+
+ def delete(self, *args, **kwargs):
+ self.config.delete()
+ return super(self.__class__, self).delete(*args, **kwargs)
+
+
class JobFactory(object):
@classmethod
+ def reimageHost(cls, new_image, booking, host):
+ """
+ This method will make all necessary changes to make a lab
+ reimage a host.
+ """
+ job = Job.objects.get(booking=booking)
+ # make hardware task new
+ hardware_relation = HostHardwareRelation.objects.get(host=host, job=job)
+ hardware_relation.config.set_image(new_image.lab_id)
+ hardware_relation.config.save()
+ hardware_relation.status = JobStatus.NEW
+
+ # re-apply networking after host is reset
+ net_relation = HostNetworkRelation.objects.get(host=host, job=job)
+ net_relation.status = JobStatus.NEW
+
+ # re-apply ssh access after host is reset
+ for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"):
+ relation.status = JobStatus.NEW
+ relation.save()
+
+ hardware_relation.save()
+ net_relation.save()
+
+ @classmethod
+ def makeSnapshotTask(cls, image, booking, host):
+ relation = SnapshotRelation()
+ job = Job.objects.get(booking=booking)
+ config = SnapshotConfig.objects.create(dashboard_id=image.id)
+
+ relation.job = job
+ relation.config = config
+ relation.config.save()
+ relation.config = relation.config
+ relation.snapshot = image
+ relation.save()
+
+ config.clear_delta()
+ config.set_host(host)
+ config.save()
+
+ @classmethod
def makeCompleteJob(cls, booking):
hosts = Host.objects.filter(bundle=booking.resource)
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,
@@ -633,7 +883,7 @@ class JobFactory(object):
job=job
)
cls.makeSoftware(
- hosts=hosts,
+ booking=booking,
job=job
)
all_users = list(booking.collaborators.all())
@@ -652,7 +902,7 @@ class JobFactory(object):
revoke=False,
job=job,
context={
- "key": user.userprofile.ssh_public_key.read(),
+ "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
"hosts": [host.labid for host in hosts]
}
)
@@ -665,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()
@@ -691,12 +941,12 @@ class JobFactory(object):
config = AccessConfig()
config.access_type = access_type
config.user = user
- if context:
- config.set_context(context)
config.save()
relation.config = config
relation.save()
config.clear_delta()
+ if context:
+ config.set_context(context)
config.set_access_type(access_type)
config.set_revoke(revoke)
config.set_user(user)
@@ -708,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()
@@ -724,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
new file mode 100644
index 0000000..df88cc6
--- /dev/null
+++ b/dashboard/src/booking/forms.py
@@ -0,0 +1,104 @@
+##############################################################################
+# 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 django.forms as forms
+from django.forms.widgets import NumberInput
+
+from workflow.forms import (
+ 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)
+ 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):
+ if "default_user" in kwargs:
+ default_user = kwargs.pop("default_user")
+ else:
+ default_user = "you"
+ self.default_user = default_user
+
+ super(QuickBookingForm, self).__init__(data=data, **kwargs)
+
+ 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)
+ self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**attrs))
+ self.fields['length'] = forms.IntegerField(
+ widget=NumberInput(
+ attrs={
+ "type": "range",
+ 'min': "1",
+ "max": "21",
+ "value": "1"
+ }
+ )
+ )
+
+ 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 HostReImageForm(forms.Form):
+
+ image_id = forms.IntegerField()
+ host_id = forms.IntegerField()
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/0002_booking_pdf.py b/dashboard/src/booking/migrations/0002_booking_pdf.py
new file mode 100644
index 0000000..53232c9
--- /dev/null
+++ b/dashboard/src/booking/migrations/0002_booking_pdf.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1 on 2018-11-09 16:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='booking',
+ name='pdf',
+ field=models.TextField(blank=True, default=''),
+ ),
+ ]
diff --git a/dashboard/src/booking/migrations/0003_auto_20190115_1733.py b/dashboard/src/booking/migrations/0003_auto_20190115_1733.py
new file mode 100644
index 0000000..70eecfe
--- /dev/null
+++ b/dashboard/src/booking/migrations/0003_auto_20190115_1733.py
@@ -0,0 +1,30 @@
+# Generated by Django 2.1 on 2019-01-15 17:33
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0002_booking_pdf'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='installer',
+ name='sup_scenarios',
+ ),
+ migrations.RemoveField(
+ model_name='opsys',
+ name='sup_installers',
+ ),
+ migrations.DeleteModel(
+ name='Installer',
+ ),
+ migrations.DeleteModel(
+ name='Opsys',
+ ),
+ migrations.DeleteModel(
+ name='Scenario',
+ ),
+ ]
diff --git a/dashboard/src/booking/migrations/0004_auto_20190124_1700.py b/dashboard/src/booking/migrations/0004_auto_20190124_1700.py
new file mode 100644
index 0000000..baa32d2
--- /dev/null
+++ b/dashboard/src/booking/migrations/0004_auto_20190124_1700.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1 on 2019-01-24 17:00
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0003_auto_20190115_1733'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='booking',
+ name='owner',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='owner', to=settings.AUTH_USER_MODEL),
+ ),
+ ]
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 d0c77b4..9836730 100644
--- a/dashboard/src/booking/models.py
+++ b/dashboard/src/booking/models.py
@@ -9,42 +9,16 @@
##############################################################################
-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
import resource_inventory.resource_manager
-class Scenario(models.Model):
- id = models.AutoField(primary_key=True)
- name = models.CharField(max_length=300)
-
- def __str__(self):
- return self.name
-
-
-class Installer(models.Model):
- id = models.AutoField(primary_key=True)
- name = models.CharField(max_length=30)
- sup_scenarios = models.ManyToManyField(Scenario, blank=True)
-
- def __str__(self):
- return self.name
-
-
-class Opsys(models.Model):
- id = models.AutoField(primary_key=True)
- name = models.CharField(max_length=100)
- sup_installers = models.ManyToManyField(Installer, blank=True)
-
- def __str__(self):
- return self.name
-
-
class Booking(models.Model):
id = models.AutoField(primary_key=True)
- owner = models.ForeignKey(User, models.CASCADE, related_name='owner')
+ owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='owner')
collaborators = models.ManyToManyField(User, related_name='collaborators')
start = models.DateTimeField()
end = models.DateTimeField()
@@ -55,8 +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
new file mode 100644
index 0000000..0e0cc5a
--- /dev/null
+++ b/dashboard/src/booking/quick_deployer.py
@@ -0,0 +1,355 @@
+##############################################################################
+# 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 json
+import uuid
+import re
+from django.db.models import Q
+from datetime import timedelta
+from django.utils import timezone
+from account.models import Lab
+
+from resource_inventory.models import (
+ Installer,
+ Image,
+ GenericResourceBundle,
+ ConfigBundle,
+ Host,
+ HostProfile,
+ HostConfiguration,
+ GenericResource,
+ GenericHost,
+ GenericInterface,
+ OPNFVRole,
+ 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,
+ BookingLengthException
+)
+from api.models import JobFactory
+
+
+# model validity exceptions
+class IncompatibleInstallerForOS(Exception):
+ pass
+
+
+class IncompatibleScenarioForInstaller(Exception):
+ pass
+
+
+class IncompatibleImageForHost(Exception):
+ pass
+
+
+class ImageOwnershipInvalid(Exception):
+ pass
+
+
+class ImageNotAvailableAtLab(Exception):
+ pass
+
+
+class LabDNE(Exception):
+ pass
+
+
+class HostProfileDNE(Exception):
+ pass
+
+
+class HostNotAvailable(Exception):
+ pass
+
+
+class NoLabSelectedError(Exception):
+ pass
+
+
+class OPNFVRoleDNE(Exception):
+ pass
+
+
+class NoRemainingPublicNetwork(Exception):
+ pass
+
+
+class BookingPermissionException(Exception):
+ pass
+
+
+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'])
+
+ host_dict = host_json['host']
+ for host_info in host_dict.values():
+ if host_info['selected']:
+ profile = HostProfile.objects.get(pk=host_info['id'])
+
+ 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 hostprofile not in available_host_types:
+ # TODO: handle deleting generic resource in this instance along with grb
+ raise HostNotAvailable('Requested host type is not available. Please try again later. Host availability can be viewed in the "Hosts" tab to the left.')
+
+ 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")
+
+ return True
+
+
+def generate_grb(owner, lab, common_id):
+ grbundle = GenericResourceBundle(owner=owner)
+ grbundle.lab = lab
+ grbundle.name = "grbundle for quick booking with uid " + common_id
+ grbundle.description = "grbundle created for quick-deploy booking"
+ grbundle.save()
+
+ 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 = generic_resource
+ ghost.profile = host_profile
+ ghost.save()
+
+ return ghost
+
+
+def generate_config_bundle(owner, common_id, grbundle):
+ cbundle = ConfigBundle()
+ 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()
+
+ 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
+
+
+def generate_hostconfig(generic_host, image, config_bundle):
+ hconf = HostConfiguration()
+ hconf.host = generic_host
+ hconf.image = image
+ hconf.bundle = config_bundle
+ hconf.is_head_node = True
+ hconf.save()
+
+ return hconf
+
+
+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_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.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()
+
+ # generate job
+ JobFactory.makeCompleteJob(booking)
+ NotificationHandler.notify_new_booking(booking)
+
+ return booking
+
+
+def drop_filter(user):
+ installer_filter = {}
+ for image in Image.objects.all():
+ installer_filter[image.id] = {}
+ for installer in image.os.sup_installers.all():
+ installer_filter[image.id][installer.id] = 1
+
+ scenario_filter = {}
+ for installer in Installer.objects.all():
+ scenario_filter[installer.id] = {}
+ for scenario in installer.sup_scenarios.all():
+ scenario_filter[installer.id][scenario.id] = 1
+
+ images = Image.objects.filter(Q(public=True) | Q(owner=user))
+ image_filter = {}
+ for image in images:
+ image_filter[image.id] = {}
+ image_filter[image.id]['lab'] = 'lab_' + str(image.from_lab.lab_user.id)
+ image_filter[image.id]['host_profile'] = 'host_' + str(image.host_type.id)
+ image_filter[image.id]['name'] = image.name
+
+ return {'installer_filter': json.dumps(installer_filter),
+ 'scenario_filter': json.dumps(scenario_filter),
+ 'image_filter': json.dumps(image_filter)}
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/urls.py b/dashboard/src/booking/urls.py
index 4d00b7f..310aaa7 100644
--- a/dashboard/src/booking/urls.py
+++ b/dashboard/src/booking/urls.py
@@ -32,7 +32,9 @@ from booking.views import (
bookingDelete,
BookingListView,
booking_stats_view,
- booking_stats_json
+ booking_stats_json,
+ quick_create,
+ booking_modify_image
)
app_name = "booking"
@@ -41,13 +43,12 @@ urlpatterns = [
url(r'^detail/(?P<booking_id>[0-9]+)/$', booking_detail_view, name='detail'),
url(r'^(?P<booking_id>[0-9]+)/$', booking_detail_view, name='booking_detail'),
-
url(r'^delete/$', BookingDeleteView.as_view(), name='delete_prefix'),
url(r'^delete/(?P<booking_id>[0-9]+)/$', BookingDeleteView.as_view(), name='delete'),
-
url(r'^delete/(?P<booking_id>[0-9]+)/confirm/$', bookingDelete, name='delete_booking'),
-
+ url(r'^modify/(?P<booking_id>[0-9]+)/image/$', booking_modify_image, name='modify_booking_image'),
url(r'^list/$', BookingListView.as_view(), name='list'),
url(r'^stats/$', booking_stats_view, name='stats'),
url(r'^stats/json$', booking_stats_json, name='stats_json'),
+ url(r'^quick/$', quick_create, name='quick_create'),
]
diff --git a/dashboard/src/booking/views.py b/dashboard/src/booking/views.py
index ab43519..bad7dc9 100644
--- a/dashboard/src/booking/views.py
+++ b/dashboard/src/booking/views.py
@@ -10,33 +10,67 @@
from django.contrib import messages
from django.shortcuts import get_object_or_404
-from django.http import JsonResponse
+from django.http import JsonResponse, HttpResponse
from django.utils import timezone
from django.views import View
from django.views.generic import TemplateView
from django.shortcuts import redirect, render
-import json
+from django.db.models import Q
+from django.urls import reverse
-from resource_inventory.models import ResourceBundle
+from resource_inventory.models import ResourceBundle, HostProfile, Image, Host
from resource_inventory.resource_manager import ResourceManager
-from booking.models import Booking, Installer, Opsys
+from account.models import Lab
+from booking.models import Booking
from booking.stats import StatisticsManager
+from booking.forms import HostReImageForm
+from api.models import JobFactory
+from workflow.views import login
+from booking.forms import QuickBookingForm
+from booking.quick_deployer import create_from_form, drop_filter
-def drop_filter(context):
- installer_filter = {}
- for os in Opsys.objects.all():
- installer_filter[os.id] = []
- for installer in os.sup_installers.all():
- installer_filter[os.id].append(installer.id)
+def quick_create_clear_fields(request):
+ request.session['quick_create_forminfo'] = None
- scenario_filter = {}
- for installer in Installer.objects.all():
- scenario_filter[installer.id] = []
- for scenario in installer.sup_scenarios.all():
- scenario_filter[installer.id].append(scenario.id)
- context.update({'installer_filter': json.dumps(installer_filter), 'scenario_filter': json.dumps(scenario_filter)})
+def quick_create(request):
+ if not request.user.is_authenticated:
+ return login(request)
+
+ if request.method == 'GET':
+ context = {}
+
+ r_manager = ResourceManager.getInstance()
+ profiles = {}
+ for lab in Lab.objects.all():
+ profiles[str(lab)] = r_manager.getAvailableHostTypes(lab)
+
+ context['lab_profile_map'] = profiles
+
+ context['form'] = QuickBookingForm(default_user=request.user.username, user=request.user)
+
+ context.update(drop_filter(request.user))
+
+ return render(request, 'booking/quick_deploy.html', context)
+ if request.method == 'POST':
+ form = QuickBookingForm(request.POST, user=request.user)
+ context = {}
+ context['lab_profile_map'] = {}
+ context['form'] = form
+
+ if form.is_valid():
+ try:
+ 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, 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)
class BookingView(TemplateView):
@@ -93,6 +127,19 @@ class ResourceBookingsJSON(View):
return JsonResponse({'bookings': list(bookings)})
+def build_image_mapping(lab, user):
+ mapping = {}
+ for profile in HostProfile.objects.filter(labs=lab):
+ images = Image.objects.filter(
+ from_lab=lab,
+ host_type=profile
+ ).filter(
+ Q(public=True) | Q(owner=user)
+ )
+ mapping[profile.name] = [{"name": image.name, "value": image.id} for image in images]
+ return mapping
+
+
def booking_detail_view(request, booking_id):
user = None
if request.user.is_authenticated:
@@ -106,15 +153,36 @@ def booking_detail_view(request, booking_id):
if user not in allowed_users:
return render(request, "dashboard/login.html", {'title': 'This page is private'})
+ context = {
+ 'title': 'Booking Details',
+ 'booking': booking,
+ 'pdf': booking.pdf,
+ 'user_id': user.id,
+ 'image_mapping': build_image_mapping(booking.lab, user)
+ }
+
return render(
request,
"booking/booking_detail.html",
- {
- 'title': 'Booking Details',
- 'booking': booking,
- 'pdf': ResourceManager().makePDF(booking.resource),
- 'user_id': user.id
- })
+ context
+ )
+
+
+def booking_modify_image(request, booking_id):
+ form = HostReImageForm(request.POST)
+ if form.is_valid():
+ booking = Booking.objects.get(id=booking_id)
+ if request.user != booking.owner:
+ return HttpResponse("unauthorized")
+ if timezone.now() > booking.end:
+ return HttpResponse("unauthorized")
+ new_image = Image.objects.get(id=form.cleaned_data['image_id'])
+ host = Host.objects.get(id=form.cleaned_data['host_id'])
+ host.config.image = new_image
+ host.config.save()
+ JobFactory.reimageHost(new_image, booking, host)
+ return HttpResponse(new_image.name)
+ return HttpResponse("error")
def booking_stats_view(request):
@@ -128,6 +196,6 @@ def booking_stats_view(request):
def booking_stats_json(request):
try:
span = int(request.GET.get("days", 14))
- except:
+ except Exception:
span = 14
return JsonResponse(StatisticsManager.getContinuousBookingTimeSeries(span), safe=False)
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 b1d97b7..597629f 100644
--- a/dashboard/src/dashboard/tasks.py
+++ b/dashboard/src/dashboard/tasks.py
@@ -11,10 +11,9 @@
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 JobStatus, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, AccessRelation
+from api.models import Job, JobStatus, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, AccessRelation
from resource_inventory.resource_manager import ResourceManager
@@ -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)
@@ -92,12 +91,15 @@ def free_hosts():
"""
gets all hosts from the database that need to be freed and frees them
"""
- networks = ~Q(~Q(job__hostnetworkrelation__status=200))
- hardware = ~Q(~Q(job__hosthardwarerelation__status=200))
+ undone_statuses = [JobStatus.NEW, JobStatus.CURRENT, JobStatus.ERROR]
+ undone_jobs = Job.objects.filter(
+ hostnetworkrelation__status__in=undone_statuses,
+ hosthardwarerelation__status__in=undone_statuses
+ )
- bookings = Booking.objects.filter(
- networks,
- hardware,
+ bookings = Booking.objects.exclude(
+ job__in=undone_jobs
+ ).filter(
end__lt=timezone.now(),
job__complete=True,
resource__isnull=False
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 36c3253..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,
}
)
@@ -78,7 +78,7 @@ def landing_view(request):
manager_detected = True
if request.method == 'GET':
- return render(request, 'dashboard/landing.html', {'manager': manager_detected, 'title': "Welcome!"})
+ return render(request, 'dashboard/landing.html', {'manager': manager_detected, 'title': "Welcome to the Lab as a Service Dashboard"})
if request.method == 'POST':
try:
diff --git a/dashboard/src/notifier/manager.py b/dashboard/src/notifier/manager.py
index 3361074..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
@@ -75,7 +75,7 @@ class NotificationHandler(object):
if (not hasattr(task, "user")) or task.user == user:
user_tasklist.append(
{
- "title": task.type_str + " Message: ",
+ "title": task.type_str() + " Message: ",
"content": task.message
}
)
@@ -94,7 +94,7 @@ class NotificationHandler(object):
"Your Booking is Ready",
message,
os.environ.get("DEFAULT_FROM_EMAIL", "opnfv@pharos-dashboard"),
- user.userprofile.email_addr,
+ [user.userprofile.email_addr],
fail_silently=False
)
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/0005_image_os.py b/dashboard/src/resource_inventory/migrations/0005_image_os.py
new file mode 100644
index 0000000..ede008e
--- /dev/null
+++ b/dashboard/src/resource_inventory/migrations/0005_image_os.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.1 on 2019-01-10 16:18
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0004_auto_20181017_1532'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='image',
+ name='os',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Opsys'),
+ ),
+ ]
diff --git a/dashboard/src/resource_inventory/migrations/0006_auto_20190124_1700.py b/dashboard/src/resource_inventory/migrations/0006_auto_20190124_1700.py
new file mode 100644
index 0000000..a5a972f
--- /dev/null
+++ b/dashboard/src/resource_inventory/migrations/0006_auto_20190124_1700.py
@@ -0,0 +1,76 @@
+# Generated by Django 2.1 on 2019-01-24 17:00
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import resource_inventory.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resource_inventory', '0005_image_os'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='cpuprofile',
+ name='host',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cpuprofile', to='resource_inventory.HostProfile'),
+ ),
+ migrations.AlterField(
+ model_name='diskprofile',
+ name='host',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='storageprofile', to='resource_inventory.HostProfile'),
+ ),
+ migrations.AlterField(
+ model_name='generichost',
+ name='profile',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.HostProfile'),
+ ),
+ migrations.AlterField(
+ model_name='generichost',
+ name='resource',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='generic_host', to='resource_inventory.GenericResource'),
+ ),
+ migrations.AlterField(
+ model_name='genericinterface',
+ name='host',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='generic_interfaces', to='resource_inventory.GenericHost'),
+ ),
+ migrations.AlterField(
+ model_name='genericresource',
+ name='bundle',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='generic_resources', to='resource_inventory.GenericResourceBundle'),
+ ),
+ migrations.AlterField(
+ model_name='genericresourcebundle',
+ name='lab',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='account.Lab'),
+ ),
+ migrations.AlterField(
+ model_name='genericresourcebundle',
+ name='owner',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterField(
+ model_name='hostconfiguration',
+ name='opnfvRole',
+ field=models.ForeignKey(on_delete=models.SET(resource_inventory.models.get_sentinal_opnfv_role), to='resource_inventory.OPNFVRole'),
+ ),
+ migrations.AlterField(
+ model_name='interfaceprofile',
+ name='host',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaceprofile', to='resource_inventory.HostProfile'),
+ ),
+ migrations.AlterField(
+ model_name='ramprofile',
+ name='host',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ramprofile', to='resource_inventory.HostProfile'),
+ ),
+ migrations.AlterField(
+ model_name='resourcebundle',
+ name='template',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.GenericResourceBundle'),
+ ),
+ ]
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 b56317b..b9f2c44 100644
--- a/dashboard/src/resource_inventory/models.py
+++ b/dashboard/src/resource_inventory/models.py
@@ -25,7 +25,7 @@ class HostProfile(models.Model):
labs = models.ManyToManyField(Lab, related_name="hostprofiles")
def validate(self):
- validname = re.compile("^[A-Za-z0-9\-\_\.\/\, ]+$")
+ validname = re.compile(r"^[A-Za-z0-9\-\_\.\/\, ]+$")
if not validname.match(self.name):
return "Invalid host profile name given. Name must only use A-Z, a-z, 0-9, hyphens, underscores, dots, commas, or spaces."
else:
@@ -39,7 +39,7 @@ class InterfaceProfile(models.Model):
id = models.AutoField(primary_key=True)
speed = models.IntegerField()
name = models.CharField(max_length=100)
- host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='interfaceprofile')
+ host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='interfaceprofile')
nic_type = models.CharField(
max_length=50,
choices=[
@@ -61,7 +61,7 @@ class DiskProfile(models.Model):
("HDD", "HDD")
])
name = models.CharField(max_length=50)
- host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='storageprofile')
+ host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='storageprofile')
rotation = models.IntegerField(default=0)
interface = models.CharField(
max_length=50,
@@ -88,7 +88,7 @@ class CpuProfile(models.Model):
("aarch64", "aarch64")
])
cpus = models.IntegerField()
- host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='cpuprofile')
+ host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='cpuprofile')
cflags = models.TextField(null=True)
def __str__(self):
@@ -99,39 +99,19 @@ class RamProfile(models.Model):
id = models.AutoField(primary_key=True)
amount = models.IntegerField()
channels = models.IntegerField()
- host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='ramprofile')
+ host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='ramprofile')
def __str__(self):
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)
name = models.CharField(max_length=300, unique=True)
xml = models.TextField()
- owner = models.ForeignKey(User, null=True, on_delete=models.DO_NOTHING)
- lab = models.ForeignKey(Lab, null=True, on_delete=models.DO_NOTHING)
+ owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
+ lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL)
description = models.CharField(max_length=1000, default="")
def getHosts(self):
@@ -145,9 +125,35 @@ 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.DO_NOTHING)
- hostname_validchars = RegexValidator(regex='(?=^.{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 (_)")
+ 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 (_)")
name = models.CharField(max_length=200, validators=[hostname_validchars])
def getHost(self):
@@ -157,7 +163,7 @@ class GenericResource(models.Model):
return self.name
def validate(self):
- validname = re.compile('(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))')
+ validname = re.compile(r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))')
if not validname.match(self.name):
return "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 (_)"
else:
@@ -167,8 +173,8 @@ class GenericResource(models.Model):
# Host template
class GenericHost(models.Model):
id = models.AutoField(primary_key=True)
- profile = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING)
- resource = models.OneToOneField(GenericResource, related_name='generic_host', on_delete=models.DO_NOTHING)
+ profile = models.ForeignKey(HostProfile, on_delete=models.CASCADE)
+ resource = models.OneToOneField(GenericResource, related_name='generic_host', on_delete=models.CASCADE)
def __str__(self):
return self.resource.name
@@ -177,20 +183,22 @@ class GenericHost(models.Model):
# Physical, actual resources
class ResourceBundle(models.Model):
id = models.AutoField(primary_key=True)
- template = models.ForeignKey(GenericResourceBundle, on_delete=models.DO_NOTHING)
+ template = models.ForeignKey(GenericResourceBundle, on_delete=models.SET_NULL, null=True)
def __str__(self):
+ if self.template is None:
+ 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.DO_NOTHING)
- host = models.ForeignKey(GenericHost, on_delete=models.DO_NOTHING, related_name='generic_interfaces')
+ 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)
@@ -222,9 +230,14 @@ 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) # consider setting to root user?
+ owner = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=200, unique=True)
description = models.CharField(max_length=1000, default="")
bundle = models.ForeignKey(GenericResourceBundle, null=True, on_delete=models.CASCADE)
@@ -238,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)
@@ -262,14 +278,18 @@ class Image(models.Model):
name = models.CharField(max_length=200)
owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
public = models.BooleanField(default=True)
- # may need to change host_type.on_delete to models.SET() once images are transferrable between compatible host types
host_type = models.ForeignKey(HostProfile, on_delete=models.CASCADE)
description = models.TextField()
+ os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE)
def __str__(self):
return self.name
+def get_sentinal_opnfv_role():
+ return OPNFVRole.objects.get_or_create(name="deleted", description="Role was deleted.")
+
+
class HostConfiguration(models.Model):
"""
model to represent a complete configuration for a single
@@ -279,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.PROTECT)
+ 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)
@@ -299,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
@@ -314,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 9282580..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
+from resource_inventory.models import (
+ Host,
+ HostConfiguration,
+ ResourceBundle,
+ HostProfile,
+ Network,
+ Vlan
+)
class ResourceManager:
@@ -33,61 +37,107 @@ class ResourceManager:
ResourceManager.instance = ResourceManager()
return ResourceManager.instance
+ def getAvailableHostTypes(self, lab):
+ hostset = Host.objects.filter(lab=lab).filter(booked=False).filter(working=True)
+ 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)
- except:
- self.fail_acquire(physical_hosts)
+ self.configureNetworking(physical_host, vlan_map)
+ except Exception:
+ self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
raise ResourceProvisioningException("Network configuration failed.")
try:
physical_host.save()
- except:
- self.fail_acquire(physical_hosts)
+ except Exception:
+ 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):
@@ -109,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
new file mode 100644
index 0000000..c3d0a4d
--- /dev/null
+++ b/dashboard/src/static/css/detail_view.css
@@ -0,0 +1,39 @@
+.card_container {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr;
+ grid-gap: 25px 25px;
+ justify-items: stretch;
+}
+
+.card_container ul > li {
+ padding: 7px !important;
+ font-size: 16px;
+}
+
+.detail_button_container .btn {
+ width: 49%;
+}
+
+.detail_button_container .btn-danger {
+ float: right;
+}
+
+#modal_warning {
+ transition: max-height 0.5s ease-out;
+ overflow: hidden;
+}
+
+.detail_card {
+ 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;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
diff --git a/dashboard/src/static/css/graph_common.css b/dashboard/src/static/css/graph_common.css
index 7f90a66..cff1516 100644
--- a/dashboard/src/static/css/graph_common.css
+++ b/dashboard/src/static/css/graph_common.css
@@ -26,9 +26,6 @@ div.mxRubberband {
margin: 0px;
}
div.mxWindow {
- -webkit-box-shadow: 3px 3px 12px #C0C0C0;
- -moz-box-shadow: 3px 3px 12px #C0C0C0;
- box-shadow: 3px 3px 12px #C0C0C0;
background: url('../img/mxgraph/window.gif');
border:1px solid #c3c3c3;
position: absolute;
@@ -67,19 +64,32 @@ td.mxWindowPane td {
font-size: 8pt;
}
td.mxWindowPane input, td.mxWindowPane select, td.mxWindowPane textarea, td.mxWindowPane radio {
- border-color: #8C8C8C;
- border-style: solid;
- border-width: 1px;
font-family: Arial;
font-size: 8pt;
padding: 1px;
}
td.mxWindowPane button {
- background: url('/static/img/mxgraph/button.gif') repeat-x;
- font-family: Arial;
- font-size: 8pt;
- padding: 2px;
- float: left;
+ color: #fff;
+ background-color: #337ab7;
+ border-color: #2e6da4;
+ display: inline-block;
+ margin: 2%;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 1.42857143;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ -ms-touch-action: manipulation;
+ touch-action: manipulation;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ background-image: none;
+ border: 1px solid transparent;
+ border-radius: 4px;
}
img.mxToolbarItem {
margin-right: 6px;
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 ef4df3a..98ab5c8 100644
--- a/dashboard/src/templates/account/booking_list.html
+++ b/dashboard/src/templates/account/booking_list.html
@@ -1,52 +1,136 @@
{% extends "base.html" %}
{% block content %}
-<script>
-function edit_booking(pk){
- var csrf = $('input[name="csrfmiddlewaretoken"]').val();
- $.ajax({
- type: "POST",
- url: "/",
- data: { "target": pk, "create": 0, "csrfmiddlewaretoken": csrf},
- beforeSend: function(request) {
- request.setRequestHeader("X-CSFRToken", csrf);
- }
- }).done(function(){
- window.location.replace("/wf/");
- }).fail(function(){})
-}
-</script>
<h2>Bookings I Own</h2>
+ <div class="card_container">
{% for booking in bookings %}
- <div style="border:2px;border-style:solid;border-color:grey;margin:5px">
- <ul>
- <li>id: {{booking.id}}</li>
- <li>lab: {{booking.resource.template.lab.lab_user.username}}</li>
- <li>resource: {{booking.resource.template.name}}</li>
- <li>start: {{booking.start}}</li>
- <li>end: {{booking.end}}</li>
- <li>purpose: {{booking.purpose}}</li>
- </ul>
- <div style="display:inline;margin:3px;padding:3px">
- <button onclick="edit_booking({{booking.id}});">Edit</button>
- <button onclick="location.href='/booking/detail/{{booking.id}}/';">Details</button>
+ <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 mr-2" href="/booking/detail/{{booking.id}}/">Details</a>
+ <button
+ class="btn btn-danger"
+ onclick='cancel_booking({{booking.id}});'
+ data-toggle="modal"
+ data-target="#resModal"
+ >Cancel</button>
</div>
</div>
{% endfor %}
+ </div>
<h2>Bookings I Collaborate On</h2>
- {% for booking in collab_bookings %}
- <div style="border:2px;border-style:solid;border-color:grey;margin:5px">
- <ul>
- <li>id: {{booking.id}}</li>
- <li>lab: {{booking.lab}}</li>
- <li>resource: {{booking.resource_name}}</li>
- <li>start: {{booking.start}}</li>
- <li>end: {{booking.end}}</li>
- <li>purpose: {{booking.purpose}}</li>
- </ul>
- <div style="display:inline;margin:3px;padding:3px">
- <button disabled=true onclick="edit_booking({{booking.id}});">Edit</button>
- <button onclick="location.href='/booking/detail/{{booking.id}}/';">Details</button>
+ <div class="card_container">
+ {% 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.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>
+ <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) {
+ current_booking_id = booking_id;
+ document.getElementById('modal_warning').style['max-height'] = '0px';
+ }
+
+ function submit_cancel_form() {
+ var ajaxForm = $("#booking_cancel_form");
+ var formData = ajaxForm.serialize();
+ req = new XMLHttpRequest();
+ var url = "cancel/" + current_booking_id;
+ req.open("POST", url, true);
+ req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ 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">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Cancel Booking?</h4>
+ <p>Everthing on your machine(s) will be lost</p>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <form id="booking_cancel_form">
+ {% csrf_token %}
+ </form>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+ <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Cancel Booking</button>
+ </div>
+ <div id="modal_warning" class="modal-footer" style="max-height:0px;" >
+ <div style="text-align:center; margin: 5px">
+ <h3>Are You Sure?</h3>
+ <p>This cannot be undone</p>
+ <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button>
+ <button class="btn btn-danger" id="confirm_cancel_button" data-dismiss="modal" onclick="submit_cancel_form();">I'm Sure</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
{% endblock %}
diff --git a/dashboard/src/templates/account/configuration_list.html b/dashboard/src/templates/account/configuration_list.html
index ee61e97..6f7844a 100644
--- a/dashboard/src/templates/account/configuration_list.html
+++ b/dashboard/src/templates/account/configuration_list.html
@@ -1,28 +1,72 @@
{% extends "base.html" %}
{% block content %}
+<div class="card_container">
+{% for config in configurations %}
+ <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>
+ <li class="list-group-item">description: {{config.description}}</li>
+ <li class="list-group-item">resource: {{config.bundle}}</li>
+ </ul>
+ </div>
+ <div class="card-footer">
+ <button
+ class="btn btn-danger w-100"
+ onclick='delete_config({{config.id}});'
+ data-toggle="modal"
+ data-target="#configModal"
+ >Delete</button>
+ </div>
+ </div>
+{% endfor %}
+</div>
<script>
-function edit_configuration(pk){
- var csrf = $('input[name="csrfmiddlewaretoken"]').val();
- $.ajax({
- type: "POST",
- url: "/",
- data: { "target": pk, "create": 2, "csrfmiddlewaretoken": csrf},
- beforeSend: function(request) {
- request.setRequestHeader("X-CSFRToken", csrf);
- }
- }).done(function(){
- window.location.replace("/wf/");
- }).fail(function(){});
-}
+ var current_config_id = -1;
+ function delete_config(config_id) {
+ current_config_id = config_id;
+ document.getElementById('modal_warning').style['max-height'] = '0px';
+ }
+
+ function submit_delete_form() {
+ var ajaxForm = $("#config_delete_form");
+ var formData = ajaxForm.serialize();
+ req = new XMLHttpRequest();
+ var url = "delete/" + current_config_id;
+ req.open("POST", url, true);
+ req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ req.onerror = function() { alert("problem submitting form"); }
+ req.send(formData);
+ }
</script>
- {% for config in configurations %}
- <div style="border:2px;border-style:solid;border-color:grey;margin:5px">
- <ul>
- <li>id: {{config.id}}</li>
- <li>name: {{config.name}}</li>
- <li>description: {{config.description}}</li>
- </ul>
- <button onclick="edit_configuration({{config.id}});">Edit</button>
+<div class="modal fade" id="configModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true">
+ <div class="modal-dialog" style="width: 450px;" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Delete Configuration?</h4>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <form id="config_delete_form">
+ {% csrf_token %}
+ </form>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+ <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Delete</button>
+ </div>
+ <div id="modal_warning" class="modal-footer" style="max-height:0px;" >
+ <div style="text-align:center; margin: 5px">
+ <h3>Are You Sure?</h3>
+ <p>This cannot be undone</p>
+ <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button>
+ <button class="btn btn-danger" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button>
+ </div>
</div>
- {% endfor %}
+ </div>
+</div>
{% endblock %}
diff --git a/dashboard/src/templates/account/details.html b/dashboard/src/templates/account/details.html
index 5641064..3092ad0 100644
--- a/dashboard/src/templates/account/details.html
+++ b/dashboard/src/templates/account/details.html
@@ -2,8 +2,8 @@
{% load staticfiles %}
{% block content %}
<h1>Account Details</h1>
-<button onclick="location.href = '{% url 'account:my-resources' %}'">My Resources</button>
-<button onclick="location.href = '{% url 'account:my-bookings' %}'">My Bookings</button>
-<button onclick="location.href = '{% url 'account:my-configurations' %}'">My Configurations</button>
-<button onclick="location.href = '{% url 'account:my-images' %}'">My Snapshots</button>
+<a class="btn btn-primary" href="{% url 'account:my-resources' %}">My Resources</a>
+<a class="btn btn-primary" href="{% url 'account:my-bookings' %}">My Bookings</a>
+<a class="btn btn-primary" href="{% url 'account:my-configurations' %}">My Configurations</a>
+<a class="btn btn-primary" href="{% url 'account:my-images' %}">My Snapshots</a>
{% endblock content %}
diff --git a/dashboard/src/templates/account/image_list.html b/dashboard/src/templates/account/image_list.html
index fb436df..068e096 100644
--- a/dashboard/src/templates/account/image_list.html
+++ b/dashboard/src/templates/account/image_list.html
@@ -1,27 +1,123 @@
{% extends "base.html" %}
{% block content %}
<h2>Images I Own</h2>
- {% for image in images %}
- <div style="border:2px;border-style:solid;border-color:grey;margin:5px">
- <ul>
- <li>id: {{image.id}}</li>
- <li>lab: {{image.from_lab.name}}</li>
- <li>name: {{image.name}}</li>
- <li>description: {{image.description}}</li>
- <li>host profile: {{image.host_type.name}}</li>
+<div class="card_container">
+{% for image in images %}
+ <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>
+ <li class="list-group-item">name: {{image.name}}</li>
+ <li class="list-group-item">description: {{image.description}}</li>
+ <li class="list-group-item">host profile: {{image.host_type.name}}</li>
</ul>
</div>
- {% endfor %}
+ <div class="card-footer">
+ <button
+ class="btn btn-danger w-100"
+ onclick='delete_image({{image.id}});'
+ data-toggle="modal"
+ data-target="#imageModal"
+ >Delete</button>
+ </div>
+ </div>
+{% endfor %}
+</div>
<h2>Public Images</h2>
+<div class="card_container">
{% for image in public_images %}
- <div style="border:2px;border-style:solid;border-color:grey;margin:5px">
- <ul>
- <li>id: {{image.id}}</li>
- <li>lab: {{image.from_lab.name}}</li>
- <li>name: {{image.name}}</li>
- <li>description: {{image.description}}</li>
- <li>host profile: {{image.host_type.name}}</li>
- </ul>
+ <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>
+ <li class="list-group-item">name: {{image.name}}</li>
+ <li class="list-group-item">description: {{image.description}}</li>
+ <li class="list-group-item">host profile: {{image.host_type.name}}</li>
+ </ul>
+ </div>
</div>
{% endfor %}
+</div>
+
+<script>
+ var current_image_id = -1;
+ var used_images = {{used_images|safe|default:"{}"}};
+ function delete_image(image_id) {
+ current_image_id = image_id;
+ document.getElementById('modal_warning').style['max-height'] = '0px';
+ var warning_header = document.getElementById("warning_header");
+ var warning_text = document.getElementById("warning_text");
+ var delete_image_button = document.getElementById("final_delete_b");
+ clear(warning_header);
+ clear(warning_text);
+ if(used_images[image_id]) {
+ warning_header.appendChild(
+ document.createTextNode("Cannot Delete")
+ );
+ warning_text.appendChild(
+ document.createTextNode("This snapshot is being used in a booking.")
+ );
+ delete_image_button.disabled = true;
+ } else {
+ warning_header.appendChild(
+ document.createTextNode("Are You Sure?")
+ );
+ warning_text.appendChild(
+ document.createTextNode("This cannot be undone")
+ );
+ delete_image_button.removeAttribute("disabled");
+ }
+ }
+
+ function submit_delete_form() {
+ var ajaxForm = $("#image_delete_form");
+ var formData = ajaxForm.serialize();
+ req = new XMLHttpRequest();
+ var url = "delete/" + current_image_id;
+ req.open("POST", url, true);
+ req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ req.onerror = function() { alert("problem submitting form"); }
+ req.send(formData);
+ }
+
+ function clear(node) {
+ while(node.lastChild) {
+ node.removeChild(node.lastChild);
+ }
+ }
+</script>
+<div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true">
+ <div class="modal-dialog" style="width: 450px;" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Delete Configuration?</h4>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <form id="image_delete_form">
+ {% csrf_token %}
+ </form>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+ <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Delete</button>
+ </div>
+ <div id="modal_warning" class="modal-footer" style="max-height:0px;" >
+ <div style="text-align:center; margin: 5px">
+ <h3 id="warning_header">Are You Sure?</h3>
+ <p id="warning_text">This cannot be undone</p>
+ <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button>
+ <button id="final_delete_b" class="btn btn-danger" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button>
+ </div>
+ </div>
+ </div>
+</div>
{% endblock %}
diff --git a/dashboard/src/templates/account/resource_list.html b/dashboard/src/templates/account/resource_list.html
index 482a000..f92f78e 100644
--- a/dashboard/src/templates/account/resource_list.html
+++ b/dashboard/src/templates/account/resource_list.html
@@ -1,28 +1,118 @@
{% extends "base.html" %}
{% block content %}
+<div class="card_container">
+{% for resource in resources %}
+ <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="card-footer">
+ <button
+ class="btn btn-danger w-100"
+ onclick='delete_resource({{resource.id}});'
+ data-toggle="modal"
+ data-target="#resModal"
+ >Delete</button>
+ </div>
+ </div>
+{% endfor %}
+</div>
<script>
-function edit_resource(pk){
- var csrf = $('input[name="csrfmiddlewaretoken"]').val();
- $.ajax({
- type: "POST",
- url: "/",
- data: { "target": pk, "create": 1, "csrfmiddlewaretoken": csrf},
- beforeSend: function(request) {
- request.setRequestHeader("X-CSFRToken", csrf);
+ var grb_mapping = {{grb_mapping|safe|default:"{}"}};
+ var booking_mapping = {{booking_mapping|safe|default:"{}"}};
+ var current_resource_id = -1;
+ function delete_resource(resource_id) {
+ document.getElementById("confirm_delete_button").removeAttribute("disabled");
+ var configs = grb_mapping[resource_id];
+ var warning = document.createTextNode("Are You Sure?");
+ var warning_subtext = document.createTextNode("This cannot be undone");
+ if(booking_mapping[resource_id]){
+ var warning = document.createTextNode("This resource is being used. It cannot be deleted.");
+ var warning_subtext = document.createTextNode("If your booking just ended, you may need to give us a few minutes to clean it up before this can be removed.");
+
+ document.getElementById("confirm_delete_button").disabled = true;
+ }
+ else if(configs.length > 0) {
+ list_configs(configs);
+ warning_text = "Are You Sure? The following Configurations will also be deleted.";
+ warning = document.createTextNode(warning_text);
+ }
+
+ current_resource_id = resource_id;
+ set_modal_text(warning, warning_subtext);
+ }
+
+ function set_modal_text(title, text) {
+ var clear = function(node) {
+ while(node.lastChild) {
+ node.removeChild(node.lastChild);
}
- }).done(function(){
- window.location.replace("/wf/");
- }).fail(function(){});
-}
+ }
+ var warning_title = document.getElementById("config_warning");
+ var warning_text = document.getElementById("warning_subtext");
+
+ clear(warning_title);
+ clear(warning_text);
+
+ warning_title.appendChild(title);
+ warning_text.appendChild(text);
+ document.getElementById('modal_warning').style['max-height'] = '0px';
+ }
+
+ function list_configs(configs) {
+ var list = document.getElementById("config_list");
+ for(var i=0; i<configs.length; i++){
+ var str = configs[i].name;
+ var list_item = document.createElement("LI");
+ list_item.appendChild(document.createTextNode(str));
+ list.appendChild(list_item);
+ }
+ }
+
+ function submit_delete_form() {
+ var ajaxForm = $("#res_delete_form");
+ var formData = ajaxForm.serialize();
+ req = new XMLHttpRequest();
+ var url = "delete/" + current_resource_id;
+ req.open("POST", url, true);
+ req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ req.onerror = function() { alert("problem submitting form"); }
+ req.send(formData);
+ }
</script>
- {% for resource in resources %}
- <div style="border:2px;border-style:solid;border-color:grey;margin:5px">
- <ul>
- <li>id: {{resource.id}}</li>
- <li>name: {{resource.name}}</li>
- <li>description: {{resource.description}}</li>
- </ul>
- <button onclick="edit_resource({{resource.id}});">Edit</button>
+<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">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Delete Resource?</h4>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <form id="res_delete_form">
+ {% csrf_token %}
+ </form>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+ <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Delete</button>
+ </div>
+ <div id="modal_warning" class="modal-footer" style="max-height:0px;" >
+ <div style="text-align:center; margin: 5px">
+ <h3 id="config_warning">Are You Sure?</h3>
+ <p id="warning_subtext">This cannot be undone</p>
+ <ul id="config_list"></ul>
+ <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button>
+ <button class="btn btn-danger" id="confirm_delete_button" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button>
+ </div>
</div>
- {% endfor %}
+ </div>
+</div>
+
{% endblock %}
diff --git a/dashboard/src/templates/account/userprofile_update_form.html b/dashboard/src/templates/account/userprofile_update_form.html
index f4bb7b5..6ab8242 100644
--- a/dashboard/src/templates/account/userprofile_update_form.html
+++ b/dashboard/src/templates/account/userprofile_update_form.html
@@ -1,23 +1,18 @@
-{% 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 %}
{% bootstrap_form form %}
<p><b>API Token</b>
- <a href="{% url 'generate_token' %}" class="btn btn-default">
+ <a href="{% url 'generate_token' %}" class="btn btn-primary">
Generate
</a>
</p>
@@ -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 c63db8c..62a9ed5 100644
--- a/dashboard/src/templates/base.html
+++ b/dashboard/src/templates/base.html
@@ -1,51 +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">
-
+<!-- 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%;
@@ -65,140 +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="cwf(0)">Create a Booking</button>
- <button class="btn drop_btn" onclick="cwf(1)">Create 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 'api-root' %}"><i
- class="fa fa-fw"></i>API
+ <a href="{% url 'booking:stats' %}" class="list-group-item list-group-item-action bg-light">
+ Booking Statistics
</a>
- </li>
- <li>
- <a href="{% url 'account:my-account' %}"><i
- class="fa fa-fw"></i>Account
+ <!-- <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 'dashboard:all_labs' %}"><i
- class="fa fa-fw"></i>Lab Info
+ <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>
- <li>
- <a href="{% url 'notifier:messages' %}"><i
- class="fa fa-fw"></i>Inbox
+ <a href="{% url 'notifier:messages' %}" class="list-group-item list-group-item-action bg-light">
+ Inbox
</a>
- </li>
- </ul>
- </div>
- <!-- /.sidebar-collapse -->
+ </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 349cb0a..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 }}
@@ -129,7 +129,7 @@
<div class="modal-body" id="booking_detail_content">
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-default" data-dismiss="modal">Close
+ <button type="button" class="btn btn-primary" data-dismiss="modal">Close
</button>
</div>
</div>
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 cae0e25..918f5af 100644
--- a/dashboard/src/templates/booking/booking_detail.html
+++ b/dashboard/src/templates/booking/booking_detail.html
@@ -1,22 +1,31 @@
{% extends "base.html" %}
{% load staticfiles %}
+{% load bootstrap4 %}
{% block extrahead %}
{{block.super}}
<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js?lang=yaml"></script>
-<script src="https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js"></script>
{% endblock %}
{% block content %}
+
+<style>
+#modal_warning {
+ transition: max-height 0.5s ease-out;
+ overflow: hidden;
+}
+
+</style>
+
<div class="container-fluid">
<div class="row">
- <div class="col-lg-6">
- <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>
@@ -50,15 +59,13 @@
</div>
</div>
<div class="row">
-
- <div class="col-lg-6">
-
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="col-lg-12">
+ <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>
@@ -79,7 +86,15 @@
</tr>
<tr>
<td>Image:</td>
- <td>{{host.config.image}}</td>
+ <td id="host_image_{{host.id}}">
+ {{host.config.image}}
+ <button
+ style="margin-left:10px;"
+ class="btn btn-primary"
+ data-toggle="modal"
+ data-target="#imageModal"
+ onclick="set_image_dropdown('{{host.profile.name}}', {{host.id}});"
+ >Change/Reset</button></td>
</tr>
<tr>
<td>RAM:</td>
@@ -111,15 +126,15 @@
<table class="table">
<tr>
<td>Size:</td>
- <td>{{host.profile.diskprofile.first.size}}GiB</td>
+ <td>{{host.profile.storageprofile.first.size}} GiB</td>
</tr>
<tr>
<td>Type:</td>
- <td>{{host.profile.diskprofile.first.media_type}}</td>
+ <td>{{host.profile.storageprofile.first.media_type}}</td>
</tr>
<tr>
<td>Mount Point:</td>
- <td>{{host.profile.diskprofile.first.name}}</td>
+ <td>{{host.profile.storageprofile.first.name}}</td>
</tr>
</table>
</td>
@@ -152,10 +167,7 @@
</table>
</td>
</tr>
-
-
</table>
-
</td>
{% endfor %}
</tr>
@@ -163,32 +175,16 @@
</div>
</div>
</div>
- <div class="col-lg-6">
-
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">PDF</h4>
- <a data-toggle="collapse" data-target="#pdf_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
- </div>
-
- <div class="panel-body" id="pdf_panel" style="padding: 0px;">
- <pre class="prettyprint lang-yaml" style="margin: 0px; padding: 0px; border: none;">
-{{pdf}}
- </pre>
- </div>
- </div>
- </div>
</div>
</div>
- <div class="col-lg-6">
- <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 {
@@ -215,7 +211,6 @@
border-radius: 50%;
animation: fadeInOut 1s infinite alternate;
-
}
@keyframes fadeInOut {
from { opacity: 0;}
@@ -244,9 +239,7 @@
{% else %}
<div class="done"></div>
{% endif %}
- </td>
-
-
+ </td>
<td>
{% if task.status < 100 %}
PENDING
@@ -257,7 +250,6 @@
{% endif %}
</td>
<td>
-
{% if task.message %}
{% if task.type_str == "Access Task" and user_id != task.config.user.id %}
Message from Lab: <pre>--secret--</pre>
@@ -270,16 +262,104 @@
</td>
<td>
{{ task.type_str }}
-
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
+ <div class="row">
+ <div class="col">
+ <div class="card">
+ <div class="card-header d-flex">
+ <h4 style="display: inline;">PDF</h4>
+ <button data-toggle="collapse" data-target="#pdf_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
+ </div>
+ <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}}
+ </pre>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</div>
</div>
+<div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
+ <div class="modal-dialog" style="width: 450px;" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title" id="exampleModalLabel" style="display: inline; float: left;">Host Image</h4>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <form id="image_host_form">
+ {% csrf_token %}
+ <select class="form-control" style="width: 80%; margin-left: 10%" id="image_select" name="image_id">
+ </select>
+ <input id="host_id_input" type="hidden" name="host_id">
+ </input>
+ </form>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+ <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Reset Host</button>
+ </div>
+ <div id="modal_warning" class="modal-footer" style="max-height:0px;" >
+ <div style="text-align:center; margin: 5px">
+ <h3>Are You Sure?</h3>
+ <p>This will wipe the disk and reimage the host</p>
+ <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button>
+ <button class="btn btn-danger" data-dismiss="modal" onclick="submit_image_form();">I'm Sure</button>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script>
+ var image_mapping = {{image_mapping|safe}};
+ var current_host_id = 0;
+ function set_image_dropdown(profile_name, host_id) {
+ document.getElementById("host_id_input").value = host_id;
+ current_host_id = host_id;
+ var dropdown = document.getElementById("image_select");
+ var length = dropdown.length;
+ //clear dropdown
+ for(i=length-1; i>=0; i--){
+ dropdown.options.remove(i);
+ }
+ var images = image_mapping[profile_name];
+ var image_length = images.length;
+ for(i=0; i<image_length; i++){
+ var opt = document.createElement("OPTION");
+ opt.value = images[i].value;
+ opt.appendChild(document.createTextNode(images[i].name));
+ dropdown.options.add(opt);
+ }
+
+ document.getElementById("modal_warning").style['max-height'] = '0px';
+ }
+
+ function submit_image_form() {
+ var ajaxForm = $("#image_host_form");
+ var formData = ajaxForm.serialize();
+ req = new XMLHttpRequest();
+ req.open("POST", "/booking/modify/{{booking.id}}/image/", true);
+ req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ req.onerror = function() { alert("problem submitting form"); }
+ req.onreadystatechange = function() {
+ if(req.readyState === 4) {
+ node = document.getElementById("host_image_" + current_host_id);
+ text = document.createTextNode(req.responseText);
+ node.replaceChild(text, node.firstChild);
+ }
+ }
+ req.send(formData);
+ }
+</script>
{% endblock content %}
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
new file mode 100644
index 0000000..07f3d89
--- /dev/null
+++ b/dashboard/src/templates/booking/quick_deploy.html
@@ -0,0 +1,187 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+{% load bootstrap4 %}
+{% block content %}
+<style>
+ .grid_container {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ padding: 30px;
+ }
+ .grid_element {
+ border-radius: 5px;
+ border: 1px solid #ccc;
+ margin: 10px;
+ padding: 7px;
+ }
+ .grid_element_wide {
+ grid-column-start: span 12;
+ }
+ .grid_element_half {
+ grid-column-start: span 6;
+ }
+ .grid_element_1third {
+ grid-column-start: span 4;
+ }
+ .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">
+{% csrf_token %}
+<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 show_label=False %}
+</div>
+<div class="grid_element booking_info_pane grid_element_1third">
+ {% bootstrap_field form.purpose %}
+ {% bootstrap_field form.project %}
+ {% bootstrap_field form.length %}
+ <p style="display:inline;">Days: </p><output id="daysout" style="display:inline;">0</output>
+ <script>
+ document.getElementById("id_length").setAttribute("oninput", "daysout.value=this.value");
+ document.getElementById("daysout").value = document.getElementById("id_length").value;
+ </script>
+</div>
+<div class="grid_element collaborator_pane grid_element_1third">
+ <label>Collaborators</label>
+ {{ form.users }}
+</div>
+<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">
+
+ function submit_form()
+ {
+ //formats data for form submission
+ multi_filter_widget.finish();
+ }
+
+ 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
+ }
+ }
+
+ 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 }};
+ var sup_installer_dict = {{ installer_filter|safe }};
+ var sup_scenario_dict = {{ scenario_filter|safe }};
+
+ function imageHider() {
+ var drop = document.getElementById("id_image");
+
+ hide_dropdown("id_image");
+
+ 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
+ {
+ if( image_object.host_profile == host_pk && image_object.lab == lab_pk )
+ {
+ drop.childNodes[i].style.display = "inherit";
+ drop.childNodes[i].disabled = false;
+ }
+ }
+ }
+ }
+
+ imageHider();
+ $('#id_installer').children().hide();
+ $('#id_scenario').children().hide();
+
+
+ Array.from(document.getElementsByClassName("grid-item-select-btn")).forEach(function(element) {
+ element.addEventListener('click', imageHider);
+ });
+
+ function installerHider() {
+ dropFilter("id_installer", sup_installer_dict, "id_image");
+ scenarioHider();
+ }
+ document.getElementById('id_image').addEventListener('change', installerHider);
+
+ function scenarioHider() {
+ dropFilter("id_scenario", sup_scenario_dict, "id_installer");
+ }
+ document.getElementById('id_installer').addEventListener('change', scenarioHider);
+
+ function dropFilter(target, target_filter, master) {
+ var dropdown = document.getElementById(target);
+
+ hide_dropdown(target);
+
+ var drop = document.getElementById(master);
+ var opts = target_filter[drop.options[drop.selectedIndex].value];
+ if (!opts) {
+ opts = {};
+ }
+
+ var map = Object.create(null);
+ for (var i = 0; i < opts.length; i++) {
+ var j = opts[i];
+ map[j] = true;
+ }
+
+ 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 id="quick_booking_confirm" onclick="submit_form();" class="btn btn-success">Confirm</button>
+</form>
+{% endblock %}
diff --git a/dashboard/src/templates/booking/stats.html b/dashboard/src/templates/booking/stats.html
index abb153b..8bc68cd 100644
--- a/dashboard/src/templates/booking/stats.html
+++ b/dashboard/src/templates/booking/stats.html
@@ -41,15 +41,28 @@ function getData(){
{% endblock %}
{% block content %}
- <p>Number of days to plot: </p>
- <input id="number_days" type="number" min="1" step="1"/>
- <button onclick="getData();">Submit</button>
- <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 a42e158..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>
@@ -58,7 +79,6 @@
{% block onleave %}
var ajaxForm = $("#booking_meta_form");
var formData = ajaxForm.serialize();
-console.log(formData);
req = new XMLHttpRequest();
req.open("POST", "/wf/workflow/", false);
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
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 3e0aacd..e6a235f 100644
--- a/dashboard/src/templates/dashboard/landing.html
+++ b/dashboard/src/templates/dashboard/landing.html
@@ -2,20 +2,30 @@
{% load staticfiles %}
{% block content %}
- <div class="">
- <p style="text-align:center;">Welcome to the Pharos Dashboard! To get started, select one of the options below:</p>
+<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;
}
@@ -27,48 +37,122 @@
display: grid;
grid-template-columns: 33% 34% 33%;
}
-</style>
-<script type="text/javascript">
- function cwf(wf_type){
- document.getElementById('id_workflow').selectedIndex = wf_type;
- document.getElementById('wf_selection_form').submit();
+
+ .landing_container {
+ display: grid;
+ grid-template-columns: 1fr 30px 1fr;
}
-</script>
-<div class='wf_create_div'>
-<button class="wf_create btn" onclick="cwf(0)">Create a Booking</button>
-<button class="wf_create btn" onclick="cwf(1)">Create a Pod</button>
-<button class="wf_create btn" onclick="cwf(2)">Configure a Pod</button>
-<button class="wf_create btn" onclick="cwf(3)">Create a Snapshot</button>
-{% if manager == True %}
-<button class="wf_continue btn" onclick="continue_wf()">Finish Unfinished Business</button>
-{% endif %}
+
+ .grid_panel {
+ padding: 30px;
+ }
+
+ .btn-primary {
+ margin: 10px;
+ }
+
+ h2 {
+ border-bottom: 1px solid #cccccc;
+ }
+
+ 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 %}
+ <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/");
}
-
- //success: window.location.replace("/wf/")
-
</script>
<div class="hidden_form" id="form_div">
@@ -85,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 31b8f33..4302543 100644
--- a/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
+++ b/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
@@ -1,403 +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 items = {{ 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) {
- init();
- }
- for(var k in selection_data) {
- selected_items = selection_data[k];
- for( var item in selected_items ){
- var node = items[item];
- if(!node['multiple']){
- var input_value = selected_items[item];
- if( input_value != 'false' ) {
- select(node);
- markAndSweep(node);
- }
- var div = document.getElementById(item)
- var input = div.parentNode.getElementsByTagName("input")[0]
- input.value = input_value;
- updateResult(item);
- } else {
- make_multiple_selection(selected_items, item);
- }
- }
- }
-}
-
-function make_multiple_selection(data, item_class){
- var node = 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 items) {
- node = 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 = items[neighId];
- toCheck.push(neighbor);
- }
- }
- }
-
- //now remove all nodes still marked
- for(var nodeId in items){
- node = items[nodeId];
- if(node['marked']){
- disable(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 items) {
- node = 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) {
- 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){
- init();
- }
- var element = document.getElementById(id);
- var node = 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_item(node);
- process(node);
+.dropdown_item > button {
+ margin: 2px;
+ justify-self: end;
}
-function add_item(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(items[div_class]);
- }
+#dropdown_wrapper {
+ display: grid;
+ grid-template-columns: 4fr 5fr;
}
+</style>
-function updateResult(nodeId){
- if(!initialized){
- init();
- }
- if(!items[nodeId]['multiple']){
- var node = document.getElementById(nodeId);
- var value = {}
- value[nodeId] = node.parentNode.getElementsByTagName("input")[0].value;
- 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 init() {
- for(nodeId in items) {
- element = document.getElementById(nodeId);
- node = 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/resource_detail.html b/dashboard/src/templates/dashboard/resource_detail.html
index 79389f0..0a443d9 100644
--- a/dashboard/src/templates/dashboard/resource_detail.html
+++ b/dashboard/src/templates/dashboard/resource_detail.html
@@ -101,12 +101,10 @@
{{ resource.owner.email }}
</p>
<p>
- <a href="{% url 'booking:create' resource_id=resource.id %}" class="btn
- btn-primary">
+ <a href="{% url 'booking:create' resource_id=resource.id %}" class="btn btn-primary">
Booking
</a>
- <a href="{{ resource.url }}" class="btn
- btn-primary">
+ <a href="{{ resource.url }}" class="btn btn-primary">
OPNFV Wiki
</a>
</p>
diff --git a/dashboard/src/templates/dashboard/searchable_select_multiple.html b/dashboard/src/templates/dashboard/searchable_select_multiple.html
index e7128b0..8bcf890 100644
--- a/dashboard/src/templates/dashboard/searchable_select_multiple.html
+++ b/dashboard/src/templates/dashboard/searchable_select_multiple.html
@@ -1,35 +1,58 @@
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
+<script src="/static/js/dashboard.js"></script>
+
+<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>
+ <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 class="autocomplete" style="width:400px;">
- <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="{{initial.name}}" oninput="search(this.value)"
+ </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;
}
@@ -37,372 +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 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;
- }
-
- 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 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);
- }
+ searchableSelectMultipleWidgetEntry();
- 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);
- }
-
- 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/email_fulfilled.txt b/dashboard/src/templates/notifier/email_fulfilled.txt
index d473961..65593db 100644
--- a/dashboard/src/templates/notifier/email_fulfilled.txt
+++ b/dashboard/src/templates/notifier/email_fulfilled.txt
@@ -3,9 +3,9 @@
The booking you requested of the OPNFV Lab as a Service has finished deploying and is ready for you to use.
The lab that fulfilled your booking request has sent you the following messages:
- {% for message in messages %}
- {% message.title %}
- {% message.content %}
+ {% for email_message in messages %}
+ {{ email_message.title }}
+ {{ email_message.content }}
--------------------
{% endfor %}
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 389ff6d..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>
@@ -14,5 +36,11 @@
{% endblock content %}
{% block onleave %}
-document.getElementById("resource_meta_form").submit();
+var ajaxForm = $("#resource_meta_form");
+var formData = ajaxForm.serialize();
+req = new XMLHttpRequest();
+req.open("POST", "/wf/workflow/", false);
+req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+req.onerror = function() { alert("problem submitting form"); }
+req.send(formData);
{% endblock %}
diff --git a/dashboard/src/templates/resource/steps/pod_definition.html b/dashboard/src/templates/resource/steps/pod_definition.html
index ab9dfb3..5826ccb 100644
--- a/dashboard/src/templates/resource/steps/pod_definition.html
+++ b/dashboard/src/templates/resource/steps/pod_definition.html
@@ -8,609 +8,13 @@
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);
- addToolbarButton(editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
- 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);
- }
- });
-
- 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 ) {
- var content = document.createElement('div');
- var innerHTML = "<button onclick=deleteCell('" + cell.getId() + "');>Remove</button>"
- innerHTML += "<button onclick='currentWindow.destroy();'>Cancel</button>"
- content.innerHTML = innerHTML;
- showWindow(this, 'Delete?', content, 200, 200);
- }
- 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 type="button" onclick="parseNetworkWindow()">Okay</button>';
- innerHtml += '<button type="button" 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");
- 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");
- newNet.id = net_id;
- var text = net_name;
- if(vlan_id){
- text += " : " + vlan_id;
- }
- var newNetValue = document.createTextNode(text);
- newNet.appendChild(newNetValue);
- newNet.style['background'] = color;
- 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;
- //form.submit();
- 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. -->
{% block content %}
<div id="graphParent"
- style="position:absolute;overflow:hidden;top:0px;bottom:0px;width:65%;left:0px;">
+ style="position:absolute;overflow:hidden;top:0px;bottom:0px;width:75%;left:0px;">
<div id="graphContainer"
style="position:relative;overflow:hidden;top:36px;bottom:0px;left:0px;right:0px;background-image:url('/static/img/mxgraph/grid.gif');cursor:default;">
</div>
@@ -618,7 +22,7 @@ function submitForm() {
<!-- Creates a container for the sidebar -->
<div id="toolbarContainer"
- style="position:absolute;white-space:nowrap;overflow:hidden;top:0px;left:0px;max-height:24px;height:36px;right:0px;padding:6px;background-image:url('/static/img/mxgraph/toolbar_bg.gif');">
+ style="position:absolute;white-space:nowrap;overflow:hidden;top:0px;left:0px;right:0px;padding:6px;">
</div>
<!-- Creates a container for the outline -->
@@ -627,12 +31,67 @@ function submitForm() {
</div>
</div>
- <div id="network_select" style="position:absolute;top:0px;bottom:0px;width:35%;right:0px;left:auto;background:grey">
- <button type="button" onclick="newNetworkWindow();">Add Network</button>
+ <style>
+ p {
+ word-break: normal;
+ white-space: normal;
+ }
+ #network_select {
+ background: inherit;
+ padding: 0px;
+ padding-top: 0px;
+ }
+ #toolbarContainer {
+ background: #DDDDDD;
+ height: 36px;
+ }
+ #toolbar_extension {
+ height: 36px;
+ background: #DDDDDD;
+ }
+ #btn_add_network {
+ width: 100%;
+ }
+ #vlan_notice {
+ margin: 20px;
+ }
+ #network_list li {
+ border-radius: 2px;
+ margin: 5px;
+ padding: 5px;
+ vertical-align: middle;
+ background: #DDDDDD;
+ }
+ #network_list {
+ list-style-type: none;
+ padding: 0;
+ }
+ .colorblob {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: inline-block;
+ vertical-align: middle;
+ }
+ .network_innertext {
+ display: inline-block;
+ padding-left: 10px;
+ vertical-align: middle;
+ padding-bottom: 0px;
+ margin-bottom: 2px;
+ }
+ .mxWindow {
+ background: #FFFFFF;
+ }
+ </style>
+
+ <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="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 %}
@@ -640,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 2e767cc..bea475d 100644
--- a/dashboard/src/templates/snapshot_workflow/steps/meta.html
+++ b/dashboard/src/templates/snapshot_workflow/steps/meta.html
@@ -1,19 +1,24 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
-
-
<style>
+.meta_container {
+ padding: 50px;
+}
</style>
-
{% bootstrap_form_errors form type='non_fields' %}
-<form id="meta_form" action="/wf/workflow/" method="POST" class="form">
-{% csrf_token %}
-{{form}}
-</form>
+<div class="meta_container">
+ <form id="meta_form" action="/wf/workflow/" method="POST" class="form">
+ {% csrf_token %}
+ <div class="form-group">
+ {% bootstrap_field form.name %}
+ {% bootstrap_field form.description %}
+ </div>
+ </form>
+</div>
{% endblock content %}
{% block onleave %}
diff --git a/dashboard/src/templates/snapshot_workflow/steps/select_host.html b/dashboard/src/templates/snapshot_workflow/steps/select_host.html
index 16dd5d4..f438bac 100644
--- a/dashboard/src/templates/snapshot_workflow/steps/select_host.html
+++ b/dashboard/src/templates/snapshot_workflow/steps/select_host.html
@@ -1,47 +1,77 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
-
<style>
.booking {
- border-style: solid;
+ border-style: none;
border-color: black;
- border-width: 2px;
- display: inline-block;
- padding: 3px;
+ border: 2px;
+ border-radius: 5px;
+ margin: 20px;
+ padding-left: 25px;
+ padding-right: 25px;
+ padding-bottom: 25px;
+ box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75);
+ transition-property: box-shadow;
+ transition-duration: 0.1s;
+ float: left;
+ }
+ .booking:hover {
+ box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.75);
+ transition-property: box-shadow;
+ transition-duration: 0.1s;
}
.host {
+ cursor: pointer;
border-style: solid;
border-color: black;
border-width: 1px;
- margin: 2px;
+ border-radius: 5px;
+ margin: 5px;
+ padding: 5px;
+ text-align: center;
+ box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.75);
+ transition-property: box-shadow;
+ transition-duration: 0.1s;
+ }
+ .host:hover {
+ box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.75);
+ transition-property: box-shadow;
+ transition-duration: 0.1s;
+ background-color: rgba(144,238,144,0.3);
+ }
+ .selected {
+ background-color: lightgreen !important;
+ }
+ .booking_container {
+ overflow: auto;
+ padding: 30px;
}
</style>
-
{% bootstrap_form_errors form type='non_fields' %}
<form id="host_select_form" action="/wf/workflow/" method="POST" class="form">
{% csrf_token %}
<input type="hidden" id="hidden_json_input", name="host"/>
</form>
-<div id="host_select_container">
+<div id="host_select_container" class="booking_container">
</div>
<script>
var selected_host = null;
-var initial = {{chosen|default:'null'}};
+var initial = {{chosen|safe|default:'null'}};
function select(booking_id, host_name){
var input = document.getElementById("hidden_json_input");
input.value = JSON.stringify({"booking": booking_id, "name": host_name});
// clear out and highlist host
if(selected_host){
- selected_host.style['background-color'] = "white";
+ selected_host.classList.remove("selected");
}
selected_host = document.getElementById("booking_" + booking_id + "_host_" + host_name);
- selected_host.style['background-color'] = "lightgrey";
+ selected_host.classList.add("selected");
}
function draw_bookings(){
@@ -53,17 +83,20 @@ function draw_bookings(){
var heading = document.createElement("H3");
heading.appendChild(document.createTextNode("Booking " + booking_id));
booking.appendChild(heading);
- var desc = "start: " + booking_hosts[booking_id].start +
- " end: " + booking_hosts[booking_id].end +
- " purpose: " + booking_hosts[booking_id].purpose;
- booking.appendChild(document.createTextNode(desc));
+ booking.appendChild(document.createTextNode("start: " + booking_hosts[booking_id].start));
+ booking.appendChild(document.createElement("BR"));
+ booking.appendChild(document.createTextNode("end: " + booking_hosts[booking_id].end));
+ booking.appendChild(document.createElement("BR"));
+ booking.appendChild(document.createTextNode("purpose: " + booking_hosts[booking_id].purpose));
+ booking.appendChild(document.createElement("BR"));
+ booking.appendChild(document.createTextNode("hosts:"));
booking.id = "booking_" + booking_id;
booking.className = "booking";
var hosts = booking_hosts[booking_id].hosts;
for(var i=0; i<hosts.length; i++){
var host = document.createElement("DIV");
host.id = "booking_" + booking_id + "_host_" + hosts[i].name;
- host.className = "host";
+ host.classList.add("host");
host.appendChild(document.createTextNode(hosts[i].name));
var hostname = hosts[i].name;
host.booking = booking_id;
diff --git a/dashboard/src/templates/workflow/confirm.html b/dashboard/src/templates/workflow/confirm.html
index 555fa56..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");
@@ -66,19 +77,34 @@
req.open("POST", "/wf/workflow/finish/", false);
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 ) { parent.redirect_root(); } }
+ req.onreadystatechange = function() { if(req.readyState === 4 ) {
+ processResponseText(req.responseText);
+ }}
req.send(formData);
}
+ function submitForm()
+ {
+ var form = $("#confirmation_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 submitting confirmation"); }
+ req.onreadystatechange = function() { if(req.readyState === 4 ) { delete_manager(); } }
+ req.send(formData);
+ }
+
+
function formconfirm()
{
select.value = "True";
- document.getElementById("confirmation_form").submit();
+ submitForm();
}
function formcancel()
{
select.value = "False";
- document.getElementById("confirmation_form").submit();
+ submitForm();
}
var confirmed = {{bypassed|default:"false"}};
@@ -91,7 +117,20 @@
function fixVlans() {
document.getElementById("vlan_input").value = "True";
- document.getElementById("vlan_form").submit();
+ var form = $("#vlan_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 submitting form"); }
+ req.onreadystatechange = function() { //replaces current page with response
+ if(req.readyState === 4 ) {
+ var d = document.getElementById("vlan_warning").innerHTML = "";
+ document.getElementById("confirm_button").disabled = false;
+ document.getElementById("cancel_button").disabled = false;
+ }
+ }
+ req.send(formData);
}
var problem = {{vlan_warning|default:'false'}};
if(problem){
@@ -121,5 +160,4 @@ if(problem){
{% endblock element_messages %}
{% endblock content %}
{% block onleave %}
-//document.getElementById("confirmation_form").submit();
{% endblock %}
diff --git a/dashboard/src/templates/workflow/no_workflow.html b/dashboard/src/templates/workflow/no_workflow.html
index ff8aab3..0ac6549 100644
--- a/dashboard/src/templates/workflow/no_workflow.html
+++ b/dashboard/src/templates/workflow/no_workflow.html
@@ -1,7 +1,3 @@
-{% extends "base.html" %}
-{% load staticfiles %}
-
-{% block content %}
-<h3>If you would like to create a booking or a resource, please use the links on the sidebar or from the homepage</h3>
-<a href="/">Go Home</a>
-{% endblock content %}
+<script>
+ top.window.location.href='/';
+</script>
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 37eff27..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,118 +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_untouched
- {
- background: #98B0AF;
+ .step_valid::after {
+ content: " \2611";
+ color: #41ba78;
}
- .step_invalid
- {
- background: #CC3300;
+ .step_untouched::after {
+ content: " \2610";
}
- .step_valid
- {
- background: #0FD57D;
+ .iframe_div {
+ width: calc(100% - 450px);
+ margin-left: 70px;
+ height: calc(100vh - 155px);
+ position: absolute;
+ border: none;
}
- #viewport-iframe
- {
- height: calc(100vh - 450);
- }
+ .iframe_elem {
+ width: 100%;
+ height: calc(100vh - 155px);
+ border: none;
+ }
+ #breadcrumbs {
+ background-color: inherit;
+ }
-</style>
+ #breadcrumbs.breadcrumb>li {
+ border: 1px solid #cccccc;
+ border-left: none;
+ }
+
+ #breadcrumbs.breadcrumb>li:first-child {
+ border-left: 1px solid #cccccc;
+ }
+
+ #breadcrumbs.breadcrumb>li+li:before {
+ content: "";
+ width: 0;
+ margin: 0;
+ padding: 0;
+ }
+
+ #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;
+ }
-<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 btn go_btn go_back">Go Back</button>
+ .topcrumb.active > span {
+ background: #007bff;
+ color: white;
+ }
-<div class="options">
- <button class="btn" onclick="cancel_wf()">Cancel</button>
+ .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">
+<!-- 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);
@@ -134,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;
}
}
@@ -192,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);
@@ -202,183 +328,146 @@
});
}
- 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";
+ }
}
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);
- }
- //draw enough rows for all steps
- var depth = meta_json['max_depth'];
- for(var i=0; i<=depth; i++){
- var div = document.createElement("DIV");
- div.id = "row"+i;
- if(i<depth){
- div.style['margin-bottom'] = "7px";
- }
- if(i>0){
- div.style['margin-top'] = "7px";
- }
- container.appendChild(div);
- }
+ $("#topPagination").children().not(".page-control").remove();
draw_steps(meta_json);
}
- function draw_steps(meta_json){
- var all_relations = meta_json['relations'];
- var relations = [];
- var active_steps = [];
- var active_step = step;
- while(active_step < meta_json['steps'].length){
- active_steps.push(active_step);
- var index = meta_json['parents'][active_step];
- var relation = all_relations[index];
- relations.push(relation);
- active_step = relation['parent'];
- }
- var child_index = meta_json['children'][step];
- var my_children = all_relations[child_index];
- if(my_children){
- relations.push(my_children);
- }
- draw_relations(relations, meta_json, active_steps);
- }
-
- function draw_relations(relations, meta_json, active_steps){
- for(var i=0; i<relations.length; i++){
- var relation = relations[i];
- var children_container = document.createElement("DIV");
- children_container.style['display'] = "inline";
- children_container.style['margin'] = "3px";
- children_container.style['padding'] = "3px";
- console.log("meta_json: ");
- console.log(meta_json);
- for(var j=0; j<relation['children'].length; j++){
- var step_json = meta_json['steps'][relation['children'][j]];
- step_json['index'] = relation['children'][j];
- var active = active_steps.indexOf(step_json['index']) > -1;
- var step_button = create_step(meta_json['steps'][relation['children'][j]], active);
- children_container.appendChild(step_button);
- }
- var parent_div = document.getElementById("row" + relation['depth']);
- parent_div.appendChild(children_container);
+ 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"]);
+ $("#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";
- console.log(step_json['message']);
-
- } 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);
}
var step_number = step_json['index'];
- step_dom.onclick = function(){ go(step_number); }
- //TODO: background color and other style
return step_dom;
}
- function cancel_wf(){
- $.ajax({
- type: "POST",
- url: "/wf/manager/",
- data: {"cancel":"",},
- beforeSend: function(request) {
- request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val()
- );
- },
- success: redirect_root()
- });
+ 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.send(formData);
+ }
+
+ function refresh_iframe() {
+ req = new XMLHttpRequest();
+ url = "/wf/workflow/";
+ req.open("GET", url, true);
+ req.onload = function (e) {
+ var doc = document.getElementById("viewport-iframe").contentWindow.document;
+ doc.open();
+ doc.write(this.responseText);
+ doc.close();
+ }
+ req.send();
}
- function redirect_root()
- {
- window.location.replace('/');
+ function write_iframe(contents) {
+ document.getElementById("viewport-iframe").contentWindow.document.innerHTML = contents;
}
- function add_wf(type){
+ function redirect_root() {
+ window.location.replace('/wf/');
+ }
+
+ 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()
@@ -386,68 +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 style="display: none;" id="workflow_pop_form_div">
+ <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 cd12ab6..42372ce 100644
--- a/dashboard/src/workflow/booking_workflow.py
+++ b/dashboard/src/workflow/booking_workflow.py
@@ -8,209 +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_default_entry(self):
- return None
+ def alert_bundle_missing(self):
+ self.set_invalid("Please select a valid resource bundle")
- def get_context(self):
- context = super(Resource_Select, self).get_context()
- default = []
- chosen_bundle = None
- default_bundle = self.get_default_entry()
- if default_bundle:
- context['disabled'] = True
- chosen_bundle = default_bundle
- if chosen_bundle.id:
- default.append(chosen_bundle.id)
- else:
- default.append("repo bundle")
- else:
- chosen_bundle = self.repo_get(self.repo_key, False)
- if chosen_bundle:
- if chosen_bundle.id:
- default.append(chosen_bundle.id)
- else:
- default.append("repo bundle")
-
- bundle = default_bundle
- if not 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
-
- 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)
- 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)
+ qs = GenericResourceBundle.objects.filter(owner=user)
+ return qs
+ def get_page_context(self):
+ return {
+ 'select_type': 'resource',
+ 'select_type_title': 'Resource Bundle',
+ 'addable_type_num': 1
+ }
-class Booking_Resource_Select(Resource_Select):
+ 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)
- def __init__(self, *args, **kwargs):
- super(Booking_Resource_Select, self).__init__(*args, **kwargs)
- self.repo_key = self.repo.BOOKING_SELECTED_GRB
- self.confirm_key = "booking"
- def get_default_entry(self):
- default = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("bundle")
- mine = self.repo_get(self.repo_key)
- if mine:
- return None
- try:
- config_bundle = self.repo_get(self.repo.BOOKING_MODELS)['booking'].config_bundle
- if default:
- return default # select created grb, even if preselected config bundle
- return config_bundle.bundle
- except:
- pass
- return default
+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)
- bundle = None
- try:
- id = int(bundle_json[0]['id'])
- bundle = ConfigBundle.objects.get(id=id)
- except ValueError:
- bundle = self.repo_get(self.repo.CONFIG_MODELS).get("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_context(self):
- context = super(SWConfig_Select, self).get_context()
- default = []
- bundle = None
- chosen_bundle = None
- created_bundle = self.repo_get(self.repo.CONFIG_MODELS, {}).get("bundle", False)
- 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("repo bundle")
- bundle = created_bundle
- context['disabled'] = True
- edit = self.repo_get(self.repo.EDIT, False)
- grb = self.repo_get(self.repo.BOOKING_SELECTED_GRB)
- context['form'] = SWConfigSelectorForm(chosen_software=default, bundle=bundle, edit=edit, resource=grb)
- return context
+ def get_page_context(self):
+ return {
+ 'select_type': 'swconfig',
+ 'select_type_title': 'Software Config',
+ 'addable_type_num': 2
+ }
+
+
+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):
@@ -235,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, {})
@@ -274,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:
@@ -291,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 feb32f2..ee44ecd 100644
--- a/dashboard/src/workflow/forms.py
+++ b/dashboard/src/workflow/forms.py
@@ -9,40 +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.initial = attrs.get("initial", [])
- super(SearchableSelectMultipleWidget, self).__init__(attrs)
+ super(SearchableSelectMultipleWidget, self).__init__()
def render(self, name, value, attrs=None, renderer=None):
@@ -59,127 +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
}
-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.
+ """
- 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'])
+ 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
- attrs = self.build_search_widget_attrs(chosen_resource, bundle, edit, queryset)
+ super().__init__(disabled=disabled, **kwargs)
- self.fields['generic_resource_bundle'] = forms.CharField(
- widget=SearchableSelectMultipleWidget(attrs=attrs)
- )
+ self.required = required
- 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
+ def clean(self, data):
+ data = data[0]
+ if not data:
+ if self.required:
+ raise ValidationError("Nothing was selected")
else:
- displayable['expanded_name'] = ""
- displayable['string'] = res.description
- displayable['id'] = res.id
- resources[res.id] = displayable
-
- if bundle:
- displayable = {}
- displayable['small_name'] = bundle.name
- displayable['expanded_name'] = "Current bundle"
- displayable['string'] = bundle.description
- displayable['id'] = "repo bundle"
- resources["repo bundle"] = displayable
- attrs = {
- 'set': resources,
- 'show_from_noentry': "true",
+ 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 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
+ }
- 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)
+ return items
+
+
+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
-
- if bundle:
- displayable = {}
- displayable['small_name'] = bundle.name
- displayable['expanded_name'] = "Current configuration"
- displayable['string'] = bundle.description
- displayable['id'] = "repo bundle"
- configs['repo bundle'] = displayable
-
- 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
- }
- 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):
@@ -188,195 +218,126 @@ 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
+ def __init__(self, *args, user_initial=[], owner=None, **kwargs):
+ super(BookingMetaForm, self).__init__(**kwargs)
- super(BookingMetaForm, 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['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():
+ 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'] = 1
- 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,
- 'items': items
+ 'display_objects': display_objects,
+ 'neighbors': neighbors,
+ 'filter_items': items
}
return context
@@ -384,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()
- attrs['selection_data'] = selection_data
+ attrs = FormUtils.getLabData(multiple_hosts=True)
self.fields['filter_field'] = MultipleSelectFilterField(
- widget=MultipleSelectFilterWidget(
- attrs=attrs
- )
+ widget=MultipleSelectFilterWidget(**attrs)
)
@@ -424,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):
@@ -461,9 +412,9 @@ class SnapshotHostSelectForm(forms.Form):
host = forms.CharField()
-class SnapshotMetaForm(forms.Form):
+class BasicMetaForm(forms.Form):
name = forms.CharField()
- description = forms.CharField()
+ description = forms.CharField(widget=forms.Textarea)
class ConfirmationForm(forms.Form):
@@ -475,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 966582c..6c6bd9a 100644
--- a/dashboard/src/workflow/models.py
+++ b/dashboard/src/workflow/models.py
@@ -10,6 +10,8 @@
from django.shortcuts import render
from django.contrib import messages
+from django.http import HttpResponse
+from django.utils import timezone
import yaml
import requests
@@ -17,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
@@ -26,13 +29,11 @@ from booking.models import Booking
class BookingAuthManager():
LFN_PROJECTS = ["opnfv"] # TODO
- def parse_url(self, info_url):
- """
- will return the PTL in the INFO file on success, or None
- """
+ def parse_github_url(self, url):
+ project_leads = []
try:
- parts = info_url.split("/")
- if parts[0].find("http") > -1: # the url include http(s)://
+ parts = url.split("/")
+ if "http" in parts[0]: # the url include http(s)://
parts = parts[2:]
if parts[-1] != "INFO.yaml":
return None
@@ -47,13 +48,94 @@ class BookingAuthManager():
info_file = requests.get(url, timeout=15).text
info_parsed = yaml.load(info_file)
ptl = info_parsed.get('project_lead')
- if not ptl:
+ if ptl:
+ project_leads.append(ptl)
+ sub_ptl = info_parsed.get("subproject_lead")
+ if sub_ptl:
+ project_leads.append(sub_ptl)
+
+ except Exception:
+ pass
+
+ return project_leads
+
+ def parse_gerrit_url(self, url):
+ project_leads = []
+ try:
+ halfs = url.split("?")
+ parts = halfs[0].split("/")
+ args = halfs[1].split(";")
+ if "http" in parts[0]: # the url include http(s)://
+ parts = parts[2:]
+ if "f=INFO.yaml" not in args:
+ return None
+ if "gerrit.opnfv.org" not in parts[0]:
return None
- return ptl
+ try:
+ i = args.index("a=blob")
+ args[i] = "a=blob_plain"
+ except ValueError:
+ pass
+ # recreate url
+ halfs[1] = ";".join(args)
+ halfs[0] = "/".join(parts)
+ # now to download and parse file
+ url = "https://" + "?".join(halfs)
+ info_file = requests.get(url, timeout=15).text
+ info_parsed = yaml.load(info_file)
+ ptl = info_parsed.get('project_lead')
+ if ptl:
+ project_leads.append(ptl)
+ sub_ptl = info_parsed.get("subproject_lead")
+ if sub_ptl:
+ project_leads.append(sub_ptl)
except Exception:
return None
+ return project_leads
+
+ def parse_opnfv_git_url(self, url):
+ project_leads = []
+ try:
+ parts = url.split("/")
+ if "http" in parts[0]: # the url include http(s)://
+ parts = parts[2:]
+ if "INFO.yaml" not in parts[-1]:
+ return None
+ if "git.opnfv.org" not in parts[0]:
+ return None
+ if parts[-2] == "tree":
+ parts[-2] = "plain"
+ # now to download and parse file
+ url = "https://" + "/".join(parts)
+ info_file = requests.get(url, timeout=15).text
+ info_parsed = yaml.load(info_file)
+ ptl = info_parsed.get('project_lead')
+ if ptl:
+ project_leads.append(ptl)
+ sub_ptl = info_parsed.get("subproject_lead")
+ if sub_ptl:
+ project_leads.append(sub_ptl)
+
+ except Exception:
+ return None
+
+ return project_leads
+
+ def parse_url(self, info_url):
+ """
+ will return the PTL in the INFO file on success, or None
+ """
+ if "github" in info_url:
+ return self.parse_github_url(info_url)
+
+ if "gerrit.opnfv.org" in info_url:
+ return self.parse_gerrit_url(info_url)
+
+ if "git.opnfv.org" in info_url:
+ return self.parse_opnfv_git_url(info_url)
+
def booking_allowed(self, booking, repo):
"""
This is the method that will have to change whenever the booking policy changes in the Infra
@@ -61,23 +143,67 @@ 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.BOOKING_INFO_FILE)
- return ptl_info and ptl_info == booking.owner.userprofile.email_addr
+ ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE))
+ for ptl in ptl_info:
+ if ptl['email'] == booking.owner.userprofile.email_addr:
+ return True
+ return False
-class WorkflowStep(object):
+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
@@ -114,29 +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.BOOKING_SELECTED_GRB, 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 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()
@@ -145,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
@@ -164,9 +333,10 @@ class Confirmation_Step(WorkflowStep):
errors = self.flush_to_db()
if errors:
messages.add_message(request, messages.ERROR, "ERROR OCCURRED: " + errors)
- return render(request, self.template, context)
- messages.add_message(request, messages.SUCCESS, "Confirmed")
- return render(request, self.template, context)
+ else:
+ messages.add_message(request, messages.SUCCESS, "Confirmed")
+
+ return HttpResponse('')
elif data == "False":
context["bypassed"] = "true"
messages.add_message(request, messages.SUCCESS, "Canceled")
@@ -175,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.BOOKING_SELECTED_GRB, 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():
@@ -216,6 +355,8 @@ class Repository():
RESOURCE_SELECT = "resource_select"
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"
@@ -223,11 +364,11 @@ class Repository():
GRB_LAST_HOSTLIST = "grb_network_previous_hostlist"
BOOKING_FORMS = "booking_forms"
SWCONF_HOSTS = "swconf_hosts"
- SWCONF_SELECTED_GRB = "swconf_selected_grb_pk"
- BOOKING_SELECTED_GRB = "booking_selected_grb_pk"
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"
@@ -238,7 +379,24 @@ 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.
+ 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"
+
+ def get_child_defaults(self):
+ return_tuples = []
+ for key in [self.SELECTED_GRESOURCE_BUNDLE, self.SESSION_USER]:
+ return_tuples.append((key, self.el.get(key)))
+ return return_tuples
+
+ def set_defaults(self, defaults):
+ for key, value in defaults:
+ self.el[key] = value
+
def get(self, key, default, id):
+
self.add_get_history(key, id)
return self.el.get(key, default)
@@ -263,16 +421,33 @@ 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()
if errors:
return errors
+ else:
+ self.el[self.HAS_RESULT] = True
+ self.el[self.RESULT_KEY] = self.SELECTED_GRESOURCE_BUNDLE
+ return
if self.CONFIG_MODELS in self.el:
errors = self.make_software_config_bundle()
if errors:
return errors
+ else:
+ self.el[self.HAS_RESULT] = True
+ 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()
@@ -290,6 +465,8 @@ class Repository():
if not booking_id:
return "SNAP, No booking ID provided"
booking = Booking.objects.get(pk=booking_id)
+ if booking.start > timezone.now() or booking.end < timezone.now():
+ return "Booking is not active"
name = self.el.get(self.SNAPSHOT_NAME)
if not name:
return "SNAP, no name provided"
@@ -305,6 +482,16 @@ class Repository():
image.owner = owner
image.host_type = host.profile
image.save()
+ try:
+ current_image = host.config.image
+ image.os = current_image.os
+ image.save()
+ except Exception:
+ 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]
@@ -334,45 +521,52 @@ 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"
else:
return "GRB no models given. CODE:0x0001"
- self.el[self.VALIDATED_MODEL_GRB] = bundle
+ 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:
@@ -403,26 +597,30 @@ class Repository():
else:
pass
- self.el[self.VALIDATED_MODEL_CONFIG] = bundle
+ self.el[self.RESULT] = bundle
return False
def make_booking(self):
models = self.el[self.BOOKING_MODELS]
owner = self.el[self.SESSION_USER]
- if self.BOOKING_SELECTED_GRB in self.el:
- selected_grb = self.el[self.BOOKING_SELECTED_GRB]
- else:
- return "BOOK, no selected resource. CODE:0x000e"
-
- if not self.reserve_vlans(selected_grb):
- return "BOOK, vlans not available"
-
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 self.SELECTED_CONFIG_BUNDLE not in self.el:
+ return "BOOK, no selected config bundle. CODE:0x001f"
+
+ booking.config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
+
if not booking.start:
return "BOOK, booking has no start. CODE:0x0010"
if not booking.end:
@@ -443,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)
@@ -459,6 +656,12 @@ class Repository():
booking.collaborators.add(collaborator)
try:
+ booking.pdf = PDFTemplater.makePDF(booking)
+ booking.save()
+ except Exception as e:
+ return "BOOK, failed to create Pod Desriptor File: " + str(e)
+
+ try:
JobFactory.makeCompleteJob(booking)
except Exception as e:
return "BOOK, serializing for api generated exception: " + str(e) + " CODE:0xFFFF"
@@ -468,32 +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 712c92b..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
@@ -133,12 +117,11 @@ class Define_Hardware(WorkflowStep):
if self.form.is_valid():
self.update_models(self.form.cleaned_data)
self.update_confirmation()
- self.metastep.set_valid("Step Completed")
+ 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)
@@ -168,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
@@ -224,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:
@@ -252,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):
@@ -390,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 4ddc397..5414784 100644
--- a/dashboard/src/workflow/snapshot_workflow.py
+++ b/dashboard/src/workflow/snapshot_workflow.py
@@ -8,13 +8,13 @@
##############################################################################
-import datetime
+from django.utils import timezone
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):
@@ -27,7 +27,7 @@ class Select_Host_Step(WorkflowStep):
context = super(Select_Host_Step, self).get_context()
context['form'] = SnapshotHostSelectForm()
booking_hosts = {}
- now = datetime.datetime.now()
+ now = timezone.now()
user = self.repo_get(self.repo.SESSION_USER)
bookings = Booking.objects.filter(start__lt=now, end__gt=now, owner=user)
for booking in bookings:
@@ -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)
@@ -87,11 +87,18 @@ class Image_Meta_Step(WorkflowStep):
def get_context(self):
context = super(Image_Meta_Step, self).get_context()
- context['form'] = SnapshotMetaForm()
+ name = self.repo_get(self.repo.SNAPSHOT_NAME, False)
+ desc = self.repo_get(self.repo.SNAPSHOT_DESC, False)
+ form = None
+ if name and desc:
+ form = BasicMetaForm(initial={"name": name, "description": desc})
+ else:
+ 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)
@@ -105,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 26ade22..0c558fc 100644
--- a/dashboard/src/workflow/sw_bundle_workflow.py
+++ b/dashboard/src/workflow/sw_bundle_workflow.py
@@ -11,33 +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.SWCONF_SELECTED_GRB
- self.confirm_key = "configuration"
-
- def get_default_entry(self):
- booking_grb = self.repo_get(self.repo.BOOKING_SELECTED_GRB)
- if booking_grb:
- return booking_grb
- created_grb = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("bundle", None)
- return created_grb
-
- 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):
@@ -46,52 +26,61 @@ 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.SWCONF_SELECTED_GRB).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:
- grb = self.repo_get(self.repo.SWCONF_SELECTED_GRB, False)
+ grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False)
if not grb:
return []
if grb.id:
@@ -102,16 +91,16 @@ class Define_Software(WorkflowStep):
def get_context(self):
context = super(Define_Software, self).get_context()
- grb = self.repo_get(self.repo.SWCONF_SELECTED_GRB, False)
+ grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE, False)
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
@@ -122,58 +111,51 @@ 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()
+ 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.SWCONF_SELECTED_GRB)
- 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']
+ 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)
+ 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)
if "configuration" not in confirm:
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 e5ef5c6..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,12 +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):
- try:
+ manager = attempt_auth(request)
+
+ if not manager:
+ return HttpResponseGone("No session found that relates to current request")
+
+ not_last_workflow, result = manager.pop_workflow()
+
+ 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):
@@ -43,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)
@@ -70,7 +98,8 @@ def manager_view(request):
logger.debug("edit found")
manager.add_workflow(workflow_type=request.POST.get('edit'), edit_object=int(request.POST.get('edit_id')))
elif request.POST.get('cancel') is not None:
- del ManagerTracker.managers[request.session['manager_session']]
+ if not manager.pop_workflow():
+ del ManagerTracker.managers[request.session['manager_session']]
return manager.status(request)
diff --git a/dashboard/src/workflow/workflow_factory.py b/dashboard/src/workflow/workflow_factory.py
index 9a42d86..03c8126 100644
--- a/dashboard/src/workflow/workflow_factory.py
+++ b/dashboard/src/workflow/workflow_factory.py
@@ -8,10 +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.opnfv_workflow import Pick_Installer, Assign_Network_Roles, Assign_Host_Roles, OPNFV_Resource_Select, MetaInfo
+from workflow.models import Confirmation_Step
import uuid
@@ -19,38 +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 MetaRelation(object):
- def __init__(self, *args, **kwargs):
- self.color = "#cccccc"
- self.parent = 0
- self.children = []
- self.depth = -1
-
- def to_json(self):
- return {
- 'color': self.color,
- 'parent': self.parent,
- 'children': self.children,
- 'depth': self.depth,
- }
-
-
class MetaStep(object):
UNTOUCHED = 0
@@ -69,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()
@@ -93,11 +64,19 @@ class MetaStep(object):
return self.id.int != other.id.int
+class Workflow(object):
+ def __init__(self, steps, repository):
+ self.repository = repository
+ self.steps = steps
+ self.active_index = 0
+
+
class WorkflowFactory():
booking_steps = [
Booking_Resource_Select,
SWConfig_Select,
- Booking_Meta
+ Booking_Meta,
+ OPNFV_Select,
]
resource_steps = [
@@ -114,7 +93,15 @@ class WorkflowFactory():
snapshot_steps = [
Select_Host_Step,
- Image_Meta_Step
+ 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):
@@ -123,14 +110,19 @@ class WorkflowFactory():
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 = self.conjure(workflow_type, repo)
+ c_step = self.make_step(Confirmation_Step, repo)
+ steps.append(c_step)
+ return Workflow(steps, repo)
def make_steps(self, step_types, repository):
- repository.el['steps'] += len(step_types)
steps = []
for step_type in step_types:
steps.append(self.make_step(step_type, repository))
@@ -140,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 95fefbf..80b8a67 100644
--- a/dashboard/src/workflow/workflow_manager.py
+++ b/dashboard/src/workflow/workflow_manager.py
@@ -10,11 +10,9 @@
from django.http import JsonResponse
-import random
-
from booking.models import Booking
-from workflow.workflow_factory import WorkflowFactory, MetaStep, MetaRelation
-from workflow.models import Repository, Confirmation_Step
+from workflow.workflow_factory import WorkflowFactory
+from workflow.models import Repository
from resource_inventory.models import (
GenericResourceBundle,
ConfigBundle,
@@ -27,86 +25,64 @@ logger = logging.getLogger(__name__)
class SessionManager():
+ def active_workflow(self):
+ return self.workflows[-1]
def __init__(self, request=None):
- self.repository = Repository()
- self.repository.el[self.repository.SESSION_USER] = request.user
- self.repository.el['active_step'] = 0
- self.steps = []
+ self.workflows = []
+
+ self.owner = request.user
+
self.factory = WorkflowFactory()
- c_step = WorkflowFactory().make_step(Confirmation_Step, self.repository)
- self.steps.append(c_step)
- metaconfirm = MetaStep()
- metaconfirm.index = 0
- metaconfirm.short_title = "confirm"
- self.repository.el['steps'] = 1
- self.metaworkflow = None
- self.metaworkflows = []
- self.metarelations = []
- self.relationreverselookup = {}
- self.initialized = False
- self.active_index = 0
- self.step_meta = [metaconfirm]
- self.relation_depth = 0
+
+ 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)
- factory_steps, meta_info = self.factory.conjure(workflow_type=workflow_type, repo=self.repository)
- offset = len(meta_info)
- for relation in self.metarelations:
- if relation.depth > self.relation_depth:
- self.relation_depth = relation.depth
- if relation.parent >= self.repository.el['active_step']:
- relation.parent += offset
- for i in range(0, len(relation.children)):
- if relation.children[i] >= self.repository.el['active_step']:
- relation.children[i] += offset
- self.step_meta[self.active_index:self.active_index] = meta_info
- self.steps[self.active_index:self.active_index] = factory_steps
-
- if self.initialized:
- relation = MetaRelation()
- relation.parent = self.repository.el['active_step'] + offset
- relation.depth = self.relationreverselookup[self.step_meta[relation.parent]].depth + 1
- if relation.depth > self.relation_depth:
- self.relation_depth = relation.depth
- for i in range(self.repository.el['active_step'], offset + self.repository.el['active_step']):
- relation.children.append(i)
- self.relationreverselookup[self.step_meta[i]] = relation
- relation.color = "#%06x" % random.randint(0, 0xFFFFFF)
- self.metarelations.append(relation)
- else:
- relation = MetaRelation()
- relation.depth = 0
- relation.parent = 500000000000
- for i in range(0, len(self.step_meta)):
- relation.children.append(i)
- self.relationreverselookup[self.step_meta[i]] = relation
- self.metarelations.append(relation)
- self.initialized = True
+
+ repo = Repository()
+ if(len(self.workflows) >= 1):
+ defaults = self.workflows[-1].repository.get_child_defaults()
+ repo.set_defaults(defaults)
+ repo.el[repo.HAS_RESULT] = False
+ repo.el[repo.SESSION_USER] = self.owner
+ repo.el[repo.SESSION_MANAGER] = self
+ self.workflows.append(
+ self.factory.create_workflow(
+ workflow_type=workflow_type,
+ repo=repo
+ )
+ )
+
+ def pop_workflow(self):
+ 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:
- steps = []
- for step in self.step_meta:
- steps.append(step.to_json())
- parents = {}
- children = {}
+ meta_json = []
+ for step in self.active_workflow().steps:
+ meta_json.append(step.to_json())
responsejson = {}
- responsejson["steps"] = steps
- responsejson["active"] = self.repository.el['active_step']
- responsejson["relations"] = []
- i = 0
- for relation in self.metarelations:
- responsejson["relations"].append(relation.to_json())
- children[relation.parent] = i
- for child in relation.children:
- parents[child] = i
- i += 1
- responsejson['max_depth'] = self.relation_depth
- responsejson['parents'] = parents
- responsejson['children'] = children
+ 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:
pass
@@ -115,16 +91,35 @@ class SessionManager():
# filter out when a step needs to handle post/form data
# if 'workflow' in post data, this post request was meant for me, not step
if request.method == 'POST' and request.POST.get('workflow', None) is None:
- return self.steps[self.active_index].post_render(request)
- return self.steps[self.active_index].render(request)
+ return self.active_workflow().steps[self.active_workflow().active_index].post_render(request)
+ return self.active_workflow().steps[self.active_workflow().active_index].render(request)
def post_render(self, request):
- return self.steps[self.active_index].post_render(request)
-
- def goto(self, num, **kwargs):
- self.repository.el['active_step'] = int(num)
- self.active_index = int(num)
- # TODO: change to include some checking
+ return self.active_workflow().steps[self.active_workflow().active_index].post_render(request)
+
+ 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
@@ -142,29 +137,28 @@ class SessionManager():
def prefill_booking(self, booking):
models = self.make_booking_models(booking)
confirmation = self.make_booking_confirm(booking)
- self.repository.el[self.repository.BOOKING_MODELS] = models
- self.repository.el[self.repository.CONFIRMATION] = confirmation
- self.repository.el[self.repository.GRESOURCE_BUNDLE_MODELS] = self.make_grb_models(booking.resource.template)
- self.repository.el[self.repository.BOOKING_SELECTED_GRB] = self.make_grb_models(booking.resource.template)['bundle']
- self.repository.el[self.repository.CONFIG_MODELS] = self.make_config_models(booking.config_bundle)
+ self.active_workflow().repository.el[self.active_workflow().repository.BOOKING_MODELS] = models
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirmation
+ self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = self.make_grb_models(booking.resource.template)
+ self.active_workflow().repository.el[self.active_workflow().repository.SELECTED_GRESOURCE_BUNDLE] = self.make_grb_models(booking.resource.template)['bundle']
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = self.make_config_models(booking.config_bundle)
def prefill_resource(self, resource):
models = self.make_grb_models(resource)
confirm = self.make_grb_confirm(resource)
- self.repository.el[self.repository.GRESOURCE_BUNDLE_MODELS] = models
- self.repository.el[self.repository.CONFIRMATION] = confirm
+ self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = models
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm
def prefill_config(self, config):
models = self.make_config_models(config)
confirm = self.make_config_confirm(config)
- self.repository.el[self.repository.CONFIG_MODELS] = models
- self.repository.el[self.repository.CONFIRMATION] = confirm
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = models
+ self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm
grb_models = self.make_grb_models(config.bundle)
- self.repository.el[self.repository.GRESOURCE_BUNDLE_MODELS] = grb_models
- self.repository.el[self.repository.SWCONF_SELECTED_GRB] = config.bundle
+ self.active_workflow().repository.el[self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS] = grb_models
def make_grb_models(self, resource):
- models = self.repository.el.get(self.repository.GRESOURCE_BUNDLE_MODELS, {})
+ models = self.active_workflow().repository.el.get(self.active_workflow().repository.GRESOURCE_BUNDLE_MODELS, {})
models['hosts'] = []
models['bundle'] = resource
models['interfaces'] = {}
@@ -181,7 +175,7 @@ class SessionManager():
return models
def make_grb_confirm(self, resource):
- confirm = self.repository.el.get(self.repository.CONFIRMATION, {})
+ confirm = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIRMATION, {})
confirm['resource'] = {}
confirm['resource']['hosts'] = []
confirm['resource']['lab'] = resource.lab.lab_user.username
@@ -190,7 +184,7 @@ class SessionManager():
return confirm
def make_config_models(self, config):
- models = self.repository.el.get(self.repository.CONFIG_MODELS, {})
+ models = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIG_MODELS, {})
models['bundle'] = config
models['host_configs'] = []
for host_conf in HostConfiguration.objects.filter(bundle=config):
@@ -199,7 +193,7 @@ class SessionManager():
return models
def make_config_confirm(self, config):
- confirm = self.repository.el.get(self.repository.CONFIRMATION, {})
+ confirm = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIRMATION, {})
confirm['configuration'] = {}
confirm['configuration']['hosts'] = []
confirm['configuration']['name'] = config.name
@@ -213,7 +207,7 @@ class SessionManager():
return confirm
def make_booking_models(self, booking):
- models = self.repository.el.get(self.repository.BOOKING_MODELS, {})
+ models = self.active_workflow().repository.el.get(self.active_workflow().repository.BOOKING_MODELS, {})
models['booking'] = booking
models['collaborators'] = []
for user in booking.collaborators.all():
@@ -221,7 +215,7 @@ class SessionManager():
return models
def make_booking_confirm(self, booking):
- confirm = self.repository.el.get(self.repository.CONFIRMATION, {})
+ confirm = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIRMATION, {})
confirm['booking'] = {}
confirm['booking']['length'] = (booking.end - booking.start).days
confirm['booking']['project'] = booking.project
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
diff --git a/dashboard/tox.ini b/dashboard/tox.ini
new file mode 100644
index 0000000..3d01e10
--- /dev/null
+++ b/dashboard/tox.ini
@@ -0,0 +1,21 @@
+[tox]
+envlist = flake8
+skipsdist = True
+
+[testenv]
+deps = -rrequirements.txt
+basepython = python3
+
+# Store flake8 config here intead of .flake8
+[flake8]
+ignore =
+ # Ignore 'line-too-long' warnings
+ E501
+exclude =
+ src/manage.py
+
+[testenv:flake8]
+deps =
+ -rrequirements.txt
+ flake8
+commands = flake8 src/