diff options
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) @@ -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 |