aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--__init__.py8
-rw-r--r--src/__init__.py8
-rw-r--r--src/account/tests/test_general.py2
-rw-r--r--src/api/models.py46
-rw-r--r--src/api/tests/test_serializers.py229
-rw-r--r--src/api/urls.py4
-rw-r--r--src/api/views.py23
-rw-r--r--src/booking/quick_deployer.py213
-rw-r--r--src/booking/tests/test_models.py7
-rw-r--r--src/booking/tests/test_quick_booking.py155
-rw-r--r--src/dashboard/exceptions.py4
-rw-r--r--src/dashboard/testing_utils.py324
-rw-r--r--src/notifier/manager.py4
-rw-r--r--src/notifier/migrations/0003_auto_20190123_1741.py23
-rw-r--r--src/notifier/migrations/0004_auto_20190124_2115.py23
-rw-r--r--src/notifier/migrations/0005_auto_20190306_1616.py18
-rw-r--r--src/notifier/models.py4
-rw-r--r--src/notifier/views.py39
-rw-r--r--src/resource_inventory/migrations/0007_auto_20190306_1616.py31
-rw-r--r--src/resource_inventory/migrations/0008_host_remote_management.py19
-rw-r--r--src/resource_inventory/models.py21
-rw-r--r--src/resource_inventory/pdf_templater.py173
-rw-r--r--src/resource_inventory/resource_manager.py115
-rw-r--r--src/templates/dashboard/idf.yaml52
-rw-r--r--src/templates/dashboard/pdf.yaml175
-rw-r--r--src/templates/notifier/inbox.html34
-rw-r--r--src/templates/notifier/notification.html65
-rw-r--r--src/templates/workflow/viewport-base.html1
-rw-r--r--src/workflow/models.py3
-rwxr-xr-xtest.sh2
30 files changed, 1286 insertions, 539 deletions
diff --git a/__init__.py b/__init__.py
deleted file mode 100644
index b6fef6c..0000000
--- a/__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/src/__init__.py b/src/__init__.py
deleted file mode 100644
index b6fef6c..0000000
--- a/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/src/account/tests/test_general.py b/src/account/tests/test_general.py
index 57ad291..3fb52b0 100644
--- a/src/account/tests/test_general.py
+++ b/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/src/api/models.py b/src/api/models.py
index 30f0f75..b35adf2 100644
--- a/src/api/models.py
+++ b/src/api/models.py
@@ -11,6 +11,7 @@
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
import json
import uuid
@@ -21,7 +22,8 @@ from resource_inventory.models import (
HostProfile,
Host,
Image,
- Interface
+ Interface,
+ RemoteInfo
)
@@ -60,6 +62,32 @@ 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()
+ return {"status": "success"}
+
def get_profile(self):
prof = {}
prof['name'] = self.lab.name
@@ -88,6 +116,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}
diff --git a/src/api/tests/test_serializers.py b/src/api/tests/test_serializers.py
deleted file mode 100644
index c1fa5af..0000000
--- a/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/src/api/urls.py b/src/api/urls.py
index 50cc6ac..d18a04d 100644
--- a/src/api/urls.py
+++ b/src/api/urls.py
@@ -39,6 +39,8 @@ from api.views import (
new_jobs,
current_jobs,
done_jobs,
+ update_host_bmc,
+ lab_host,
GenerateTokenView
)
@@ -51,6 +53,8 @@ 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>/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/src/api/views.py b/src/api/views.py
index c72c85c..a56dcfe 100644
--- a/src/api/views.py
+++ b/src/api/views.py
@@ -54,6 +54,16 @@ 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 lab_status(request, lab_name=""):
lab_token = request.META.get('HTTP_AUTH_TOKEN')
lab_manager = LabManagerTracker.get(lab_name, lab_token)
@@ -62,6 +72,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)
@@ -93,6 +115,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/src/booking/quick_deployer.py b/src/booking/quick_deployer.py
index d838de9..8a81d18 100644
--- a/src/booking/quick_deployer.py
+++ b/src/booking/quick_deployer.py
@@ -33,11 +33,14 @@ from resource_inventory.models import (
OPNFVConfig
)
from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
+from notifier.manager import NotificationHandler
from booking.models import Booking
from dashboard.exceptions import (
InvalidHostnameException,
ResourceAvailabilityException,
- ModelValidationException
+ ModelValidationException,
+ BookingLengthException
)
from api.models import JobFactory
@@ -87,22 +90,8 @@ class NoRemainingPublicNetwork(Exception):
pass
-def create_from_form(form, request):
- quick_booking_id = str(uuid.uuid4())
-
- host_field = form.cleaned_data['filter_field']
- host_json = json.loads(host_field)
- purpose_field = form.cleaned_data['purpose']
- project_field = form.cleaned_data['project']
- users_field = form.cleaned_data['users']
- host_name = form.cleaned_data['hostname']
- length = form.cleaned_data['length']
-
- image = form.cleaned_data['image']
- scenario = form.cleaned_data['scenario']
- installer = form.cleaned_data['installer']
-
- # get all initial info we need to validate
+def parse_host_field(host_field_contents):
+ host_json = json.loads(host_field_contents)
lab_dict = host_json['labs'][0]
lab_id = list(lab_dict.keys())[0]
lab_user_id = int(lab_id.split("_")[-1])
@@ -114,110 +103,185 @@ def create_from_form(form, request):
profile = HostProfile.objects.get(id=profile_id)
# check validity of field data before trying to apply to models
+ if len(host_json['labs']) != 1:
+ raise NoLabSelectedError("No lab was selected")
if not lab:
raise LabDNE("Lab with provided ID does not exist")
if not profile:
raise HostProfileDNE("Host type with provided ID does not exist")
- # check that hostname is valid
- if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", host_name):
- raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
- # check that image os is compatible with installer
- if installer in image.os.sup_installers.all():
- # if installer not here, we can omit that and not check for scenario
- if not scenario:
- raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
- if scenario not in installer.sup_scenarios.all():
- raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
- if image.from_lab != lab:
- raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
- if image.host_type != profile:
- raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
- if not image.public and image.owner != request.user:
- raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
+ return lab, profile
+
- # check if host type is available
- # ResourceManager.getInstance().acquireHost(ghost, lab.name)
+def check_available_matching_host(lab, hostprofile):
available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
- if profile not in available_host_types:
+ if hostprofile not in available_host_types:
# TODO: handle deleting generic resource in this instance along with grb
raise HostNotAvailable("Could not book selected host due to changed availability. Try again later")
- # check if any hosts with profile at lab are still available
- hostset = Host.objects.filter(lab=lab, profile=profile).filter(booked=False).filter(working=True)
- if not hostset.first():
+ hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
+ if not hostset.exists():
raise HostNotAvailable("Couldn't find any matching unbooked hosts")
- # generate GenericResourceBundle
- if len(host_json['labs']) != 1:
- raise NoLabSelectedError("No lab was selected")
+ return True
+
- grbundle = GenericResourceBundle(owner=request.user)
+def generate_grb(owner, lab, common_id):
+ grbundle = GenericResourceBundle(owner=owner)
grbundle.lab = lab
- grbundle.name = "grbundle for quick booking with uid " + quick_booking_id
+ grbundle.name = "grbundle for quick booking with uid " + common_id
grbundle.description = "grbundle created for quick-deploy booking"
grbundle.save()
- # generate GenericResource, GenericHost
- gresource = GenericResource(bundle=grbundle, name=host_name)
+ return grbundle
+
+
+def generate_gresource(bundle, hostname):
+ if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
+ raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
+ gresource = GenericResource(bundle=bundle, name=hostname)
gresource.save()
+ return gresource
+
+
+def generate_ghost(generic_resource, host_profile):
ghost = GenericHost()
- ghost.resource = gresource
- ghost.profile = profile
+ ghost.resource = generic_resource
+ ghost.profile = host_profile
ghost.save()
- # generate config bundle
+ return ghost
+
+
+def generate_config_bundle(owner, common_id, grbundle):
cbundle = ConfigBundle()
- cbundle.owner = request.user
- cbundle.name = "configbundle for quick booking with uid " + quick_booking_id
+ cbundle.owner = owner
+ cbundle.name = "configbundle for quick booking with uid " + common_id
cbundle.description = "configbundle created for quick-deploy booking"
cbundle.bundle = grbundle
cbundle.save()
- # generate OPNFVConfig pointing to cbundle
- if installer:
- opnfvconfig = OPNFVConfig()
- opnfvconfig.scenario = scenario
- opnfvconfig.installer = installer
- opnfvconfig.bundle = cbundle
- opnfvconfig.save()
+ return cbundle
+
+
+def generate_opnfvconfig(scenario, installer, config_bundle):
+ opnfvconfig = OPNFVConfig()
+ opnfvconfig.scenario = scenario
+ opnfvconfig.installer = installer
+ opnfvconfig.bundle = config_bundle
+ opnfvconfig.save()
+
+ return opnfvconfig
+
- # generate HostConfiguration pointing to cbundle
+def generate_hostconfig(generic_host, image, config_bundle):
hconf = HostConfiguration()
- hconf.host = ghost
+ hconf.host = generic_host
hconf.image = image
- hconf.opnfvRole = OPNFVRole.objects.get(name="Jumphost")
- if not hconf.opnfvRole:
- raise OPNFVRoleDNE("No jumphost role was found")
- hconf.bundle = cbundle
+
+ opnfvrole = OPNFVRole.objects.get(name="Jumphost")
+ if not opnfvrole:
+ raise OPNFVRoleDNE("No jumphost role was found.")
+
+ hconf.opnfvRole = opnfvrole
+ hconf.bundle = config_bundle
hconf.save()
+ return hconf
+
+
+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 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_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)
+
+ # if no installer provided, just create blank host
+ if installer:
+ generate_opnfvconfig(scenario, installer, cbundle)
+
+ generate_hostconfig(ghost, image, cbundle)
+
# construct generic interfaces
- for interface_profile in profile.interfaceprofile.all():
+ for interface_profile in host_profile.interfaceprofile.all():
generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
generic_interface.save()
- ghost.save()
# get vlan, assign to first interface
publicnetwork = lab.vlan_manager.get_public_vlan()
- publicvlan = publicnetwork.vlan
if not publicnetwork:
raise NoRemainingPublicNetwork("No public networks were available for your pod")
+ publicvlan = publicnetwork.vlan
lab.vlan_manager.reserve_public_vlan(publicvlan)
vlan = Vlan.objects.create(vlan_id=publicvlan, tagged=False, public=True)
vlan.save()
+
ghost.generic_interfaces.first().vlans.add(vlan)
ghost.generic_interfaces.first().save()
# generate resource bundle
- try:
- resource_bundle = ResourceManager.getInstance().convertResourceBundle(grbundle, config=cbundle)
- except ResourceAvailabilityException:
- raise ResourceAvailabilityException("Requested resources not available")
- except ModelValidationException:
- raise ModelValidationException("Encountered error while saving grbundle")
+ resource_bundle = generate_resource_bundle(grbundle, cbundle)
# generate booking
booking = Booking()
@@ -228,7 +292,7 @@ def create_from_form(form, request):
booking.start = timezone.now()
booking.end = timezone.now() + timedelta(days=int(length))
booking.resource = resource_bundle
- booking.pdf = ResourceManager().makePDF(booking.resource)
+ booking.pdf = PDFTemplater.makePDF(booking.resource)
booking.config_bundle = cbundle
booking.save()
users_field = users_field[2:-2]
@@ -241,6 +305,7 @@ def create_from_form(form, request):
# generate job
JobFactory.makeCompleteJob(booking)
+ NotificationHandler.notify_new_booking(booking)
def drop_filter(user):
diff --git a/src/booking/tests/test_models.py b/src/booking/tests/test_models.py
index c7fb25d..6170295 100644
--- a/src/booking/tests/test_models.py
+++ b/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/src/booking/tests/test_quick_booking.py b/src/booking/tests/test_quick_booking.py
new file mode 100644
index 0000000..936a9a5
--- /dev/null
+++ b/src/booking/tests/test_quick_booking.py
@@ -0,0 +1,155 @@
+##############################################################################
+# 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 (
+ instantiate_host,
+ instantiate_user,
+ instantiate_userprofile,
+ instantiate_lab,
+ instantiate_installer,
+ instantiate_image,
+ instantiate_scenario,
+ instantiate_os,
+ make_hostprofile_set,
+ instantiate_opnfvrole,
+ instantiate_publicnet,
+)
+# from dashboard import test_utils
+
+
+class QuickBookingValidFormTestCase(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.loginuser = instantiate_user(False, username="newtestuser", password="testpassword")
+ instantiate_userprofile(cls.loginuser, True)
+
+ lab_user = instantiate_user(True)
+ cls.lab = instantiate_lab(lab_user)
+
+ cls.host_profile = make_hostprofile_set(cls.lab)
+ cls.scenario = instantiate_scenario()
+ cls.installer = instantiate_installer([cls.scenario])
+ os = instantiate_os([cls.installer])
+ cls.image = instantiate_image(cls.lab, 1, cls.loginuser, os, cls.host_profile)
+ cls.host = instantiate_host(cls.host_profile, cls.lab)
+ cls.role = instantiate_opnfvrole()
+ cls.pubnet = instantiate_publicnet(10, cls.lab)
+
+ cls.lab_selected = 'lab_' + str(cls.lab.lab_user.id) + '_selected'
+ cls.host_selected = 'host_' + str(cls.host_profile.id) + '_selected'
+
+ cls.post_data = cls.build_post_data()
+
+ cls.client = Client()
+
+ @classmethod
+ def build_post_data(cls):
+ post_data = {}
+ post_data['filter_field'] = '{"hosts":[{"host_' + str(cls.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(cls.lab.lab_user.id) + '":"true"}]}'
+ post_data['purpose'] = 'purposefieldcontentstring'
+ post_data['project'] = 'projectfieldcontentstring'
+ post_data['length'] = '3'
+ post_data['ignore_this'] = 1
+ post_data['users'] = ''
+ post_data['hostname'] = 'hostnamefieldcontentstring'
+ post_data['image'] = str(cls.image.id)
+ post_data['installer'] = str(cls.installer.id)
+ post_data['scenario'] = str(cls.scenario.id)
+ return post_data
+
+ 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.loginuser.username, password="testpassword")
+
+ def is_valid_booking(self, booking):
+ self.assertEqual(booking.owner, self.loginuser)
+ self.assertEqual(booking.purpose, 'purposefieldcontentstring')
+ self.assertEqual(booking.project, 'projectfieldcontentstring')
+ delta = booking.end - booking.start
+ delta -= datetime.timedelta(days=3)
+ self.assertLess(delta, datetime.timedelta(minutes=1))
+
+ resourcebundle = booking.resource
+ configbundle = booking.config_bundle
+
+ self.assertEqual(self.installer, configbundle.opnfv_config.first().installer)
+ self.assertEqual(self.scenario, configbundle.opnfv_config.first().scenario)
+ self.assertEqual(resourcebundle.template.getHosts()[0].profile, self.host_profile)
+ self.assertEqual(resourcebundle.template.getHosts()[0].resource.name, 'hostnamefieldcontentstring')
+
+ return True
+
+ 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.assertTrue(self.is_valid_booking(booking))
+
+ def test_with_valid_form(self):
+ response = self.post()
+
+ self.assertEqual(response.status_code, 200)
+ booking = Booking.objects.first()
+ self.assertIsNotNone(booking)
+ self.assertTrue(self.is_valid_booking(booking))
diff --git a/src/dashboard/exceptions.py b/src/dashboard/exceptions.py
index 9c16a06..7111bf8 100644
--- a/src/dashboard/exceptions.py
+++ b/src/dashboard/exceptions.py
@@ -50,3 +50,7 @@ class InvalidVlanConfigurationException(Exception):
class NetworkExistsException(Exception):
pass
+
+
+class BookingLengthException(Exception):
+ pass
diff --git a/src/dashboard/testing_utils.py b/src/dashboard/testing_utils.py
new file mode 100644
index 0000000..e98b5e6
--- /dev/null
+++ b/src/dashboard/testing_utils.py
@@ -0,0 +1,324 @@
+##############################################################################
+# 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
+
+import json
+
+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,
+)
+
+
+class BookingContextData(object):
+ def prepopulate(self, *args, **kwargs):
+ self.loginuser = instantiate_user(False, username=kwargs.get("login_username", "newtestuser"), password="testpassword")
+ instantiate_userprofile(self.loginuser, True)
+
+ lab_user = kwargs.get("lab_user", instantiate_user(True))
+ self.lab = instantiate_lab(lab_user)
+
+ self.host_profile = make_hostprofile_set(self.lab)
+ self.scenario = instantiate_scenario()
+ self.installer = instantiate_installer([self.scenario])
+ os = instantiate_os([self.installer])
+ self.image = instantiate_image(self.lab, 1, self.loginuser, os, self.host_profile)
+ self.host = instantiate_host(self.host_profile, self.lab)
+ self.role = instantiate_opnfvrole()
+ self.pubnet = instantiate_publicnet(10, self.lab)
+
+
+def instantiate_user(is_superuser,
+ 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 instantiate_userprofile(user=None, can_book_multiple=False):
+ if not user:
+ user = instantiate_user(True, 'test_user', 'test_pass', 'test_user@test_site.org')
+ userprofile = UserProfile()
+ userprofile.user = user
+ userprofile.booking_privledge = can_book_multiple
+
+ userprofile.save()
+
+ return user
+
+
+def instantiate_vlanmanager(vlans=None,
+ block_size=20,
+ allow_overlapping=False,
+ reserved_vlans=None
+ ):
+ vlanmanager = VlanManager()
+ if not vlans:
+ vlans = []
+ for vlan in range(0, 4095):
+ vlans.append(vlan % 2)
+ vlanmanager.vlans = json.dumps(vlans)
+ if not reserved_vlans:
+ reserved_vlans = []
+ for vlan in range(0, 4095):
+ reserved_vlans.append(0)
+ vlanmanager.reserved_vlans = json.dumps(vlans)
+ vlanmanager.block_size = block_size
+ vlanmanager.allow_overlapping = allow_overlapping
+
+ vlanmanager.save()
+
+ return vlanmanager
+
+
+def instantiate_lab(user=None,
+ name="Test Lab Instance",
+ status=LabStatus.UP,
+ vlan_manager=None
+ ):
+ if not vlan_manager:
+ vlan_manager = instantiate_vlanmanager()
+
+ if not user:
+ user = instantiate_user(True, 'test_user', 'test_pass', 'test_user@test_site.org')
+
+ lab = Lab()
+ lab.lab_user = user
+ lab.name = name
+ lab.contact_email = 'test_lab@test_site.org'
+ lab.contact_phone = '603 123 4567'
+ lab.status = status
+ lab.vlan_manager = vlan_manager
+ lab.description = 'test lab instantiation'
+ lab.api_token = '12345678'
+
+ lab.save()
+
+ return lab
+
+
+"""
+resource_inventory instantiation section for permenant resources
+"""
+
+
+def make_hostprofile_set(lab, name="test_hostprofile"):
+ hostprof = instantiate_hostprofile(lab, name=name)
+ instantiate_diskprofile(hostprof, 500, name=name)
+ instantiate_cpuprofile(hostprof)
+ instantiate_interfaceprofile(hostprof, name=name)
+ instantiate_ramprofile(hostprof)
+
+ return hostprof
+
+
+def instantiate_hostprofile(lab,
+ host_type=0,
+ name="test hostprofile instance"
+ ):
+ hostprof = HostProfile()
+ hostprof.host_type = host_type
+ hostprof.name = name
+ hostprof.description = 'test hostprofile instance'
+ hostprof.save()
+ hostprof.labs.add(lab)
+
+ hostprof.save()
+
+ return hostprof
+
+
+def instantiate_ramprofile(host,
+ channels=4,
+ amount=256):
+ ramprof = RamProfile()
+ ramprof.host = host
+ ramprof.amount = amount
+ ramprof.channels = channels
+ ramprof.save()
+
+ return ramprof
+
+
+def instantiate_diskprofile(hostprofile,
+ size=0,
+ media_type="SSD",
+ name="test diskprofile",
+ rotation=0,
+ interface="sata"):
+
+ diskprof = DiskProfile()
+ diskprof.name = name
+ diskprof.size = size
+ diskprof.media_type = media_type
+ diskprof.host = hostprofile
+ diskprof.rotation = rotation
+ diskprof.interface = interface
+
+ diskprof.save()
+
+ return diskprof
+
+
+def instantiate_cpuprofile(hostprofile,
+ cores=4,
+ architecture="x86_64",
+ cpus=4,
+ ):
+ cpuprof = CpuProfile()
+ cpuprof.cores = cores
+ cpuprof.architecture = architecture
+ cpuprof.cpus = cpus
+ cpuprof.host = hostprofile
+ cpuprof.cflags = ''
+
+ cpuprof.save()
+
+ return cpuprof
+
+
+def instantiate_interfaceprofile(hostprofile,
+ speed=1000,
+ name="test interface profile",
+ nic_type="pcie"
+ ):
+ intprof = InterfaceProfile()
+ intprof.host = hostprofile
+ intprof.name = name
+ intprof.speed = speed
+ intprof.nic_type = nic_type
+
+ intprof.save()
+
+ return intprof
+
+
+def instantiate_image(lab,
+ lab_id,
+ owner,
+ os,
+ host_profile,
+ public=True,
+ name="default image",
+ description="default image"
+ ):
+ image = Image()
+ image.from_lab = lab
+ image.lab_id = lab_id
+ image.os = os
+ image.host_type = host_profile
+ image.public = public
+ image.name = name
+ image.description = description
+
+ image.save()
+
+ return image
+
+
+def instantiate_scenario(name="test scenario"):
+ scenario = Scenario()
+ scenario.name = name
+ scenario.save()
+ return scenario
+
+
+def instantiate_installer(supported_scenarios,
+ name="test installer"
+ ):
+ installer = Installer()
+ installer.name = name
+ installer.save()
+ for scenario in supported_scenarios:
+ installer.sup_scenarios.add(scenario)
+
+ installer.save()
+ return installer
+
+
+def instantiate_os(supported_installers,
+ name="test operating system",
+ ):
+ os = Opsys()
+ os.name = name
+ os.save()
+ for installer in supported_installers:
+ os.sup_installers.add(installer)
+ os.save()
+ return os
+
+
+def instantiate_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"):
+ host = Host()
+ host.lab = lab
+ host.profile = host_profile
+ host.name = name
+ host.booked = booked
+ host.working = working
+ host.config = config
+ host.template = template
+ host.bundle = bundle
+ host.model = model
+ host.vendor = vendor
+
+ host.save()
+
+ return host
+
+
+def instantiate_opnfvrole(name="Jumphost",
+ description="test opnfvrole"):
+ role = OPNFVRole()
+ role.name = name
+ role.description = description
+ role.save()
+
+ return role
+
+
+def instantiate_publicnet(vlan,
+ lab,
+ in_use=False,
+ cidr="0.0.0.0/0",
+ gateway="0.0.0.0"):
+ pubnet = PublicNetwork()
+ pubnet.lab = lab
+ pubnet.vlan = vlan
+ pubnet.cidr = cidr
+ pubnet.gateway = gateway
+ pubnet.save()
+
+ return pubnet
diff --git a/src/notifier/manager.py b/src/notifier/manager.py
index f03c2cc..240cf85 100644
--- a/src/notifier/manager.py
+++ b/src/notifier/manager.py
@@ -18,13 +18,13 @@ class NotificationHandler(object):
@classmethod
def notify_new_booking(cls, booking):
template = "notifier/new_booking.html"
- titles = ["You have a new Booking", "You have been added to a Booking"]
+ titles = ["You have a new booking (" + str(booking.id) + ")", "You have been added to a booking (" + str(booking.id) + ")"]
cls.booking_notify(booking, template, titles)
@classmethod
def notify_booking_end(cls, booking):
template = "notifier/end_booking.html"
- titles = ["Your booking has ended", "A booking you collaborate on has ended"]
+ titles = ["Your booking (" + str(booking.id) + ") has ended", "A booking (" + str(booking.id) + ") that you collaborate on has ended"]
cls.booking_notify(booking, template, titles)
@classmethod
diff --git a/src/notifier/migrations/0003_auto_20190123_1741.py b/src/notifier/migrations/0003_auto_20190123_1741.py
new file mode 100644
index 0000000..f491993
--- /dev/null
+++ b/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/src/notifier/migrations/0004_auto_20190124_2115.py b/src/notifier/migrations/0004_auto_20190124_2115.py
new file mode 100644
index 0000000..306ec7b
--- /dev/null
+++ b/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/src/notifier/migrations/0005_auto_20190306_1616.py b/src/notifier/migrations/0005_auto_20190306_1616.py
new file mode 100644
index 0000000..d92c988
--- /dev/null
+++ b/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/src/notifier/models.py b/src/notifier/models.py
index 5e7c60e..49189e8 100644
--- a/src/notifier/models.py
+++ b/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/src/notifier/views.py b/src/notifier/views.py
index 4ee757f..3a85eda 100644
--- a/src/notifier/views.py
+++ b/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/src/resource_inventory/migrations/0007_auto_20190306_1616.py b/src/resource_inventory/migrations/0007_auto_20190306_1616.py
new file mode 100644
index 0000000..19a49c5
--- /dev/null
+++ b/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/src/resource_inventory/migrations/0008_host_remote_management.py b/src/resource_inventory/migrations/0008_host_remote_management.py
new file mode 100644
index 0000000..f74a535
--- /dev/null
+++ b/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/src/resource_inventory/models.py b/src/resource_inventory/models.py
index ebf63cc..4e3974e 100644
--- a/src/resource_inventory/models.py
+++ b/src/resource_inventory/models.py
@@ -291,6 +291,26 @@ class HostConfiguration(models.Model):
return "config with " + str(self.host) + " and image " + str(self.image)
+class RemoteInfo(models.Model):
+ address = models.CharField(max_length=15)
+ mac_address = models.CharField(max_length=17)
+ password = models.CharField(max_length=100)
+ user = models.CharField(max_length=100)
+ management_type = models.CharField(max_length=50, default="ipmi")
+ versions = models.CharField(max_length=100) # json serialized list of floats
+
+
+def get_default_remote_info():
+ return RemoteInfo.objects.get_or_create(
+ address="default",
+ mac_address="default",
+ password="default",
+ user="default",
+ management_type="default",
+ versions="[default]"
+ )[0].pk
+
+
# Concrete host, actual machine in a lab
class Host(models.Model):
id = models.AutoField(primary_key=True)
@@ -305,6 +325,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
diff --git a/src/resource_inventory/pdf_templater.py b/src/resource_inventory/pdf_templater.py
new file mode 100644
index 0000000..9f7e7f1
--- /dev/null
+++ b/src/resource_inventory/pdf_templater.py
@@ -0,0 +1,173 @@
+##############################################################################
+# 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, resource):
+ """
+ fills the pod descriptor file template with info about the resource
+ """
+ template = "dashboard/pdf.yaml"
+ info = {}
+ info['details'] = cls.get_pdf_details(resource)
+ info['jumphost'] = cls.get_pdf_jumphost(resource)
+ info['nodes'] = cls.get_pdf_nodes(resource)
+
+ 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_pdf_jumphost(cls, resource):
+ """
+ returns a dict of all the info for the "jumphost" section
+ """
+ jumphost = Host.objects.get(bundle=resource, config__opnfvRole__name__iexact="jumphost")
+ jumphost_info = cls.get_pdf_host(jumphost)
+ remote_params = jumphost_info['remote_management'] # jumphost has extra block not in normal hosts
+ remote_params.pop("address")
+ remote_params.pop("mac_address")
+ jumphost_info['remote_params'] = remote_params
+ jumphost_info['os'] = jumphost.config.image.os.name
+ return jumphost_info
+
+ @classmethod
+ def get_pdf_nodes(cls, resource):
+ """
+ returns a list of all the "nodes" (every host except jumphost)
+ """
+ pdf_nodes = []
+ nodes = Host.objects.filter(bundle=resource).exclude(config__opnfvRole__name__iexact="jumphost")
+ 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_management'] = 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
+ profile = InterfaceProfile.objects.get(host=interface.host.profile, name=interface.name)
+ iface_info['speed'] = str(int(profile.speed / 1000)) + "gb"
+ return iface_info
+
+ @classmethod
+ def get_pdf_host_remote_management(cls, host):
+ """
+ gives the remote params of the host
+ """
+ mgmt = {}
+ mgmt['address'] = "I dunno"
+ mgmt['mac_address'] = "I dunno"
+ mgmt['pass'] = "I dunno"
+ mgmt['type'] = "I dunno"
+ mgmt['user'] = "I dunno"
+ mgmt['versions'] = ["I dunno"]
+ return mgmt
diff --git a/src/resource_inventory/resource_manager.py b/src/resource_inventory/resource_manager.py
index 812fcd7..52b0055 100644
--- a/src/resource_inventory/resource_manager.py
+++ b/src/resource_inventory/resource_manager.py
@@ -8,9 +8,6 @@
##############################################################################
-from django.template.loader import render_to_string
-
-import booking
from dashboard.exceptions import (
ResourceExistenceException,
ResourceAvailabilityException,
@@ -38,6 +35,31 @@ class ResourceManager:
hostprofileset = HostProfile.objects.filter(host__in=hostset, labs=lab)
return set(hostprofileset)
+ def hostsAvailable(self, grb):
+ """
+ This method will check if the given GenericResourceBundle
+ is available. No changes to the database
+ """
+
+ # count up hosts
+ profile_count = {}
+ for host in grb.getHosts():
+ if host.profile not in profile_count:
+ profile_count[host.profile] = 0
+ profile_count[host.profile] += 1
+
+ # check that all required hosts are available
+ for profile in profile_count.keys():
+ available = Host.objects.filter(
+ booked=False,
+ lab=grb.lab,
+ profile=profile
+ ).count()
+ needed = profile_count[profile]
+ if available < needed:
+ return False
+ return True
+
# public interface
def deleteResourceBundle(self, resourceBundle):
for host in Host.objects.filter(bundle=resourceBundle):
@@ -117,90 +139,3 @@ class ResourceManager:
def fail_acquire(self, hosts):
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/src/templates/dashboard/idf.yaml b/src/templates/dashboard/idf.yaml
new file mode 100644
index 0000000..5da20c4
--- /dev/null
+++ b/src/templates/dashboard/idf.yaml
@@ -0,0 +1,52 @@
+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/src/templates/dashboard/pdf.yaml b/src/templates/dashboard/pdf.yaml
index 297e04b..c893919 100644
--- a/src/templates/dashboard/pdf.yaml
+++ b/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/src/templates/notifier/inbox.html b/src/templates/notifier/inbox.html
index 471eae4..4184d1d 100644
--- a/src/templates/notifier/inbox.html
+++ b/src/templates/notifier/inbox.html
@@ -9,7 +9,7 @@
.inbox-panel {
display: grid;
- grid-template-columns: 30% 70%;
+ grid-template-columns: 30% 5% 65%;
}
.section-panel {
@@ -22,7 +22,8 @@
}
.card-container {
- box-shadow: 0 0 5px 2px #cccccc;
+ border: 1px solid #cccccc;
+ border-bottom: 0px;
}
.card {
height: 50px;
@@ -43,7 +44,7 @@
}
#inbox-iframe {
- height: calc(100vh - 130px);
+ height: calc(100vh - 57px);
}
.half_width {
@@ -51,29 +52,45 @@
}
.card-wrapper {
}
+
+ #page-wrapper{
+ padding: 0px;
+ }
+
+ .read_notification{
+ background-color: #efefef;
+ }
</style>
<div class="inbox-panel">
<div class="section-panel">
+ <h4>New:</h4>
<div class="card-container">
- {% for notification in notifications %}
+ {% for notification in unread_notifications %}
<div class="inbox-entry card" onclick="showmessage({{notification.id}}); setactive(this);">
{{ notification }}
</div>
{% endfor %}
</div>
+ <h4>Read:</h4>
+ <div class="card-container">
+ {% for notification in read_notifications %}
+ <div class="inbox-entry card read_notification" onclick="showmessage({{notification.id}}); setactive(this);">
+ {{ notification }}
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ <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>
+ <iframe id="inbox-iframe" frameBorder="0" width="100%" height="100vh" scrolling="yes">Please select a notification</iframe>
</div>
</div>
</div>
<script type="text/javascript">
- $('#inbox-iframe').load(function() {
- sizetoiframe(this);
- })
function showmessage(msg_id)
{
@@ -82,5 +99,4 @@
}
</script>
-
{% endblock %}
diff --git a/src/templates/notifier/notification.html b/src/templates/notifier/notification.html
index 65d26c9..0eafa60 100644
--- a/src/templates/notifier/notification.html
+++ b/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/src/templates/workflow/viewport-base.html b/src/templates/workflow/viewport-base.html
index 9ddb4b8..f78bc01 100644
--- a/src/templates/workflow/viewport-base.html
+++ b/src/templates/workflow/viewport-base.html
@@ -419,7 +419,6 @@
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;
}
diff --git a/src/workflow/models.py b/src/workflow/models.py
index 7dae279..cdfddef 100644
--- a/src/workflow/models.py
+++ b/src/workflow/models.py
@@ -21,6 +21,7 @@ from api.models import JobFactory
from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
from resource_inventory.models import Image, GenericInterface
from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
from notifier.manager import NotificationHandler
from booking.models import Booking
@@ -577,7 +578,7 @@ class Repository():
booking.collaborators.add(collaborator)
try:
- booking.pdf = ResourceManager().makePDF(booking.resource)
+ booking.pdf = PDFTemplater.makePDF(booking.resource)
booking.save()
except Exception as e:
return "BOOK, failed to create Pod Desriptor File: " + str(e)
diff --git a/test.sh b/test.sh
index 7931cf0..0fbfd0e 100755
--- a/test.sh
+++ b/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