Adds basic angular QoS panel to Horizon

This patch adds the QoS panel which displays Neutron QoS policies.
This patch allows read only view of the policies, displaying the name,
id, description and shared status of each policy. Policy actions and
rules to be added in follow-up patches.

To test:
Neutron's QoS plugin must be enabled in your setup.
To enable the QoS plugin in devstack please add the following lines
in the local.conf

    enable_plugin neutron git://git.openstack.org/openstack/neutron
    enable_service q-qos

and rebuild your stack (./stack.sh).

Create some qos policies via CLI (neutron qos-policy-create policy1).
now you can list the policies using this patch.

Based on initial commit: https://review.openstack.org/#/c/247997
Co-Authored-By: Masco <mkaliyam@redhat.com>
Co-Authored-By: Rob Cresswell <robert.cresswell@outlook.com>

Change-Id: If46aeb04879d91a1a9305a94a23cecea2041d378
Implements: blueprint network-bandwidth-limiting-qos
This commit is contained in:
Beth Elwell 2017-01-11 10:34:51 +00:00
parent bfaa4e64ec
commit 12fe351823
18 changed files with 494 additions and 11 deletions

@ -1570,3 +1570,31 @@ def get_feature_permission(request, feature, operation=None):
# If all checks are passed, now a given feature is allowed.
return True
class QoSPolicy(NeutronAPIDictWrapper):
"""Wrapper for neutron QoS Policy."""
def to_dict(self):
return self._apidict
def policy_create(request, **kwargs):
"""Create a QoS Policy.
:param request: request context
:param name: name of the policy
:param description: description of policy
:param shared: boolean (true or false)
:return: QoSPolicy object
"""
body = {'policy': kwargs}
policy = neutronclient(request).create_qos_policy(body=body).get('policy')
return QoSPolicy(policy)
def policy_list(request, **kwargs):
"""List of QoS Policies."""
policies = neutronclient(request).list_qos_policies(
**kwargs).get('policies')
return [QoSPolicy(p) for p in policies]

@ -243,3 +243,24 @@ class QuotasSets(generic.View):
message = _('Service Neutron is disabled or quotas extension not '
'available.')
raise rest_utils.AjaxError(501, message)
@urls.register
class QoSPolicies(generic.View):
"""API for QoS Policy."""
url_regex = r'neutron/qos_policies/$'
@rest_utils.ajax()
def get(self, request):
"""Get a list of QoS policies.
The listing result is an object with property "items".
Each item is a qos policy.
"""
# TODO(amotoki):
# project_id=request.user.project_id should be changed to
# tenant_id=request.user.project_id once bug 1695954 is
# addressed to allow tenant_id to be accepted.
result = api.neutron.policy_list(request,
project_id=request.user.project_id)
return {'items': [p.to_dict() for p in result]}

@ -0,0 +1,42 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.api import neutron
LOG = logging.getLogger(__name__)
class NetworkQoS(horizon.Panel):
name = _("Network QoS")
slug = "network_qos"
permissions = ('openstack.services.network',)
def allowed(self, context):
request = context['request']
try:
return (
super(NetworkQoS, self).allowed(context)
and request.user.has_perms(self.permissions)
and neutron.is_extension_supported(request,
extension_alias='qos')
)
except Exception:
LOG.error("Call to list enabled services failed. This is likely "
"due to a problem communicating with the Neutron "
"endpoint. Neutron QoS panel will not be displayed.")
return False

@ -0,0 +1,22 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from django.utils.translation import ugettext_lazy as _
from horizon.browsers import views
title = _("Network QoS Policies")
urlpatterns = [
url(r'^$', views.AngularIndexView.as_view(title=title), name='index'),
]

@ -0,0 +1,10 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'network_qos'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'network'
# Python panel class of the PANEL to be added.
ADD_PANEL = ('openstack_dashboard.dashboards.project.network_qos'
'.panel.NetworkQoS')

@ -37,6 +37,7 @@
'horizon.app.core.flavors',
'horizon.app.core.images',
'horizon.app.core.metadata',
'horizon.app.core.network_qos',
'horizon.app.core.openstack-service-api',
'horizon.app.core.trunks',
'horizon.app.core.workflow',

@ -0,0 +1,3 @@
<hz-resource-panel resource-type-name="OS::Neutron::QoSPolicy">
<hz-resource-table resource-type-name="OS::Neutron::QoSPolicy"></hz-resource-table>
</hz-resource-panel>

