From c29f3aaa7c20c89cb1eac818d2981c16fd484ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 24 Mar 2017 22:04:53 +0000 Subject: [PATCH] Add QoS bandwidth limit for instance ingress traffic This patch introduces the new parameter "direction" to the QoS bandwidth limit rule. It will allow the creation of bandwidth limit rules for either ingress or egress traffic. For backwards compatibility the default direction will be egress. DocImpact: Ingress bandwidth limit available for QoS APIImpact: New type of parameter for QoS rule in neutron API Change-Id: Ia13568879c2b6f80fb190ccafe7e19ca05b0c6a8 Partial-Bug: #1560961 --- .../alembic_migrations/versions/EXPAND_HEAD | 2 +- ...os_add_direction_to_bw_limit_rule_table.py | 81 ++++++++++++++++++ neutron/db/qos/models.py | 15 +++- neutron/extensions/qos.py | 14 +++- neutron/extensions/qos_bw_limit_direction.py | 84 +++++++++++++++++++ neutron/objects/qos/policy.py | 22 ++++- neutron/objects/qos/rule.py | 17 +++- .../qos/drivers/linuxbridge/driver.py | 4 +- .../qos/drivers/openvswitch/driver.py | 4 +- neutron/services/qos/drivers/sriov/driver.py | 4 +- neutron/services/qos/qos_plugin.py | 2 +- neutron/tests/tempest/api/base.py | 7 +- neutron/tests/tempest/api/test_qos.py | 80 ++++++++++++++---- .../services/network/json/network_client.py | 13 ++- neutron/tests/unit/objects/qos/test_policy.py | 51 +++++++++-- neutron/tests/unit/objects/qos/test_rule.py | 20 +++++ neutron/tests/unit/objects/test_objects.py | 8 +- ...ress-bandwidth-limit-54cea12dbea71172.yaml | 6 ++ 18 files changed, 384 insertions(+), 50 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/pike/expand/2b42d90729da_qos_add_direction_to_bw_limit_rule_table.py create mode 100644 neutron/extensions/qos_bw_limit_direction.py create mode 100644 releasenotes/notes/QoS-ingress-bandwidth-limit-54cea12dbea71172.yaml diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 8c1796ba3b3..fdd9050af31 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -804a3c76314c +2b42d90729da diff --git a/neutron/db/migration/alembic_migrations/versions/pike/expand/2b42d90729da_qos_add_direction_to_bw_limit_rule_table.py b/neutron/db/migration/alembic_migrations/versions/pike/expand/2b42d90729da_qos_add_direction_to_bw_limit_rule_table.py new file mode 100644 index 00000000000..cf6616339c2 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/pike/expand/2b42d90729da_qos_add_direction_to_bw_limit_rule_table.py @@ -0,0 +1,81 @@ +# Copyright 2017 OpenStack Foundation +# +# 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. +# + +"""qos add direction to bw_limit_rule table + +Revision ID: 2b42d90729da +Revises: 804a3c76314c +Create Date: 2017-04-03 20:56:00.169599 + +""" + +# revision identifiers, used by Alembic. +revision = '2b42d90729da' +down_revision = '804a3c76314c' + +from alembic import op +import sqlalchemy as sa + +from neutron.common import constants +from neutron.db import migration + + +policies_table_name = "qos_policies" +bw_limit_table_name = "qos_bandwidth_limit_rules" +direction_enum = sa.Enum( + constants.EGRESS_DIRECTION, constants.INGRESS_DIRECTION, + name="directions" +) + + +def upgrade(): + if op.get_context().bind.dialect.name == 'postgresql': + direction_enum.create(op.get_bind(), checkfirst=True) + + with migration.remove_fks_from_table(bw_limit_table_name, + remove_unique_constraints=True): + op.add_column(bw_limit_table_name, + sa.Column("direction", direction_enum, + server_default=constants.EGRESS_DIRECTION, + nullable=False)) + + op.create_unique_constraint( + op.f('qos_bandwidth_rules0qos_policy_id0direction'), + bw_limit_table_name, + ['qos_policy_id', 'direction']) + + +def expand_drop_exceptions(): + """ + Drop the existing QoS policy foreign key uniq constraint and then replace + it with new unique constraint for pair (policy_id, direction). + + As names of constraints are different in MySQL and PGSQL there is need to + add both variants to drop exceptions. + """ + + # TODO(slaweq): replace hardcoded constaints names with names get directly + # from database model after bug + # https://bugs.launchpad.net/neutron/+bug/1685352 will be closed + return { + sa.ForeignKeyConstraint: [ + "qos_bandwidth_limit_rules_ibfk_1", # MySQL name + "qos_bandwidth_limit_rules_qos_policy_id_fkey" # PGSQL name + ], + sa.UniqueConstraint: [ + "qos_policy_id", # MySQL name + "qos_bandwidth_limit_rules_qos_policy_id_key" # PGSQL name + ] + } diff --git a/neutron/db/qos/models.py b/neutron/db/qos/models.py index 2afe4260fef..da5f9b9b31a 100644 --- a/neutron/db/qos/models.py +++ b/neutron/db/qos/models.py @@ -78,12 +78,23 @@ class QosBandwidthLimitRule(model_base.HasId, model_base.BASEV2): qos_policy_id = sa.Column(sa.String(36), sa.ForeignKey('qos_policies.id', ondelete='CASCADE'), - nullable=False, - unique=True) + nullable=False) max_kbps = sa.Column(sa.Integer) max_burst_kbps = sa.Column(sa.Integer) revises_on_change = ('qos_policy', ) qos_policy = sa.orm.relationship(QosPolicy, load_on_pending=True) + direction = sa.Column(sa.Enum(constants.EGRESS_DIRECTION, + constants.INGRESS_DIRECTION, + name="directions"), + default=constants.EGRESS_DIRECTION, + server_default=constants.EGRESS_DIRECTION, + nullable=False) + __table_args__ = ( + sa.UniqueConstraint( + qos_policy_id, direction, + name="qos_bandwidth_rules0qos_policy_id0direction"), + model_base.BASEV2.__table_args__ + ) class QosDscpMarkingRule(model_base.HasId, model_base.BASEV2): diff --git a/neutron/extensions/qos.py b/neutron/extensions/qos.py index 81a55477fa4..020bca40e86 100644 --- a/neutron/extensions/qos.py +++ b/neutron/extensions/qos.py @@ -32,6 +32,7 @@ from neutron.objects.qos import rule as rule_object from neutron.plugins.common import constants from neutron.services.qos import qos_consts +ALIAS = "qos" QOS_PREFIX = "/qos" # Attribute Map @@ -74,8 +75,10 @@ RESOURCE_ATTRIBUTE_MAP = { } } +BANDWIDTH_LIMIT_RULES = "bandwidth_limit_rules" + SUB_RESOURCE_ATTRIBUTE_MAP = { - 'bandwidth_limit_rules': { + BANDWIDTH_LIMIT_RULES: { 'parent': {'collection_name': 'policies', 'member_name': 'policy'}, 'parameters': dict(QOS_RULE_COMMON_FIELDS, @@ -88,7 +91,7 @@ SUB_RESOURCE_ATTRIBUTE_MAP = { 'allow_post': True, 'allow_put': True, 'is_visible': True, 'default': 0, 'validate': {'type:range': [0, - common_constants.DB_INTEGER_MAX_VALUE]}}}) + common_constants.DB_INTEGER_MAX_VALUE]}}}), }, 'dscp_marking_rules': { 'parent': {'collection_name': 'policies', @@ -196,12 +199,15 @@ class Qos(api_extensions.ExtensionDescriptor): def update_attributes_map(self, attributes, extension_attrs_map=None): super(Qos, self).update_attributes_map( - attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP) + attributes, + extension_attrs_map=dict(list(RESOURCE_ATTRIBUTE_MAP.items()) + + list(SUB_RESOURCE_ATTRIBUTE_MAP.items()))) def get_extended_resources(self, version): if version == "2.0": return dict(list(EXTENDED_ATTRIBUTES_2_0.items()) + - list(RESOURCE_ATTRIBUTE_MAP.items())) + list(RESOURCE_ATTRIBUTE_MAP.items()) + + list(SUB_RESOURCE_ATTRIBUTE_MAP.items())) else: return {} diff --git a/neutron/extensions/qos_bw_limit_direction.py b/neutron/extensions/qos_bw_limit_direction.py new file mode 100644 index 00000000000..c8f51aedda9 --- /dev/null +++ b/neutron/extensions/qos_bw_limit_direction.py @@ -0,0 +1,84 @@ +# 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.common import constants as common_constants +from neutron.extensions import qos + + +# The name of the extension. +NAME = "Direction for QoS bandwidth limit rule" + +# The alias of the extension. +ALIAS = "qos-bw-limit-direction" + +# The description of the extension. +DESCRIPTION = ("Allow to configure QoS bandwidth limit rule with specific " + "direction: ingress or egress") + +# The list of required extensions. +REQUIRED_EXTENSIONS = [qos.ALIAS] + +# The list of optional extensions. +OPTIONAL_EXTENSIONS = None + +# The resource attribute map for the extension. +SUB_RESOURCE_ATTRIBUTE_MAP = { + qos.BANDWIDTH_LIMIT_RULES: { + 'parameters': dict( + qos.SUB_RESOURCE_ATTRIBUTE_MAP[ + qos.BANDWIDTH_LIMIT_RULES]['parameters'], + **{'direction': { + 'allow_post': True, + 'allow_put': True, + 'is_visible': True, + 'default': common_constants.EGRESS_DIRECTION, + 'validate': { + 'type:values': common_constants.VALID_DIRECTIONS}}} + ) + } +} + + +class Qos_bw_limit_direction(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-04-10T10: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 SUB_RESOURCE_ATTRIBUTE_MAP + else: + return {} diff --git a/neutron/objects/qos/policy.py b/neutron/objects/qos/policy.py index dfb84b85d74..8057f4131de 100644 --- a/neutron/objects/qos/policy.py +++ b/neutron/objects/qos/policy.py @@ -21,6 +21,7 @@ from oslo_versionedobjects import exception from oslo_versionedobjects import fields as obj_fields from neutron._i18n import _ +from neutron.common import constants as n_const from neutron.common import exceptions from neutron.db import api as db_api from neutron.db import models_v2 @@ -40,7 +41,8 @@ class QosPolicy(rbac_db.NeutronRbacObject): # Version 1.2: Added QosMinimumBandwidthRule # Version 1.3: Added standard attributes (created_at, revision, etc) # Version 1.4: Changed tenant_id to project_id - VERSION = '1.4' + # Version 1.5: Direction for bandwidth limit rule added + VERSION = '1.5' # required by RbacNeutronMetaclass rbac_db_model = QosPolicyRBAC @@ -222,6 +224,19 @@ class QosPolicy(rbac_db.NeutronRbacObject): return [rule for rule in rules if rule['versioned_object.name'] in obj_names] + def filter_ingress_bandwidth_limit_rules(rules): + bwlimit_obj_name = rule_obj_impl.QosBandwidthLimitRule.obj_name() + filtered_rules = [] + for rule in rules: + if rule['versioned_object.name'] == bwlimit_obj_name: + direction = rule['versioned_object.data'].get("direction") + if direction == n_const.EGRESS_DIRECTION: + rule['versioned_object.data'].pop('direction') + filtered_rules.append(rule) + else: + filtered_rules.append(rule) + return filtered_rules + _target_version = versionutils.convert_version_to_tuple(target_version) names = [] if _target_version >= (1, 0): @@ -244,3 +259,8 @@ class QosPolicy(rbac_db.NeutronRbacObject): if _target_version < (1, 4): primitive['tenant_id'] = primitive.pop('project_id') + + if _target_version < (1, 5): + if 'rules' in primitive: + primitive['rules'] = filter_ingress_bandwidth_limit_rules( + primitive['rules']) diff --git a/neutron/objects/qos/rule.py b/neutron/objects/qos/rule.py index d24f808e90f..f53f2ffcb8d 100644 --- a/neutron/objects/qos/rule.py +++ b/neutron/objects/qos/rule.py @@ -24,6 +24,7 @@ from oslo_versionedobjects import exception from oslo_versionedobjects import fields as obj_fields import six +from neutron.common import constants as n_const from neutron.db import api as db_api from neutron.db.qos import models as qos_db_model from neutron.objects import base @@ -50,11 +51,12 @@ class QosRule(base.NeutronDbObject): # Version 1.0: Initial version, only BandwidthLimitRule # 1.1: Added DscpMarkingRule # 1.2: Added QosMinimumBandwidthRule + # 1.3: Added direction for BandwidthLimitRule # #NOTE(mangelajo): versions need to be handled from the top QosRule object # because it's the only reference QosPolicy can make # to them via obj_relationships version map - VERSION = '1.2' + VERSION = '1.3' fields = { 'id': common_types.UUIDField(), @@ -106,11 +108,22 @@ class QosBandwidthLimitRule(QosRule): fields = { 'max_kbps': obj_fields.IntegerField(nullable=True), - 'max_burst_kbps': obj_fields.IntegerField(nullable=True) + 'max_burst_kbps': obj_fields.IntegerField(nullable=True), + 'direction': common_types.FlowDirectionEnumField( + default=n_const.EGRESS_DIRECTION) } rule_type = qos_consts.RULE_TYPE_BANDWIDTH_LIMIT + def obj_make_compatible(self, primitive, target_version): + _target_version = versionutils.convert_version_to_tuple(target_version) + if _target_version < (1, 3) and 'direction' in primitive: + direction = primitive.pop('direction') + if direction == n_const.INGRESS_DIRECTION: + raise exception.IncompatibleObjectVersion( + objver=target_version, + objtype="QosBandwidthLimitRule") + @obj_base.VersionedObjectRegistry.register class QosDscpMarkingRule(QosRule): diff --git a/neutron/services/qos/drivers/linuxbridge/driver.py b/neutron/services/qos/drivers/linuxbridge/driver.py index 5cc3de65274..57e4394e3cf 100644 --- a/neutron/services/qos/drivers/linuxbridge/driver.py +++ b/neutron/services/qos/drivers/linuxbridge/driver.py @@ -29,7 +29,9 @@ SUPPORTED_RULES = { qos_consts.MAX_KBPS: { 'type:range': [0, constants.DB_INTEGER_MAX_VALUE]}, qos_consts.MAX_BURST: { - 'type:range': [0, constants.DB_INTEGER_MAX_VALUE]} + 'type:range': [0, constants.DB_INTEGER_MAX_VALUE]}, + qos_consts.DIRECTION: { + 'type:values': [constants.EGRESS_DIRECTION]} }, qos_consts.RULE_TYPE_DSCP_MARKING: { qos_consts.DSCP_MARK: {'type:values': constants.VALID_DSCP_MARKS} diff --git a/neutron/services/qos/drivers/openvswitch/driver.py b/neutron/services/qos/drivers/openvswitch/driver.py index 9ee4f908e9a..c54da7188cb 100644 --- a/neutron/services/qos/drivers/openvswitch/driver.py +++ b/neutron/services/qos/drivers/openvswitch/driver.py @@ -29,7 +29,9 @@ SUPPORTED_RULES = { qos_consts.MAX_KBPS: { 'type:range': [0, constants.DB_INTEGER_MAX_VALUE]}, qos_consts.MAX_BURST: { - 'type:range': [0, constants.DB_INTEGER_MAX_VALUE]} + 'type:range': [0, constants.DB_INTEGER_MAX_VALUE]}, + qos_consts.DIRECTION: { + 'type:values': [constants.EGRESS_DIRECTION]} }, qos_consts.RULE_TYPE_DSCP_MARKING: { qos_consts.DSCP_MARK: {'type:values': constants.VALID_DSCP_MARKS} diff --git a/neutron/services/qos/drivers/sriov/driver.py b/neutron/services/qos/drivers/sriov/driver.py index b9bb7cd923e..3883a70154f 100644 --- a/neutron/services/qos/drivers/sriov/driver.py +++ b/neutron/services/qos/drivers/sriov/driver.py @@ -29,7 +29,9 @@ SUPPORTED_RULES = { qos_consts.MAX_KBPS: { 'type:range': [0, constants.DB_INTEGER_MAX_VALUE]}, qos_consts.MAX_BURST: { - 'type:range': [0, constants.DB_INTEGER_MAX_VALUE]} + 'type:range': [0, constants.DB_INTEGER_MAX_VALUE]}, + qos_consts.DIRECTION: { + 'type:values': [constants.EGRESS_DIRECTION]} }, qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH: { qos_consts.MIN_KBPS: { diff --git a/neutron/services/qos/qos_plugin.py b/neutron/services/qos/qos_plugin.py index 3e89366e221..a4986e7a900 100644 --- a/neutron/services/qos/qos_plugin.py +++ b/neutron/services/qos/qos_plugin.py @@ -36,7 +36,7 @@ class QoSPlugin(qos.QoSPluginBase): service parameters over ports and networks. """ - supported_extension_aliases = ['qos'] + supported_extension_aliases = ['qos', 'qos-bw-limit-direction'] __native_pagination_support = True __native_sorting_support = True diff --git a/neutron/tests/tempest/api/base.py b/neutron/tests/tempest/api/base.py index 49c48d65d25..55ad35560c4 100644 --- a/neutron/tests/tempest/api/base.py +++ b/neutron/tests/tempest/api/base.py @@ -374,11 +374,12 @@ class BaseNetworkTest(test.BaseTestCase): return qos_policy @classmethod - def create_qos_bandwidth_limit_rule(cls, policy_id, - max_kbps, max_burst_kbps): + def create_qos_bandwidth_limit_rule(cls, policy_id, max_kbps, + max_burst_kbps, + direction=constants.EGRESS_DIRECTION): """Wrapper utility that returns a test QoS bandwidth limit rule.""" body = cls.admin_client.create_bandwidth_limit_rule( - policy_id, max_kbps, max_burst_kbps) + policy_id, max_kbps, max_burst_kbps, direction) qos_rule = body['bandwidth_limit_rule'] cls.qos_rules.append(qos_rule) return qos_rule diff --git a/neutron/tests/tempest/api/test_qos.py b/neutron/tests/tempest/api/test_qos.py index 104fb9b5e40..728029786f7 100644 --- a/neutron/tests/tempest/api/test_qos.py +++ b/neutron/tests/tempest/api/test_qos.py @@ -17,12 +17,16 @@ from tempest.lib import decorators from tempest.lib import exceptions from tempest import test +import testscenarios import testtools from neutron.services.qos import qos_consts from neutron.tests.tempest.api import base +load_tests = testscenarios.load_tests_apply_scenarios + + class QosTestJSON(base.BaseAdminNetworkTest): @classmethod @test.requires_ext(extension="qos", service="network") @@ -360,20 +364,34 @@ class QosTestJSON(base.BaseAdminNetworkTest): class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): + + direction = None + @classmethod @test.requires_ext(extension="qos", service="network") @base.require_qos_rule_type(qos_consts.RULE_TYPE_BANDWIDTH_LIMIT) def resource_setup(cls): super(QosBandwidthLimitRuleTestJSON, cls).resource_setup() + @property + def opposite_direction(self): + if self.direction == "ingress": + return "egress" + elif self.direction == "egress": + return "ingress" + else: + return None + @decorators.idempotent_id('8a59b00b-3e9c-4787-92f8-93a5cdf5e378') def test_rule_create(self): policy = self.create_qos_policy(name='test-policy', description='test policy', shared=False) - rule = self.create_qos_bandwidth_limit_rule(policy_id=policy['id'], - max_kbps=200, - max_burst_kbps=1337) + rule = self.create_qos_bandwidth_limit_rule( + policy_id=policy['id'], + max_kbps=200, + max_burst_kbps=1337, + direction=self.direction) # Test 'show rule' retrieved_rule = self.admin_client.show_bandwidth_limit_rule( @@ -382,6 +400,8 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): self.assertEqual(rule['id'], retrieved_rule['id']) self.assertEqual(200, retrieved_rule['max_kbps']) self.assertEqual(1337, retrieved_rule['max_burst_kbps']) + if self.direction: + self.assertEqual(self.direction, retrieved_rule['direction']) # Test 'list rules' rules = self.admin_client.list_bandwidth_limit_rules(policy['id']) @@ -404,12 +424,14 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): shared=False) self.create_qos_bandwidth_limit_rule(policy_id=policy['id'], max_kbps=200, - max_burst_kbps=1337) + max_burst_kbps=1337, + direction=self.direction) self.assertRaises(exceptions.Conflict, self.create_qos_bandwidth_limit_rule, policy_id=policy['id'], - max_kbps=201, max_burst_kbps=1338) + max_kbps=201, max_burst_kbps=1338, + direction=self.direction) @decorators.idempotent_id('149a6988-2568-47d2-931e-2dbc858943b3') def test_rule_update(self): @@ -418,18 +440,24 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): shared=False) rule = self.create_qos_bandwidth_limit_rule(policy_id=policy['id'], max_kbps=1, - max_burst_kbps=1) + max_burst_kbps=1, + direction=self.direction) - self.admin_client.update_bandwidth_limit_rule(policy['id'], - rule['id'], - max_kbps=200, - max_burst_kbps=1337) + self.admin_client.update_bandwidth_limit_rule( + policy['id'], + rule['id'], + max_kbps=200, + max_burst_kbps=1337, + direction=self.opposite_direction) retrieved_policy = self.admin_client.show_bandwidth_limit_rule( policy['id'], rule['id']) retrieved_policy = retrieved_policy['bandwidth_limit_rule'] self.assertEqual(200, retrieved_policy['max_kbps']) self.assertEqual(1337, retrieved_policy['max_burst_kbps']) + if self.opposite_direction: + self.assertEqual(self.opposite_direction, + retrieved_policy['direction']) @decorators.idempotent_id('67ee6efd-7b33-4a68-927d-275b4f8ba958') def test_rule_delete(self): @@ -437,7 +465,7 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): description='test policy', shared=False) rule = self.admin_client.create_bandwidth_limit_rule( - policy['id'], 200, 1337)['bandwidth_limit_rule'] + policy['id'], 200, 1337, self.direction)['bandwidth_limit_rule'] retrieved_policy = self.admin_client.show_bandwidth_limit_rule( policy['id'], rule['id']) @@ -454,14 +482,14 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): self.assertRaises( exceptions.NotFound, self.create_qos_bandwidth_limit_rule, - 'policy', 200, 1337) + 'policy', 200, 1337, self.direction) @decorators.idempotent_id('a4a2e7ad-786f-4927-a85a-e545a93bd274') def test_rule_create_forbidden_for_regular_tenants(self): self.assertRaises( exceptions.Forbidden, self.client.create_bandwidth_limit_rule, - 'policy', 1, 2) + 'policy', 1, 2, self.direction) @decorators.idempotent_id('1bfc55d9-6fd8-4293-ab3a-b1d69bf7cd2e') def test_rule_update_forbidden_for_regular_tenants_own_policy(self): @@ -471,7 +499,8 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): tenant_id=self.client.tenant_id) rule = self.create_qos_bandwidth_limit_rule(policy_id=policy['id'], max_kbps=1, - max_burst_kbps=1) + max_burst_kbps=1, + direction=self.direction) self.assertRaises( exceptions.Forbidden, self.client.update_bandwidth_limit_rule, @@ -485,7 +514,8 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): tenant_id=self.admin_client.tenant_id) rule = self.create_qos_bandwidth_limit_rule(policy_id=policy['id'], max_kbps=1, - max_burst_kbps=1) + max_burst_kbps=1, + direction=self.direction) self.assertRaises( exceptions.NotFound, self.client.update_bandwidth_limit_rule, @@ -498,14 +528,16 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): shared=False) rule1 = self.create_qos_bandwidth_limit_rule(policy_id=policy1['id'], max_kbps=200, - max_burst_kbps=1337) + max_burst_kbps=1337, + direction=self.direction) policy2 = self.create_qos_policy(name='test-policy2', description='test policy2', shared=False) rule2 = self.create_qos_bandwidth_limit_rule(policy_id=policy2['id'], max_kbps=5000, - max_burst_kbps=2523) + max_burst_kbps=2523, + direction=self.direction) # Test 'list rules' rules = self.admin_client.list_bandwidth_limit_rules(policy1['id']) @@ -515,6 +547,20 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): self.assertNotIn(rule2['id'], rules_ids) +class QosBandwidthLimitRuleWithDirectionTestJSON( + QosBandwidthLimitRuleTestJSON): + + scenarios = [ + ('ingress', {'direction': 'ingress'}), + ('egress', {'direction': 'egress'}), + ] + + @classmethod + @test.requires_ext(extension="qos-bw-limit-direction", service="network") + def resource_setup(cls): + super(QosBandwidthLimitRuleWithDirectionTestJSON, cls).resource_setup() + + class RbacSharedQosPoliciesTest(base.BaseAdminNetworkTest): force_tenant_isolation = True diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py index 83c45700bc8..f5621ea785d 100644 --- a/neutron/tests/tempest/services/network/json/network_client.py +++ b/neutron/tests/tempest/services/network/json/network_client.py @@ -577,16 +577,19 @@ class NetworkClientJSON(service_client.RestClient): self.expected_success(200, resp.status) return service_client.ResponseBody(resp, body) - def create_bandwidth_limit_rule(self, policy_id, max_kbps, max_burst_kbps): + def create_bandwidth_limit_rule(self, policy_id, max_kbps, + max_burst_kbps, direction=None): uri = '%s/qos/policies/%s/bandwidth_limit_rules' % ( self.uri_prefix, policy_id) - post_data = self.serialize({ + post_data = { 'bandwidth_limit_rule': { 'max_kbps': max_kbps, 'max_burst_kbps': max_burst_kbps } - }) - resp, body = self.post(uri, post_data) + } + if direction: + post_data['bandwidth_limit_rule']['direction'] = direction + resp, body = self.post(uri, self.serialize(post_data)) self.expected_success(201, resp.status) body = jsonutils.loads(body) return service_client.ResponseBody(resp, body) @@ -610,6 +613,8 @@ class NetworkClientJSON(service_client.RestClient): def update_bandwidth_limit_rule(self, policy_id, rule_id, **kwargs): uri = '%s/qos/policies/%s/bandwidth_limit_rules/%s' % ( self.uri_prefix, policy_id, rule_id) + if "direction" in kwargs and kwargs['direction'] is None: + kwargs.pop('direction') post_data = {'bandwidth_limit_rule': kwargs} resp, body = self.put(uri, jsonutils.dumps(post_data)) body = self.deserialize_single(body) diff --git a/neutron/tests/unit/objects/qos/test_policy.py b/neutron/tests/unit/objects/qos/test_policy.py index fbe26daf6e7..c5c7f475644 100644 --- a/neutron/tests/unit/objects/qos/test_policy.py +++ b/neutron/tests/unit/objects/qos/test_policy.py @@ -14,6 +14,7 @@ import mock from oslo_versionedobjects import exception import testtools +from neutron.common import constants as n_const from neutron.common import exceptions as n_exc from neutron.db import models_v2 from neutron.objects.db import api as db_api @@ -132,13 +133,17 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, self.objs[0].create() return self.objs[0] - def _create_test_policy_with_rules(self, rule_type, reload_rules=False): + def _create_test_policy_with_rules(self, rule_type, reload_rules=False, + bwlimit_direction=None): policy_obj = self._create_test_policy() rules = [] for obj_cls in (RULE_OBJ_CLS.get(rule_type) for rule_type in rule_type): rule_fields = self.get_random_object_fields(obj_cls=obj_cls) rule_fields['qos_policy_id'] = policy_obj.id + if (obj_cls.rule_type == qos_consts.RULE_TYPE_BANDWIDTH_LIMIT and + bwlimit_direction is not None): + rule_fields['direction'] = bwlimit_direction rule_obj = obj_cls(self.context, **rule_fields) rule_obj.create() rules.append(rule_obj) @@ -380,11 +385,11 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, policy_obj, rule_objs = self._create_test_policy_with_rules( RULE_OBJ_CLS.keys(), reload_rules=True) - policy_obj_v1_2 = self._policy_through_version( + policy_obj_v1_5 = self._policy_through_version( policy_obj, policy.QosPolicy.VERSION) for rule_obj in rule_objs: - self.assertIn(rule_obj, policy_obj_v1_2.rules) + self.assertIn(rule_obj, policy_obj_v1_5.rules) def test_object_version_degradation_1_3_to_1_2_null_description(self): policy_obj = self._create_test_policy() @@ -398,7 +403,8 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, policy_obj, rule_objs = self._create_test_policy_with_rules( [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, qos_consts.RULE_TYPE_DSCP_MARKING, - qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH], reload_rules=True) + qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH], + reload_rules=True, bwlimit_direction=n_const.EGRESS_DIRECTION) policy_obj_v1_0 = self._policy_through_version(policy_obj, '1.0') @@ -412,7 +418,8 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, policy_obj, rule_objs = self._create_test_policy_with_rules( [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, qos_consts.RULE_TYPE_DSCP_MARKING, - qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH], reload_rules=True) + qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH], + reload_rules=True, bwlimit_direction=n_const.EGRESS_DIRECTION) policy_obj_v1_1 = self._policy_through_version(policy_obj, '1.1') @@ -426,12 +433,14 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, policy_obj, rule_objs = self._create_test_policy_with_rules( [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, qos_consts.RULE_TYPE_DSCP_MARKING, - qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH], reload_rules=True) + qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH], + reload_rules=True, bwlimit_direction=n_const.EGRESS_DIRECTION) policy_obj_v1_2 = self._policy_through_version(policy_obj, '1.2') - for rule_obj in rule_objs: - self.assertIn(rule_obj, policy_obj_v1_2.rules) + self.assertIn(rule_objs[0], policy_obj_v1_2.rules) + self.assertIn(rule_objs[1], policy_obj_v1_2.rules) + self.assertIn(rule_objs[2], policy_obj_v1_2.rules) def test_v1_4_to_v1_3_drops_project_id(self): policy_new = self._create_test_policy() @@ -440,6 +449,32 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, self.assertNotIn('project_id', policy_v1_3['versioned_object.data']) self.assertIn('tenant_id', policy_v1_3['versioned_object.data']) + def test_object_version_degradation_1_5_to_1_4_ingress_direction(self): + policy_obj, rule_objs = self._create_test_policy_with_rules( + [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, + qos_consts.RULE_TYPE_DSCP_MARKING, + qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH], + reload_rules=True, bwlimit_direction=n_const.INGRESS_DIRECTION) + + policy_obj_v1_4 = self._policy_through_version(policy_obj, '1.4') + + self.assertNotIn(rule_objs[0], policy_obj_v1_4.rules) + self.assertIn(rule_objs[1], policy_obj_v1_4.rules) + self.assertIn(rule_objs[2], policy_obj_v1_4.rules) + + def test_object_version_degradation_1_5_to_1_4_egress_direction(self): + policy_obj, rule_objs = self._create_test_policy_with_rules( + [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, + qos_consts.RULE_TYPE_DSCP_MARKING, + qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH], + reload_rules=True, bwlimit_direction=n_const.EGRESS_DIRECTION) + + policy_obj_v1_4 = self._policy_through_version(policy_obj, '1.4') + + self.assertIn(rule_objs[0], policy_obj_v1_4.rules) + self.assertIn(rule_objs[1], policy_obj_v1_4.rules) + self.assertIn(rule_objs[2], policy_obj_v1_4.rules) + def test_filter_by_shared(self): policy_obj = policy.QosPolicy( self.context, name='shared-policy', shared=True) diff --git a/neutron/tests/unit/objects/qos/test_rule.py b/neutron/tests/unit/objects/qos/test_rule.py index dfb6a34af4d..e675ce4a83d 100644 --- a/neutron/tests/unit/objects/qos/test_rule.py +++ b/neutron/tests/unit/objects/qos/test_rule.py @@ -14,6 +14,7 @@ from neutron_lib import constants from oslo_versionedobjects import exception +from neutron.common import constants as n_const from neutron.objects.qos import policy from neutron.objects.qos import rule from neutron.services.qos import qos_consts @@ -117,6 +118,25 @@ class QosBandwidthLimitRuleObjectTestCase(test_base.BaseObjectIfaceTestCase): dict_ = obj.to_dict() self.assertEqual(qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, dict_['type']) + def test_bandwidth_limit_object_version_degradation(self): + self.db_objs[0]['direction'] = n_const.EGRESS_DIRECTION + rule_obj = rule.QosBandwidthLimitRule(self.context, **self.db_objs[0]) + primitive_rule = rule_obj.obj_to_primitive('1.2') + self.assertNotIn( + "direction", primitive_rule['versioned_object.data'].keys()) + self.assertEqual( + self.db_objs[0]['max_kbps'], + primitive_rule['versioned_object.data']['max_kbps']) + self.assertEqual( + self.db_objs[0]['max_burst_kbps'], + primitive_rule['versioned_object.data']['max_burst_kbps']) + + self.db_objs[0]['direction'] = n_const.INGRESS_DIRECTION + rule_obj = rule.QosBandwidthLimitRule(self.context, **self.db_objs[0]) + self.assertRaises( + exception.IncompatibleObjectVersion, + rule_obj.obj_to_primitive, '1.2') + class QosBandwidthLimitRuleDbObjectTestCase(test_base.BaseDbObjectTestCase, testlib_api.SqlTestCase): diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 1352265e4f2..6083af67533 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -60,11 +60,11 @@ object_data = { 'PortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3', 'ProviderResourceAssociation': '1.0-05ab2d5a3017e5ce9dd381328f285f34', 'ProvisioningBlock': '1.0-c19d6d05bfa8143533471c1296066125', - 'QosBandwidthLimitRule': '1.2-4e44a8f5c2895ab1278399f87b40a13d', - 'QosDscpMarkingRule': '1.2-0313c6554b34fd10c753cb63d638256c', - 'QosMinimumBandwidthRule': '1.2-314c3419f4799067cc31cc319080adff', + 'QosBandwidthLimitRule': '1.3-51b662b12a8d1dfa89288d826c6d26d3', + 'QosDscpMarkingRule': '1.3-0313c6554b34fd10c753cb63d638256c', + 'QosMinimumBandwidthRule': '1.3-314c3419f4799067cc31cc319080adff', 'QosRuleType': '1.2-e6fd08fcca152c339cbd5e9b94b1b8e7', - 'QosPolicy': '1.4-50460f619c34428ec5651916e938e5a0', + 'QosPolicy': '1.5-50460f619c34428ec5651916e938e5a0', 'Quota': '1.0-6bb6a0f1bd5d66a2134ffa1a61873097', 'QuotaUsage': '1.0-6fbf820368681aac7c5d664662605cf9', 'Reservation': '1.0-49929fef8e82051660342eed51b48f2a', diff --git a/releasenotes/notes/QoS-ingress-bandwidth-limit-54cea12dbea71172.yaml b/releasenotes/notes/QoS-ingress-bandwidth-limit-54cea12dbea71172.yaml new file mode 100644 index 00000000000..96483330b22 --- /dev/null +++ b/releasenotes/notes/QoS-ingress-bandwidth-limit-54cea12dbea71172.yaml @@ -0,0 +1,6 @@ +--- +features: + - The QoS service plugin now supports new attribute in + ``qos_bandwidth_limit_rule``. This new parameter is called + ``direction`` and allows to specify direction of traffic + for which the limit should be applied.