New API call to get details of supported QoS rule type

This commit adds new API call that allows to discover
details about supported QoS rule type and its parameters
by each of loaded backend drivers.

DocImpact: New call to get details about supported
           rule_type for each loaded backend driver
ApiImpact

Change-Id: I2008e9d3e400dd717434fbdd2e693c9c5e34c3a4
Closes-Bug: #1686035
This commit is contained in:
Sławek Kapłoński 2017-06-19 06:35:25 +00:00 committed by Slawek Kaplonski
parent 9d20de62bf
commit 2cc547241c
13 changed files with 395 additions and 6 deletions

View File

@ -165,3 +165,7 @@ PORT_BINDING_STATUS_ACTIVE = 'ACTIVE'
PORT_BINDING_STATUS_INACTIVE = 'INACTIVE'
PORT_BINDING_STATUSES = (PORT_BINDING_STATUS_ACTIVE,
PORT_BINDING_STATUS_INACTIVE)
# Possible types of values (e.g. in QoS rule types)
VALUES_TYPE_CHOICES = "choices"
VALUES_TYPE_RANGE = "range"

View File

@ -48,6 +48,8 @@ QOS_RULE_COMMON_FIELDS = {
'is_visible': True},
}
RULE_TYPES = "rule_types"
RESOURCE_ATTRIBUTE_MAP = {
COLLECTION_NAME: {
'id': {'allow_post': False, 'allow_put': False,
@ -66,7 +68,7 @@ RESOURCE_ATTRIBUTE_MAP = {
'is_visible': True},
'rules': {'allow_post': False, 'allow_put': False, 'is_visible': True},
},
'rule_types': {
RULE_TYPES: {
'type': {'allow_post': False, 'allow_put': False,
'is_visible': True}
}
@ -309,6 +311,10 @@ class QoSPluginBase(service_base.ServicePluginBase):
def get_plugin_type(cls):
return constants.QOS
@abc.abstractmethod
def get_rule_type(self, context, rule_type_name, fields=None):
pass
@abc.abstractmethod
def get_rule_types(self, context, filters=None, fields=None, sorts=None,
limit=None, marker=None, page_reverse=False):

View File

@ -0,0 +1,74 @@
# Copyright (c) 2017 OVH SAS
# All rights reserved.
#
# 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 neutron_lib.api import extensions as api_extensions
from neutron.extensions import qos
# The name of the extension.
NAME = "Details of QoS rule types"
# The alias of the extension.
ALIAS = "qos-rule-type-details"
# The description of the extension.
DESCRIPTION = ("Expose details about QoS rule types supported by loaded "
"backend drivers")
# The list of required extensions.
REQUIRED_EXTENSIONS = [qos.ALIAS]
# The list of optional extensions.
OPTIONAL_EXTENSIONS = None
# The resource attribute map for the extension.
RESOURCE_ATTRIBUTE_MAP = {
qos.RULE_TYPES: {
'drivers': {'allow_post': False, 'allow_put': False,
'is_visible': True}
}
}
class Qos_rule_type_details(api_extensions.ExtensionDescriptor):
@classmethod
def get_name(cls):
return NAME
@classmethod
def get_alias(cls):
return ALIAS
@classmethod
def get_description(cls):
return DESCRIPTION
@classmethod
def get_updated(cls):
return "2017-06-22T10:00:00-00:00"
def get_required_extensions(self):
return REQUIRED_EXTENSIONS or []
def get_optional_extensions(self):
return OPTIONAL_EXTENSIONS or []
def get_extended_resources(self, version):
if version == "2.0":
return RESOURCE_ATTRIBUTE_MAP
else:
return {}

View File

@ -13,10 +13,12 @@
from neutron.plugins.common import constants
from neutron_lib.plugins import directory
from oslo_log import log as logging
from oslo_utils import versionutils
from oslo_versionedobjects import base as obj_base
from oslo_versionedobjects import fields as obj_fields
from neutron.objects import base
from neutron.objects import common_types
from neutron.services.qos import qos_consts
LOG = logging.getLogger(__name__)
@ -35,12 +37,29 @@ class QosRuleType(base.NeutronObject):
# Version 1.0: Initial version
# Version 1.1: Added QosDscpMarkingRule
# Version 1.2: Added QosMinimumBandwidthRule
VERSION = '1.2'
# Version 1.3: Added drivers field
VERSION = '1.3'
fields = {
'type': RuleTypeField(),
'drivers': obj_fields.ListOfObjectsField(
'QosRuleTypeDriver', nullable=True)
}
synthetic_fields = ['drivers']
# we don't receive context because we don't need db access at all
@classmethod
def get_object(cls, rule_type_name, **kwargs):
plugin = directory.get_plugin(alias=constants.QOS)
drivers = plugin.supported_rule_type_details(rule_type_name)
drivers_obj = [QosRuleTypeDriver(
name=driver['name'],
supported_parameters=driver['supported_parameters'])
for driver in drivers]
return cls(type=rule_type_name, drivers=drivers_obj)
# we don't receive context because we don't need db access at all
@classmethod
def get_objects(cls, validate_filters=True, **kwargs):
@ -52,3 +71,29 @@ class QosRuleType(base.NeutronObject):
# TODO(ihrachys): apply filters to returned result
return [cls(type=type_) for type_ in rule_types]
def obj_make_compatible(self, primitive, target_version):
_target_version = versionutils.convert_version_to_tuple(target_version)
if _target_version < (1, 3):
primitive.pop('drivers', None)
@obj_base.VersionedObjectRegistry.register
class QosRuleTypeDriver(base.NeutronObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'name': obj_fields.StringField(),
'supported_parameters': common_types.ListOfDictOfMiscValuesField()
}
def to_dict(self):
return {
'name': self.name,
'supported_parameters': self.supported_parameters}
@classmethod
def get_objects(cls, context, **kwargs):
raise NotImplementedError()

View File

@ -20,6 +20,7 @@ from neutron.api.rpc.callbacks import events as rpc_events
from neutron.api.rpc.callbacks.producer import registry as rpc_registry
from neutron.api.rpc.callbacks import resources
from neutron.api.rpc.handlers import resources_rpc
from neutron.common import constants
from neutron.common import exceptions
from neutron.objects.qos import policy as policy_object
from neutron.services.qos import qos_consts
@ -83,6 +84,20 @@ class QosServiceDriverManager(object):
'driver': driver.name})
return False
@staticmethod
def _parse_parameter_values(parameter_values):
validator, possible_values = list(parameter_values.items())[0]
if validator == 'type:range':
parameter_values = {
"start": possible_values[0],
"end": possible_values[1]
}
parameter_type = constants.VALUES_TYPE_RANGE
elif validator == 'type:values':
parameter_values = possible_values
parameter_type = constants.VALUES_TYPE_CHOICES
return parameter_values, parameter_type
def call(self, method_name, *args, **kwargs):
"""Helper method for calling a method across all extension drivers."""
exc_list = []
@ -158,3 +173,27 @@ class QosServiceDriverManager(object):
LOG.debug("Supported QoS rule types "
"(common subset for all loaded QoS drivers): %s", rule_types)
return rule_types
def supported_rule_type_details(self, rule_type_name):
if not self._drivers:
return []
rule_type_drivers = []
for driver in self._drivers:
if rule_type_name in driver.supported_rules:
supported_parameters = []
rule_parameters = driver.supported_rules.get(rule_type_name)
for name, values in rule_parameters.items():
parameter_values, parameter_type = (
self._parse_parameter_values(values))
supported_parameters.append({
"parameter_name": name,
"parameter_values": parameter_values,
"parameter_type": parameter_type
})
rule_type_drivers.append({
"name": driver.name,
"supported_parameters": supported_parameters
})
return rule_type_drivers

View File

@ -16,6 +16,7 @@
from neutron_lib.callbacks import events as callbacks_events
from neutron_lib.callbacks import registry as callbacks_registry
from neutron_lib.callbacks import resources as callbacks_resources
from neutron_lib import exceptions as lib_exc
from neutron.common import exceptions as n_exc
from neutron.db import api as db_api
@ -40,7 +41,8 @@ class QoSPlugin(qos.QoSPluginBase):
"""
supported_extension_aliases = ['qos',
'qos-bw-limit-direction',
'qos-default']
'qos-default',
'qos-rule-type-details']
__native_pagination_support = True
__native_sorting_support = True
@ -264,6 +266,13 @@ class QoSPlugin(qos.QoSPluginBase):
return policy_object.QosPolicy.get_objects(context, _pager=pager,
**filters)
@db_base_plugin_common.filter_fields
@db_base_plugin_common.convert_result_to_dict
def get_rule_type(self, context, rule_type_name, fields=None):
if not context.is_admin:
raise lib_exc.NotAuthorized()
return rule_type_object.QosRuleType.get_object(rule_type_name)
@db_base_plugin_common.filter_fields
@db_base_plugin_common.convert_result_to_dict
def get_rule_types(self, context, filters=None, fields=None,
@ -273,6 +282,9 @@ class QoSPlugin(qos.QoSPluginBase):
filters = {}
return rule_type_object.QosRuleType.get_objects(**filters)
def supported_rule_type_details(self, rule_type_name):
return self.driver_manager.supported_rule_type_details(rule_type_name)
@property
def supported_rule_types(self):
return self.driver_manager.supported_rule_types

View File

@ -31,6 +31,12 @@ class QosTestJSON(base.BaseAdminNetworkTest):
required_extensions = ['qos']
@staticmethod
def _get_driver_details(rule_type_details, driver_name):
for driver in rule_type_details['drivers']:
if driver['name'] == driver_name:
return driver
@decorators.idempotent_id('108fbdf7-3463-4e47-9871-d07f3dcf5bbb')
def test_create_policy(self):
policy = self.create_qos_policy(name='test-policy',
@ -172,6 +178,31 @@ class QosTestJSON(base.BaseAdminNetworkTest):
for rule in actual_list_rule_types:
self.assertEqual(tuple(expected_rule_keys), tuple(rule.keys()))
@decorators.idempotent_id('8ececa21-ef97-4904-a152-9f04c90f484d')
def test_show_rule_type_details_as_user(self):
self.assertRaises(
exceptions.Forbidden,
self.client.show_qos_rule_type,
qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
@decorators.idempotent_id('d0a2460b-7325-481f-a531-050bd96ab25e')
def test_show_rule_type_details_as_admin(self):
# Since returned rule types depend on loaded backend drivers this test
# is checking only if returned keys are same as expected keys
# In theory, we could make the test conditional on which ml2 drivers
# are enabled in gate, but that option doesn't seem to be
# available through tempest.lib framework
expected_rule_type_details_keys = ['type', 'drivers']
rule_type_details = self.admin_client.show_qos_rule_type(
qos_consts.RULE_TYPE_BANDWIDTH_LIMIT).get("rule_type")
# Verify that only required fields present in rule details
self.assertEqual(
sorted(tuple(expected_rule_type_details_keys)),
sorted(tuple(rule_type_details.keys())))
def _disassociate_network(self, client, network_id):
updated_network = client.update_network(network_id,
qos_policy_id=None)

View File

@ -727,6 +727,14 @@ class NetworkClientJSON(service_client.RestClient):
body = jsonutils.loads(body)
return service_client.ResponseBody(resp, body)
def show_qos_rule_type(self, rule_type_name):
uri = '%s/qos/rule-types/%s' % (
self.uri_prefix, rule_type_name)
resp, body = self.get(uri)
self.expected_success(200, resp.status)
body = jsonutils.loads(body)
return service_client.ResponseBody(resp, body)
def create_trunk(self, parent_port_id, subports,
tenant_id=None, name=None, admin_state_up=None,
description=None):

View File

@ -16,6 +16,7 @@
import mock
from oslo_config import cfg
from neutron.common import constants
from neutron import manager
from neutron.objects.qos import rule_type
from neutron.services.qos import qos_consts
@ -25,6 +26,22 @@ from neutron.tests import base as test_base
DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'
DRIVER_SUPPORTED_PARAMETERS = [
{
'parameter_name': qos_consts.MAX_KBPS,
'parameter_type': constants.VALUES_TYPE_RANGE,
'parameter_values': {"start": 0, "end": constants.DB_INTEGER_MAX_VALUE}
}, {
'parameter_name': qos_consts.MAX_BURST,
'parameter_type': constants.VALUES_TYPE_RANGE,
'parameter_values': {"start": 0, "end": constants.DB_INTEGER_MAX_VALUE}
}, {
'parameter_name': qos_consts.DIRECTION,
'parameter_type': constants.VALUES_TYPE_CHOICES,
'parameter_values': constants.VALID_DIRECTIONS
}
]
class QosRuleTypeObjectTestCase(test_base.BaseTestCase):
@ -37,6 +54,26 @@ class QosRuleTypeObjectTestCase(test_base.BaseTestCase):
cfg.CONF.set_override("service_plugins", ["qos"])
manager.init()
def test_get_object(self):
driver_details = {
'name': "backend_driver",
'supported_parameters': DRIVER_SUPPORTED_PARAMETERS
}
with mock.patch.object(
qos_plugin.QoSPlugin, 'supported_rule_type_details',
return_value=[driver_details]
):
rule_type_details = rule_type.QosRuleType.get_object(
qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
self.assertEqual(
driver_details['name'], rule_type_details.drivers[0].name)
self.assertEqual(
driver_details['supported_parameters'],
rule_type_details.drivers[0].supported_parameters)
self.assertEqual(1, len(rule_type_details.drivers))
self.assertEqual(
qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, rule_type_details.type)
def test_get_objects(self):
rule_types_mock = mock.PropertyMock(
return_value=set(qos_consts.VALID_RULE_TYPES))
@ -64,3 +101,14 @@ class QosRuleTypeObjectTestCase(test_base.BaseTestCase):
self.assertIn(qos_consts.RULE_TYPE_DSCP_MARKING,
tuple(rule_type_v1_1.fields['type'].AUTO_TYPE.
_valid_values))
def test_object_version_degradation_1_3_to_1_2(self):
drivers_obj = rule_type.QosRuleTypeDriver(
name="backend_driver", supported_parameters=[{}]
)
qos_rule_type = rule_type.QosRuleType(
type=qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, drivers=[drivers_obj])
rule_type_v1_2 = self._policy_through_version(qos_rule_type, '1.2')
self.assertNotIn("drivers", rule_type_v1_2)
self.assertIn("type", rule_type_v1_2)

View File

@ -66,7 +66,8 @@ object_data = {
'QosBandwidthLimitRule': '1.3-51b662b12a8d1dfa89288d826c6d26d3',
'QosDscpMarkingRule': '1.3-0313c6554b34fd10c753cb63d638256c',
'QosMinimumBandwidthRule': '1.3-314c3419f4799067cc31cc319080adff',
'QosRuleType': '1.2-e6fd08fcca152c339cbd5e9b94b1b8e7',
'QosRuleType': '1.3-7286188edeb3a0386f9cf7979b9700fc',
'QosRuleTypeDriver': '1.0-7d8cb9f0ef661ac03700eae97118e3db',
'QosPolicy': '1.6-4adb0cde3102c10d8970ec9487fd7fe7',
'QosPolicyDefault': '1.0-59e5060eedb1f06dd0935a244d27d11c',
'Quota': '1.0-6bb6a0f1bd5d66a2134ffa1a61873097',

View File

@ -206,6 +206,89 @@ class TestQosDriversManagerRules(TestQosDriversManagerBase):
})
self.assertEqual(driver_manager.supported_rule_types, set([]))
def test__parse_parameter_values(self):
range_parameter = {'type:range': [0, 10]}
values_parameter = {'type:values': [1, 10, 100, 1000]}
expected_parsed_range_parameter = {'start': 0, 'end': 10}
expected_parsed_values_parameter = [1, 10, 100, 1000]
parameter_values, parameter_type = (
driver_mgr.QosServiceDriverManager._parse_parameter_values(
range_parameter))
self.assertEqual(
expected_parsed_range_parameter, parameter_values)
self.assertEqual(
constants.VALUES_TYPE_RANGE, parameter_type)
parameter_values, parameter_type = (
driver_mgr.QosServiceDriverManager._parse_parameter_values(
values_parameter))
self.assertEqual(
expected_parsed_values_parameter, parameter_values)
self.assertEqual(
constants.VALUES_TYPE_CHOICES, parameter_type)
def test_supported_rule_type_details(self):
driver_manager = self._create_manager_with_drivers({
'driver-A': {
'is_loaded': True,
'rules': {
qos_consts.RULE_TYPE_BANDWIDTH_LIMIT: {
"max_kbps": {'type:range': [0, 1000]},
"max_burst_kbps": {'type:range': [0, 1000]}
}
}
},
'driver-B': {
'is_loaded': True,
'rules': {
qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH: {
"min_kbps": {'type:range': [0, 1000]},
'direction': {
'type:values': constants.VALID_DIRECTIONS}
},
qos_consts.RULE_TYPE_DSCP_MARKING: {
"dscp_mark": {
'type:values': constants.VALID_DSCP_MARKS}
}
}
}
})
expected_rule_type_details = [{
'name': 'driver-A',
'supported_parameters': [{
'parameter_name': 'max_kbps',
'parameter_type': constants.VALUES_TYPE_RANGE,
'parameter_values': {'start': 0, 'end': 1000}
}, {
'parameter_name': 'max_burst_kbps',
'parameter_type': constants.VALUES_TYPE_RANGE,
'parameter_values': {'start': 0, 'end': 1000}
}]
}]
bandwidth_limit_details = driver_manager.supported_rule_type_details(
qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
self.assertEqual(
len(expected_rule_type_details), len(bandwidth_limit_details))
self.assertEqual(
expected_rule_type_details[0]['name'],
bandwidth_limit_details[0]['name'])
self.assertEqual(
len(expected_rule_type_details[0]['supported_parameters']),
len(bandwidth_limit_details[0]['supported_parameters'])
)
for parameter in expected_rule_type_details[0]['supported_parameters']:
self.assertIn(
parameter,
bandwidth_limit_details[0]['supported_parameters'])
def test_supported_rule_type_details_no_drivers_loaded(self):
driver_manager = self._create_manager_with_drivers({})
self.assertEqual(
[],
driver_manager.supported_rule_type_details(
qos_consts.RULE_TYPE_BANDWIDTH_LIMIT))
class TestQosDriversCalls(TestQosDriversManagerBase):
"""Test QoS driver calls"""

View File

@ -12,16 +12,18 @@
import mock
from neutron_lib import context
from neutron_lib import exceptions as lib_exc
from neutron_lib.plugins import directory
from oslo_config import cfg
from oslo_utils import uuidutils
from neutron.common import constants
from neutron.common import exceptions as n_exc
from neutron import manager
from neutron.objects import base as base_object
from neutron.objects.qos import policy as policy_object
from neutron.objects.qos import rule as rule_object
from neutron.plugins.common import constants
from neutron.plugins.common import constants as plugins_constants
from neutron.services.qos import qos_consts
from neutron.services.qos import qos_plugin
from neutron.tests.unit.services.qos import base
@ -55,7 +57,7 @@ class TestQosPlugin(base.BaseQosTestCase):
cfg.CONF.set_override("service_plugins", ["qos"])
manager.init()
self.qos_plugin = directory.get_plugin(constants.QOS)
self.qos_plugin = directory.get_plugin(plugins_constants.QOS)
self.qos_plugin.driver_manager = mock.Mock()
@ -811,6 +813,34 @@ class TestQosPlugin(base.BaseQosTestCase):
self.assertRaises(AttributeError, getattr, self.qos_plugin,
'create_policy_bandwidth_limit_rules')
def test_get_rule_type(self):
admin_ctxt = context.get_admin_context()
drivers_details = [{
'name': 'fake-driver',
'supported_parameters': [{
'parameter_name': 'max_kbps',
'parameter_type': constants.VALUES_TYPE_RANGE,
'parameter_range': {'start': 0, 'end': 100}
}]
}]
with mock.patch.object(
qos_plugin.QoSPlugin, "supported_rule_type_details",
return_value=drivers_details
):
rule_type_details = self.qos_plugin.get_rule_type(
admin_ctxt, qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
self.assertEqual(
qos_consts.RULE_TYPE_BANDWIDTH_LIMIT,
rule_type_details['type'])
self.assertEqual(
drivers_details, rule_type_details['drivers'])
def test_get_rule_type_as_user(self):
self.assertRaises(
lib_exc.NotAuthorized,
self.qos_plugin.get_rule_type,
self.ctxt, qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
def test_get_rule_types(self):
rule_types_mock = mock.PropertyMock(
return_value=qos_consts.VALID_RULE_TYPES)

View File

@ -0,0 +1,8 @@
---
prelude: >
New API to get details of supported rule types.
features:
- |
The QoS service plugin can now expose details about supported QoS rule
types in Neutron deployment.
New API call is allowed only for users with admin priviliges.