@ -0,0 +1,127 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
/**
* @ngdoc horizon.dashboard.project.network_qos
* @ngModule
*
* @description
* Provides all of the services and widgets required
* to support and display the QoS panel.
*/
angular
.module('horizon.app.core.network_qos', [
'ngRoute'
])
.constant('horizon.app.core.network_qos.resourceType', 'OS::Neutron::QoSPolicy')
.run(run)
.config(config);
run.$inject = [
'horizon.framework.conf.resource-type-registry.service',
'horizon.app.core.network_qos.service',
'horizon.app.core.network_qos.resourceType',
'horizon.app.core.openstack-service-api.neutron'
];
function run(registry,
qosService,
qosResourceType) {
registry.getResourceType(qosResourceType)
.setNames(gettext('QoS Policy'), gettext('QoS Policies'))
.setProperties(qosProperties(qosService))
.setListFunction(qosService.getPoliciesPromise)
.tableColumns
.append({
id: 'name',
priority: 1,
sortDefault: true
})
.append({
id: 'id',
priority: 1
})
.append({
id: 'description',
priority: 1
})
.append({
id: 'shared',
priority: 1
});
registry.getResourceType(qosResourceType).filterFacets
.append({
label: gettext('Policy Name'),
name: 'name',
singleton: true,
persistent: true
})
.append({
label: gettext('Policy ID'),
name: 'id',
singleton: true
})
.append({
label: gettext('Description'),
name: 'description',
singleton: true
})
.append({
label: gettext('Shared'),
name: 'shared',
singleton: true,
options: [
{label: gettext('Yes'), key: 'true'},
{label: gettext('No'), key: 'false'}
]
});
}
/**
* @name qosProperties
* @description resource properties for QoS module
*/
function qosProperties() {
return {
name: gettext('Policy Name'),
id: gettext('Policy ID'),
description: gettext('Description'),
shared: { label: gettext('Shared'), filters: ['yesno'] }
};
}
config.$inject = [
'$provide',
'$windowProvider',
'$routeProvider'
];
/**
* @name horizon.dashboard.project.network_qos.basePath
* @description Base path for the QoS code
*/
function config($provide, $windowProvider, $routeProvider) {
var path = $windowProvider.$get().STATIC_URL + 'app/core/network_qos/';
$provide.constant('horizon.app.core.network_qos.basePath', path);
$routeProvider.when('/project/network_qos', {
templateUrl: path + 'panel.html'
});
}
})();

@ -0,0 +1,24 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
describe('horizon.app.core.network_qos', function () {
it('should exist', function () {
expect(angular.module('horizon.app.core.network_qos')).toBeDefined();
});
});
})();

@ -0,0 +1,74 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
"use strict";
angular.module('horizon.app.core.network_qos')
.factory('horizon.app.core.network_qos.service', qosService);
qosService.$inject = [
'$filter',
'horizon.app.core.openstack-service-api.neutron',
'horizon.app.core.openstack-service-api.userSession'
];
/*
* @ngdoc factory
* @name horizon.app.core.network_qos.service
*
* @description
* This service provides functions that are used through the QoS
* features. These are primarily used in the module registrations
* but do not need to be restricted to such use. Each exposed function
* is documented below.
*/
function qosService($filter, neutron, userSession) {
var version;
return {
getPoliciesPromise: getPoliciesPromise
};
/*
* @ngdoc function
* @name getPoliciesPromise
* @description
* Given filter/query parameters, returns a promise for the matching
* policies. This is used in displaying lists of policies. In this case,
* we need to modify the API's response by adding a composite value called
* 'trackBy' to assist the display mechanism when updating rows.
*/
function getPoliciesPromise(params) {
return userSession.get().then(getQoSPolicies);
function getQoSPolicies() {
return neutron.getQoSPolicies(params).then(modifyResponse);
}
function modifyResponse(response) {
return {data: {items: response.data.items.map(modifyQos)}};
function modifyQos(policy) {
policy.trackBy = policy.id;
policy.apiVersion = version;
policy.name = policy.name || policy.id;
return policy;
}
}
}
}
})();

@ -0,0 +1,56 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
"use strict";
describe('horizon.app.core.network_qos', function () {
it('should exist', function () {
expect(angular.module('horizon.app.core.network_qos')).toBeDefined();
});
});
describe('qosService', function() {
var service;
beforeEach(module('horizon.app.core'));
beforeEach(inject(function($injector) {
service = $injector.get('horizon.app.core.network_qos.service');
}));
describe('getPoliciesPromise', function() {
it("provides a promise that gets translated", inject(function($q, $injector, $timeout) {
var neutron = $injector.get('horizon.app.core.openstack-service-api.neutron');
var session = $injector.get('horizon.app.core.openstack-service-api.userSession');
var deferred = $q.defer();
var deferredSession = $q.defer();
spyOn(neutron, 'getQoSPolicies').and.returnValue(deferred.promise);
spyOn(session, 'get').and.returnValue(deferredSession.promise);
var result = service.getPoliciesPromise({});
deferredSession.resolve({});
deferred.resolve({
data: {
items: [{id: 1, name: 'policy1'},{id:2}]
}
});
$timeout.flush();
expect(neutron.getQoSPolicies).toHaveBeenCalled();
expect(result.$$state.value.data.items[0].name).toBe('policy1');
expect(result.$$state.value.data.items[0].trackBy).toBe(1);
//if no name given, name should be id value
expect(result.$$state.value.data.items[1].name).toBe(2);
}));
});
});
})();

