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 90636b779db..0c4b25e31d7 100644 --- a/neutron/services/qos/qos_plugin.py +++ b/neutron/services/qos/qos_plugin.py @@ -37,7 +37,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 8a596342d4b..99d01a575b4 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -62,11 +62,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.