From 636f1b539446c27c703744159c10bc08739f8937 Mon Sep 17 00:00:00 2001 From: Miguel Lavalle Date: Sun, 28 Oct 2018 20:12:29 -0500 Subject: [PATCH] Define qos-rules-alias extension This patch adds qos-rules-alias extension 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. Change-Id: Ia7535d83e3ae874106e22652dfd97bd9250ad37b Partial-Bug: #1777627 --- neutron/conf/policies/qos.py | 99 ++++++++++++ neutron/extensions/qos.py | 23 +++ neutron/extensions/qos_rules_alias.py | 37 +++++ neutron/services/qos/qos_plugin.py | 71 ++++++++- .../unit/services/qos/test_qos_plugin.py | 142 ++++++++++++++++++ ...ules-alias-extension-ebf23b87460ee36e.yaml | 9 ++ 6 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 neutron/extensions/qos_rules_alias.py create mode 100644 releasenotes/notes/qos-rules-alias-extension-ebf23b87460ee36e.yaml 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.