@ -44,7 +44,8 @@
getExtensions: getExtensions,
getDefaultQuotaSets: getDefaultQuotaSets,
updateProjectQuota: updateProjectQuota,
getTrunks: getTrunks
getTrunks: getTrunks,
getQoSPolicies: getQoSPolicies
};
return service;
@ -334,6 +335,23 @@
});
}
// QoS policies
/**
* @name horizon.app.core.openstack-service-api.neutron.getQoSPolicies
* @description
* Get a list of qos policies.
*
* The listing result is an object with property "items". Each item is
* a QoS policy.
*/
function getQoSPolicies() {
return apiService.get('/api/neutron/qos_policies/')
.error(function () {
toastService.add('error', gettext('Unable to retrieve the qos policies.'));
});
}
// Trunks
/**

@ -22,18 +22,17 @@
var apiService = {};
var toastService = {};
beforeEach(
module('horizon.mock.openstack-service-api',
function($provide, initServices) {
testCall = initServices($provide, apiService, toastService);
})
);
beforeEach(function() {
module('horizon.mock.openstack-service-api', function($provide, initServices) {
testCall = initServices($provide, apiService, toastService);
});
beforeEach(module('horizon.app.core.openstack-service-api'));
module('horizon.app.core.openstack-service-api');
beforeEach(inject(['horizon.app.core.openstack-service-api.neutron', function(neutronAPI) {
service = neutronAPI;
}]));
inject(['horizon.app.core.openstack-service-api.neutron', function(neutronAPI) {
service = neutronAPI;
}]);
});
it('defines the service', function() {
expect(service).toBeDefined();
@ -149,6 +148,12 @@
"testInput": [
42
]
},
{
"func": "getQoSPolicies",
"method": "get",
"path": "/api/neutron/qos_policies/",
"error": "Unable to retrieve the qos policies."
}
];
@ -161,4 +166,5 @@
});
});
})();

@ -702,6 +702,33 @@ class NeutronApiTests(test.APITestCase):
self.assertEqual(10, len(ret_val))
self.assertEqual(port_ids, [p.id for p in ret_val])
def test_qos_policies_list(self):
exp_policies = self.qos_policies.list()
api_qos_policies = {'policies': self.api_qos_policies.list()}
neutronclient = self.stub_neutronclient()
neutronclient.list_qos_policies().AndReturn(api_qos_policies)
self.mox.ReplayAll()
ret_val = api.neutron.policy_list(self.request)
self.assertEqual(len(ret_val), len(exp_policies))
self.assertIsInstance(ret_val[0], api.neutron.QoSPolicy)
self.assertEqual(exp_policies[0].name, ret_val[0].name)
def test_qos_policy_create(self):
qos_policy = self.api_qos_policies.first()
post_data = {'policy': {'name': qos_policy['name']}}
neutronclient = self.stub_neutronclient()
neutronclient.create_qos_policy(body=post_data) \
.AndReturn({'policy': qos_policy})
self.mox.ReplayAll()
ret_val = api.neutron.policy_create(self.request,
name=qos_policy['name'])
self.assertIsInstance(ret_val, api.neutron.QoSPolicy)
self.assertEqual(qos_policy['name'], ret_val.name)
class NeutronApiSecurityGroupTests(NeutronApiTestBase):

@ -290,6 +290,11 @@ TEST_GLOBAL_MOCKS_ON_PANELS = {
'.trunks.panel.Trunks.can_access'),
'return_value': True,
},
'qos': {
'method': ('openstack_dashboard.dashboards.project'
'.network_qos.panel.NetworkQoS.can_access'),
'return_value': True,
},
'vpn': {
'method': ('openstack_dashboard.dashboards.project'
'.vpn.panel.VPN.can_access'),

@ -53,6 +53,7 @@ def data(TEST):
TEST.fw_policies = utils.TestDataContainer()
TEST.fw_rules = utils.TestDataContainer()
TEST.ip_availability = utils.TestDataContainer()
TEST.qos_policies = utils.TestDataContainer()
# Data return by neutronclient.
TEST.api_agents = utils.TestDataContainer()
@ -79,6 +80,7 @@ def data(TEST):
TEST.api_fw_policies = utils.TestDataContainer()
TEST.api_fw_rules = utils.TestDataContainer()
TEST.api_ip_availability = utils.TestDataContainer()
TEST.api_qos_policies = utils.TestDataContainer()
# 1st network.
network_dict = {'admin_state_up': True,
@ -961,3 +963,15 @@ def data(TEST):
TEST.ip_availability.add(availability)
TEST.api_ip_availability.add(availability)
# qos policies
policy_dict = {'id': 'a21dcd22-7189-cccc-aa32-22adafaf16a7',
'name': 'policy 1',
'tenant_id': '1'}
TEST.api_qos_policies.add(policy_dict)
TEST.qos_policies.add(neutron.QoSPolicy(policy_dict))
policy_dict1 = {'id': 'a21dcd22-7189-ssss-aa32-22adafaf16a7',
'name': 'policy 2',
'tenant_id': '1'}
TEST.api_qos_policies.add(policy_dict1)
TEST.qos_policies.add(neutron.QoSPolicy(policy_dict1))

@ -0,0 +1,5 @@
---
features:
- The Network QoS panel allows users to view a list of created network
policies. This panel displays a table view of the name, id, description
and shared status of each policy.