diff --git a/neutron/conf/policies/qos.py b/neutron/conf/policies/qos.py index b5a66f1e5ea..ca992e37b34 100644 --- a/neutron/conf/policies/qos.py +++ b/neutron/conf/policies/qos.py @@ -236,6 +236,105 @@ rules = [ }, ] ), + policy.DocumentedRuleDefault( + 'get_alias_bandwidth_limit_rule', + 'rule:get_policy_bandwidth_limit_rule', + 'Get a QoS bandwidth limit rule through alias', + [ + { + 'method': 'GET', + 'path': '/qos/alias_bandwidth_limit_rules/{rule_id}/', + }, + ] + ), + policy.DocumentedRuleDefault( + 'update_alias_bandwidth_limit_rule', + 'rule:update_policy_bandwidth_limit_rule', + 'Update a QoS bandwidth limit rule through alias', + [ + { + 'method': 'PUT', + 'path': '/qos/alias_bandwidth_limit_rules/{rule_id}/', + }, + ] + ), + policy.DocumentedRuleDefault( + 'delete_alias_bandwidth_limit_rule', + 'rule:delete_policy_bandwidth_limit_rule', + 'Delete a QoS bandwidth limit rule through alias', + [ + { + 'method': 'DELETE', + 'path': '/qos/alias_bandwidth_limit_rules/{rule_id}/', + }, + ] + ), + policy.DocumentedRuleDefault( + 'get_alias_dscp_marking_rule', + 'rule:get_policy_dscp_marking_rule', + 'Get a QoS DSCP marking rule through alias', + [ + { + 'method': 'GET', + 'path': '/qos/alias_dscp_marking_rules/{rule_id}/', + }, + ] + ), + policy.DocumentedRuleDefault( + 'update_alias_dscp_marking_rule', + 'rule:update_policy_dscp_marking_rule', + 'Update a QoS DSCP marking rule through alias', + [ + { + 'method': 'PUT', + 'path': '/qos/alias_dscp_marking_rules/{rule_id}/', + }, + ] + ), + policy.DocumentedRuleDefault( + 'delete_alias_dscp_marking_rule', + 'rule:delete_policy_dscp_marking_rule', + 'Delete a QoS DSCP marking rule through alias', + [ + { + 'method': 'DELETE', + 'path': '/qos/alias_dscp_marking_rules/{rule_id}/', + }, + ] + ), + policy.DocumentedRuleDefault( + 'get_alias_minimum_bandwidth_rule', + 'rule:get_policy_minimum_bandwidth_rule', + 'Get a QoS minimum bandwidth rule through alias', + [ + { + 'method': 'GET', + 'path': '/qos/alias_minimum_bandwidth_rules/{rule_id}/', + }, + ] + ), + policy.DocumentedRuleDefault( + 'update_alias_minimum_bandwidth_rule', + 'rule:update_policy_minimum_bandwidth_rule', + 'Update a QoS minimum bandwidth rule through alias', + [ + { + 'method': 'PUT', + 'path': '/qos/alias_minimum_bandwidth_rules/{rule_id}/', + }, + ] + ), + policy.DocumentedRuleDefault( + 'delete_alias_minimum_bandwidth_rule', + 'rule:delete_policy_minimum_bandwidth_rule', + 'Delete a QoS minimum bandwidth rule through alias', + [ + { + 'method': 'DELETE', + 'path': '/qos/alias_minimum_bandwidth_rules/{rule_id}/', + }, + ] + ), ] diff --git a/neutron/extensions/qos.py b/neutron/extensions/qos.py index e04a44d8928..78675661252 100644 --- a/neutron/extensions/qos.py +++ b/neutron/extensions/qos.py @@ -97,6 +97,9 @@ class QoSPluginBase(service_base.ServicePluginBase): r"^((create|update|delete)_policy_(?P.*)_rule)$"), re.compile( r"^(get_policy_(?P.*)_(rules|rule))$"), + # The following entry handles rule alias calls + re.compile( + r"^((update|delete|get)_alias_(?P.*)_rule)$"), ] def __getattr__(self, attrib): @@ -106,6 +109,10 @@ class QoSPluginBase(service_base.ServicePluginBase): update_policy_rule method will handle requests for both update_policy_dscp_marking_rule and update_policy_bandwidth_limit_rule. + In the case of rule alias calls, the update_rule method will handle + requests for both update_dscp_marking_rule and + update_bandwidth_limit_rule. + :param attrib: the requested method; in the normal case, this will be, for example, "update_policy_dscp_marking_rule" :type attrib: str @@ -121,6 +128,7 @@ class QoSPluginBase(service_base.ServicePluginBase): # from "delete_policy_dscp_marking_rule" we'll get # "delete_policy_rule". proxy_method = attrib.replace(rule_type + '_', '') + proxy_method = proxy_method.replace('alias_', '') rule_cls = self.rule_objects[rule_type] return self._call_proxy_method(proxy_method, rule_cls) @@ -163,8 +171,11 @@ class QoSPluginBase(service_base.ServicePluginBase): args_list = list(args[1:]) params = kwargs rule_data_name = rule_cls.rule_type + "_rule" + alias_rule_data_name = 'alias_' + rule_data_name if rule_data_name in params: params['rule_data'] = params.pop(rule_data_name) + elif alias_rule_data_name in params: + params['rule_data'] = params.pop(alias_rule_data_name) return getattr(self, method_name)( context, rule_cls, *args_list, **params @@ -233,3 +244,15 @@ class QoSPluginBase(service_base.ServicePluginBase): filters=None, fields=None, sorts=None, limit=None, marker=None, page_reverse=False): pass + + @abc.abstractmethod + def update_rule(self, context, rule_cls, rule_id, rule_data): + pass + + @abc.abstractmethod + def delete_rule(self, context, rule_cls, rule_id): + pass + + @abc.abstractmethod + def get_rule(self, context, rule_cls, rule_id, fields=None): + pass diff --git a/neutron/extensions/qos_rules_alias.py b/neutron/extensions/qos_rules_alias.py new file mode 100644 index 00000000000..99dd53a1d94 --- /dev/null +++ b/neutron/extensions/qos_rules_alias.py @@ -0,0 +1,37 @@ +# Copyright (c) 2018 Huawei Technology, Inc. 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.definitions import qos_rules_alias as apidef +from neutron_lib.api import extensions as api_extensions +from neutron_lib.plugins import constants + +from neutron.api.v2 import resource_helper + + +class Qos_rules_alias(api_extensions.APIExtensionDescriptor): + """Extension class supporting QoS rules alias API resources.""" + api_definition = apidef + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + plural_mappings = resource_helper.build_plural_mappings( + {}, apidef.RESOURCE_ATTRIBUTE_MAP) + + return resource_helper.build_resource_info( + plural_mappings, + apidef.RESOURCE_ATTRIBUTE_MAP, + constants.QOS, + translate_name=True, + allow_bulk=True) diff --git a/neutron/services/qos/qos_plugin.py b/neutron/services/qos/qos_plugin.py index be1ed61ff7f..df450367b2d 100644 --- a/neutron/services/qos/qos_plugin.py +++ b/neutron/services/qos/qos_plugin.py @@ -18,6 +18,7 @@ from neutron_lib.api.definitions import port_resource_request from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import qos as qos_apidef from neutron_lib.api.definitions import qos_bw_minimum_ingress +from neutron_lib.api.definitions import qos_rules_alias 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 @@ -56,7 +57,8 @@ class QoSPlugin(qos.QoSPluginBase): 'qos-default', 'qos-rule-type-details', port_resource_request.ALIAS, - qos_bw_minimum_ingress.ALIAS] + qos_bw_minimum_ingress.ALIAS, + qos_rules_alias.ALIAS] __native_pagination_support = True __native_sorting_support = True @@ -453,6 +455,37 @@ class QoSPlugin(qos.QoSPluginBase): return rule + def _get_policy_id(self, context, rule_cls, rule_id): + with db_api.autonested_transaction(context.session): + rule_object = rule_cls.get_object(context, id=rule_id) + if not rule_object: + raise qos_exc.QosRuleNotFound(policy_id="", rule_id=rule_id) + return rule_object.qos_policy_id + + def update_rule(self, context, rule_cls, rule_id, rule_data): + """Update a QoS policy rule alias. This method processes a QoS policy + rule update, where the rule is an API first level resource instead of a + subresource of a policy. + + :param context: neutron api request context + :type context: neutron.context.Context + :param rule_cls: the rule object class + :type rule_cls: a class from the rule_object (qos.objects.rule) module + :param rule_id: the id of the QoS policy rule to update + :type rule_id: str uuid + :param rule_data: the new rule data to update + :type rule_data: dict + + :returns: a QoS policy rule object + :raises: qos_exc.QosRuleNotFound + """ + policy_id = self._get_policy_id(context, rule_cls, rule_id) + rule_data_name = rule_cls.rule_type + '_rule' + alias_rule_data_name = 'alias_' + rule_data_name + rule_data[rule_data_name] = rule_data.pop(alias_rule_data_name) + return self.update_policy_rule(context, rule_cls, rule_id, policy_id, + rule_data) + def delete_policy_rule(self, context, rule_cls, rule_id, policy_id): """Delete a QoS policy rule. @@ -480,6 +513,24 @@ class QoSPlugin(qos.QoSPluginBase): self.driver_manager.call(qos_consts.UPDATE_POLICY, context, policy) + def delete_rule(self, context, rule_cls, rule_id): + """Delete a QoS policy rule alias. This method processes a QoS policy + rule delete, where the rule is an API first level resource instead of a + subresource of a policy. + + :param context: neutron api request context + :type context: neutron.context.Context + :param rule_cls: the rule object class + :type rule_cls: a class from the rule_object (qos.objects.rule) module + :param rule_id: the id of the QosPolicy Rule to delete + :type rule_id: str uuid + + :returns: None + :raises: qos_exc.QosRuleNotFound + """ + policy_id = self._get_policy_id(context, rule_cls, rule_id) + return self.delete_policy_rule(context, rule_cls, rule_id, policy_id) + @db_base_plugin_common.filter_fields @db_base_plugin_common.convert_result_to_dict def get_policy_rule(self, context, rule_cls, rule_id, policy_id, @@ -506,6 +557,24 @@ class QoSPlugin(qos.QoSPluginBase): raise qos_exc.QosRuleNotFound(policy_id=policy_id, rule_id=rule_id) return rule + def get_rule(self, context, rule_cls, rule_id, fields=None): + """Get a QoS policy rule alias. This method processes a QoS policy + rule get, where the rule is an API first level resource instead of a + subresource of a policy + + :param context: neutron api request context + :type context: neutron.context.Context + :param rule_cls: the rule object class + :type rule_cls: a class from the rule_object (qos.objects.rule) module + :param rule_id: the id of the QoS policy rule to get + :type rule_id: str uuid + + :returns: a QoS policy rule object + :raises: qos_exc.QosRuleNotFound + """ + policy_id = self._get_policy_id(context, rule_cls, rule_id) + return self.get_policy_rule(context, rule_cls, rule_id, policy_id) + # TODO(QoS): enforce rule types when accessing rule objects @db_base_plugin_common.filter_fields @db_base_plugin_common.convert_result_to_dict diff --git a/neutron/tests/unit/services/qos/test_qos_plugin.py b/neutron/tests/unit/services/qos/test_qos_plugin.py index a246c36dc05..9f2009155b1 100644 --- a/neutron/tests/unit/services/qos/test_qos_plugin.py +++ b/neutron/tests/unit/services/qos/test_qos_plugin.py @@ -13,6 +13,7 @@ import copy import mock +from neutron_lib.api.definitions import qos from neutron_lib.callbacks import events from neutron_lib import constants as lib_constants from neutron_lib import context @@ -25,16 +26,20 @@ from neutron_lib.plugins import directory from neutron_lib.services.qos import constants as qos_consts from oslo_config import cfg from oslo_utils import uuidutils +import webob.exc from neutron.common import constants +from neutron.extensions import qos_rules_alias from neutron import manager from neutron.objects.qos import policy as policy_object from neutron.objects.qos import rule as rule_object from neutron.services.qos import qos_plugin +from neutron.tests.unit.db import test_db_base_plugin_v2 from neutron.tests.unit.services.qos import base DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2' +SERVICE_PLUGIN_KLASS = 'neutron.services.qos.qos_plugin.QoSPlugin' class TestQosPlugin(base.BaseQosTestCase): @@ -1054,3 +1059,140 @@ class TestQosPlugin(base.BaseQosTestCase): self.assertLess( action_index, mock_manager.mock_calls.index(driver_mock_call)) + + +class QoSRuleAliasTestExtensionManager(object): + + def get_resources(self): + return qos_rules_alias.Qos_rules_alias.get_resources() + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class TestQoSRuleAlias(test_db_base_plugin_v2.NeutronDbPluginV2TestCase): + + def setUp(self): + # Remove MissingAuthPlugin exception from logs + self.patch_notifier = mock.patch( + 'neutron.notifiers.batch_notifier.BatchNotifier._notify') + self.patch_notifier.start() + plugin = 'ml2' + service_plugins = {'qos_plugin_name': SERVICE_PLUGIN_KLASS} + ext_mgr = QoSRuleAliasTestExtensionManager() + super(TestQoSRuleAlias, self).setUp(plugin=plugin, ext_mgr=ext_mgr, + service_plugins=service_plugins) + self.qos_plugin = directory.get_plugin(plugins_constants.QOS) + + self.ctxt = context.Context('fake_user', 'fake_tenant') + self.rule_objects = { + 'bandwidth_limit': rule_object.QosBandwidthLimitRule, + 'dscp_marking': rule_object.QosDscpMarkingRule, + 'minimum_bandwidth': rule_object.QosMinimumBandwidthRule + } + + self.qos_policy_id = uuidutils.generate_uuid() + self.rule_data = { + 'bandwidth_limit_rule': {'max_kbps': 100, + 'max_burst_kbps': 150}, + 'dscp_marking_rule': {'dscp_mark': 16}, + 'minimum_bandwidth_rule': {'min_kbps': 10} + } + + def _update_rule(self, rule_type, rule_id, **kwargs): + data = {'alias_%s_rule' % rule_type: kwargs} + resource = '%s/alias-%s-rules' % (qos.ALIAS, + rule_type.replace('_', '-')) + request = self.new_update_request(resource, data, rule_id, self.fmt) + res = request.get_response(self.ext_api) + if res.status_int >= webob.exc.HTTPClientError.code: + raise webob.exc.HTTPClientError(code=res.status_int) + return self.deserialize(self.fmt, res) + + def _show_rule(self, rule_type, rule_id): + resource = '%s/alias-%s-rules' % (qos.ALIAS, + rule_type.replace('_', '-')) + request = self.new_show_request(resource, rule_id, self.fmt) + res = request.get_response(self.ext_api) + if res.status_int >= webob.exc.HTTPClientError.code: + raise webob.exc.HTTPClientError(code=res.status_int) + return self.deserialize(self.fmt, res) + + def _delete_rule(self, rule_type, rule_id): + resource = '%s/alias-%s-rules' % (qos.ALIAS, + rule_type.replace('_', '-')) + request = self.new_delete_request(resource, rule_id, self.fmt) + res = request.get_response(self.ext_api) + if res.status_int >= webob.exc.HTTPClientError.code: + raise webob.exc.HTTPClientError(code=res.status_int) + + @mock.patch.object(qos_plugin.QoSPlugin, "update_policy_rule") + def test_update_rule(self, update_policy_rule_mock): + calls = [] + for rule_type, rule_object_class in self.rule_objects.items(): + rule_id = uuidutils.generate_uuid() + rule_data_name = '%s_rule' % rule_type + data = self.rule_data[rule_data_name] + rule = rule_object_class(self.ctxt, id=rule_id, + qos_policy_id=self.qos_policy_id, + **data) + with mock.patch( + 'neutron.objects.qos.rule.QosRule.get_object', + return_value=rule + ), mock.patch.object(self.qos_plugin, 'get_policy_rule', + return_value=rule.to_dict()): + self._update_rule(rule_type, rule_id, **data) + calls.append(mock.call(mock.ANY, rule_object_class, rule_id, + self.qos_policy_id, {rule_data_name: data})) + update_policy_rule_mock.assert_has_calls(calls, any_order=True) + + @mock.patch.object(qos_plugin.QoSPlugin, "get_policy_rule") + def test_show_rule(self, get_policy_rule_mock): + calls = [] + for rule_type, rule_object_class in self.rule_objects.items(): + rule_id = uuidutils.generate_uuid() + rule_data_name = '%s_rule' % rule_type + data = self.rule_data[rule_data_name] + rule = rule_object_class(self.ctxt, id=rule_id, + qos_policy_id=self.qos_policy_id, + **data) + with mock.patch('neutron.objects.qos.rule.QosRule.get_object', + return_value=rule): + self._show_rule(rule_type, rule_id) + calls.append(mock.call(mock.ANY, rule_object_class, rule_id, + self.qos_policy_id)) + get_policy_rule_mock.assert_has_calls(calls, any_order=True) + + @mock.patch.object(qos_plugin.QoSPlugin, "delete_policy_rule") + def test_delete_rule(self, delete_policy_rule_mock): + calls = [] + for rule_type, rule_object_class in self.rule_objects.items(): + rule_id = uuidutils.generate_uuid() + rule_data_name = '%s_rule' % rule_type + data = self.rule_data[rule_data_name] + rule = rule_object_class(self.ctxt, id=rule_id, + qos_policy_id=self.qos_policy_id, + **data) + with mock.patch( + 'neutron.objects.qos.rule.QosRule.get_object', + return_value=rule + ), mock.patch.object(self.qos_plugin, 'get_policy_rule', + return_value=rule.to_dict()): + self._delete_rule(rule_type, rule_id) + calls.append(mock.call(mock.ANY, rule_object_class, rule_id, + self.qos_policy_id)) + delete_policy_rule_mock.assert_has_calls(calls, any_order=True) + + def test_show_non_existing_rule(self): + for rule_type, rule_object_class in self.rule_objects.items(): + rule_id = uuidutils.generate_uuid() + with mock.patch('neutron.objects.qos.rule.QosRule.get_object', + return_value=None): + resource = '%s/alias-%s-rules' % (qos.ALIAS, + rule_type.replace('_', '-')) + request = self.new_show_request(resource, rule_id, self.fmt) + res = request.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int) diff --git a/releasenotes/notes/qos-rules-alias-extension-ebf23b87460ee36e.yaml b/releasenotes/notes/qos-rules-alias-extension-ebf23b87460ee36e.yaml new file mode 100644 index 00000000000..530a409d468 --- /dev/null +++ b/releasenotes/notes/qos-rules-alias-extension-ebf23b87460ee36e.yaml @@ -0,0 +1,9 @@ +--- +prelude: > + Support alias end points for rules in QoS API. +features: + - | + The ``qos-rules-alias`` API extension was implemented to enable users to + perform GET, PUT and DELETE operations on QoS rules as though they are + first level resources. In other words, the user doesn't have to specify the + QoS policy ID.