From 60325f4ae9ec53734d792d111cbcf24270d57417 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Mon, 18 Jul 2016 11:52:12 +0100 Subject: [PATCH] Add QoS minimum bandwidth rule for instance egress traffic This patch introduces the front end implementation for QoS minimum bandwidth rule. APIImpact: New type of parameter for QoS rule in neutron API DocImpact Change-Id: I6b619a96a2bfde164646c71409b671352bc6ce7d Partial-Bug: #1560963 --- doc/source/devref/quality_of_service.rst | 4 + etc/policy.json | 4 + .../alembic_migrations/versions/EXPAND_HEAD | 2 +- ...f0f87d4_add_qos_minimum_bandwidth_rules.py | 49 ++++++ neutron/db/qos/models.py | 22 +++ neutron/extensions/qos.py | 18 +- neutron/objects/qos/policy.py | 21 ++- neutron/objects/qos/rule.py | 23 ++- neutron/objects/qos/rule_type.py | 5 +- neutron/services/qos/qos_consts.py | 6 +- neutron/tests/etc/policy.json | 4 + neutron/tests/tempest/api/test_qos.py | 155 +++++++++++++++++- .../services/network/json/network_client.py | 47 ++++++ neutron/tests/unit/objects/qos/test_policy.py | 102 +++++++----- neutron/tests/unit/objects/qos/test_rule.py | 34 ++++ neutron/tests/unit/objects/test_base.py | 5 + neutron/tests/unit/objects/test_objects.py | 9 +- .../unit/services/qos/test_qos_plugin.py | 53 ++++++ ...s-min-egress-bw-rule-b1c80f5675a4c1c3.yaml | 10 ++ 19 files changed, 509 insertions(+), 64 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/newton/expand/0f5bef0f87d4_add_qos_minimum_bandwidth_rules.py create mode 100644 releasenotes/notes/qos-min-egress-bw-rule-b1c80f5675a4c1c3.yaml diff --git a/doc/source/devref/quality_of_service.rst b/doc/source/devref/quality_of_service.rst index 9a8569490e8..97170cd7afc 100644 --- a/doc/source/devref/quality_of_service.rst +++ b/doc/source/devref/quality_of_service.rst @@ -184,6 +184,10 @@ For QoS, new neutron objects were implemented: of the IP header, and only certain configurations are valid. As a result, the list of valid DSCP rule types is: 0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 46, 48, and 56. +* QosMinimumBandwidthRule: defines the minimum assured bandwidth rule type, + characterized by a min_kbps parameter. This rule has also a direction + parameter to set the traffic direction, from the instance point of view. The + only direction now implemented is egress. Those are defined in: diff --git a/etc/policy.json b/etc/policy.json index 3d9830d610a..ef93ff0c6d0 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -208,6 +208,10 @@ "delete_policy_dscp_marking_rule": "rule:admin_only", "update_policy_dscp_marking_rule": "rule:admin_only", "get_rule_type": "rule:regular_user", + "get_policy_minimum_bandwidth_rule": "rule:regular_user", + "create_policy_minimum_bandwidth_rule": "rule:admin_only", + "delete_policy_minimum_bandwidth_rule": "rule:admin_only", + "update_policy_minimum_bandwidth_rule": "rule:admin_only", "restrict_wildcard": "(not field:rbac_policy:target_tenant=*) or rule:admin_only", "create_rbac_policy": "", diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index ee7f95f6512..0b693579b10 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -a5648cfeeadf +0f5bef0f87d4 diff --git a/neutron/db/migration/alembic_migrations/versions/newton/expand/0f5bef0f87d4_add_qos_minimum_bandwidth_rules.py b/neutron/db/migration/alembic_migrations/versions/newton/expand/0f5bef0f87d4_add_qos_minimum_bandwidth_rules.py new file mode 100644 index 00000000000..221cf816d02 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/newton/expand/0f5bef0f87d4_add_qos_minimum_bandwidth_rules.py @@ -0,0 +1,49 @@ +# Copyright 2016 Intel Corporation. +# +# 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. +# + +"""add_qos_minimum_bandwidth_rules + +Revision ID: 0f5bef0f87d4 +Revises: a5648cfeeadf +Create Date: 2016-07-29 14:33:37.243487 + +""" + +# revision identifiers, used by Alembic. +revision = '0f5bef0f87d4' +down_revision = 'a5648cfeeadf' + +from alembic import op +import sqlalchemy as sa + +from neutron.common import constants + + +def upgrade(): + op.create_table( + 'qos_minimum_bandwidth_rules', + sa.Column('id', sa.String(length=36), primary_key=True), + sa.Column('qos_policy_id', + sa.String(length=36), + sa.ForeignKey('qos_policies.id', ondelete='CASCADE'), + nullable=False, index=True), + sa.Column('min_kbps', sa.Integer()), + sa.Column('direction', sa.Enum(constants.EGRESS_DIRECTION, + constants.INGRESS_DIRECTION, + name='directions'), + nullable=False, server_default=constants.EGRESS_DIRECTION), + sa.UniqueConstraint('qos_policy_id', 'direction', + name='qos_minimum_bandwidth_rules0qos_policy_id0direction') + ) diff --git a/neutron/db/qos/models.py b/neutron/db/qos/models.py index 137f13ddd82..829e29cf2bd 100644 --- a/neutron/db/qos/models.py +++ b/neutron/db/qos/models.py @@ -16,6 +16,7 @@ import sqlalchemy as sa from neutron.api.v2 import attributes as attrs +from neutron.common import constants from neutron.db import model_base from neutron.db import models_v2 from neutron.db import rbac_db_models @@ -87,3 +88,24 @@ class QosDscpMarkingRule(models_v2.HasId, model_base.BASEV2): nullable=False, unique=True) dscp_mark = sa.Column(sa.Integer) + + +class QosMinimumBandwidthRule(models_v2.HasId, model_base.BASEV2): + __tablename__ = 'qos_minimum_bandwidth_rules' + qos_policy_id = sa.Column(sa.String(36), + sa.ForeignKey('qos_policies.id', + ondelete='CASCADE'), + nullable=False, + index=True) + min_kbps = sa.Column(sa.Integer) + direction = sa.Column(sa.Enum(constants.EGRESS_DIRECTION, + constants.INGRESS_DIRECTION, + name='directions'), + nullable=False, + server_default=constants.EGRESS_DIRECTION) + __table_args__ = ( + sa.UniqueConstraint( + qos_policy_id, direction, + name='qos_minimum_bandwidth_rules0qos_policy_id0direction'), + model_base.BASEV2.__table_args__ + ) diff --git a/neutron/extensions/qos.py b/neutron/extensions/qos.py index 217610b8b84..2421c22f6de 100644 --- a/neutron/extensions/qos.py +++ b/neutron/extensions/qos.py @@ -95,6 +95,21 @@ SUB_RESOURCE_ATTRIBUTE_MAP = { 'is_visible': True, 'default': None, 'validate': {'type:values': common_constants. VALID_DSCP_MARKS}}}) + }, + 'minimum_bandwidth_rules': { + 'parent': {'collection_name': 'policies', + 'member_name': 'policy'}, + 'parameters': dict(QOS_RULE_COMMON_FIELDS, + **{'min_kbps': { + 'allow_post': True, 'allow_put': True, + 'is_visible': True, 'default': None, + 'validate': {'type:range': [0, + common_constants.DB_INTEGER_MAX_VALUE]}}, + 'direction': { + 'allow_post': True, 'allow_put': True, + 'is_visible': True, 'default': 'egress', + 'validate': {'type:values': + [common_constants.EGRESS_DIRECTION]}}}) } } @@ -194,7 +209,8 @@ class QoSPluginBase(service_base.ServicePluginBase): # The rule object type to use for each incoming rule-related request. rule_objects = {'bandwidth_limit': rule_object.QosBandwidthLimitRule, - 'dscp_marking': rule_object.QosDscpMarkingRule} + 'dscp_marking': rule_object.QosDscpMarkingRule, + 'minimum_bandwidth': rule_object.QosMinimumBandwidthRule} # Patterns used to call method proxies for all policy-rule-specific # method calls (see __getattr__ docstring, below). diff --git a/neutron/objects/qos/policy.py b/neutron/objects/qos/policy.py index ea70819a927..cbe3039f032 100644 --- a/neutron/objects/qos/policy.py +++ b/neutron/objects/qos/policy.py @@ -38,7 +38,8 @@ from neutron.objects import rbac_db class QosPolicy(base.NeutronDbObject): # Version 1.0: Initial version # Version 1.1: QosDscpMarkingRule introduced - VERSION = '1.1' + # Version 1.2: Added QosMinimumBandwidthRule + VERSION = '1.2' # required by RbacNeutronMetaclass rbac_db_model = QosPolicyRBAC @@ -212,11 +213,15 @@ class QosPolicy(base.NeutronDbObject): return set(bound_tenants) def obj_make_compatible(self, primitive, target_version): + def filter_rules(obj_names, rules): + return filter(lambda rule: + (rule['versioned_object.name'] in obj_names), rules) + _target_version = versionutils.convert_version_to_tuple(target_version) - if _target_version < (1, 1): - if 'rules' in primitive: - bw_obj_name = rule_obj_impl.QosBandwidthLimitRule.obj_name() - primitive['rules'] = filter( - lambda rule: (rule['versioned_object.name'] == - bw_obj_name), - primitive['rules']) + names = [] + if _target_version >= (1, 0): + names.append(rule_obj_impl.QosBandwidthLimitRule.obj_name()) + if _target_version >= (1, 1): + names.append(rule_obj_impl.QosDscpMarkingRule.obj_name()) + if 'rules' in primitive and names: + primitive['rules'] = filter_rules(names, primitive['rules']) diff --git a/neutron/objects/qos/rule.py b/neutron/objects/qos/rule.py index 3f188fe67e4..393f0fdd5f7 100644 --- a/neutron/objects/qos/rule.py +++ b/neutron/objects/qos/rule.py @@ -49,11 +49,12 @@ def get_rules(context, qos_policy_id): class QosRule(base.NeutronDbObject): # Version 1.0: Initial version, only BandwidthLimitRule # 1.1: Added DscpMarkingRule + # 1.2: Added QosMinimumBandwidthRule # #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.1' + VERSION = '1.2' fields = { 'id': obj_fields.UUIDField(), @@ -117,3 +118,23 @@ class QosDscpMarkingRule(QosRule): raise exception.IncompatibleObjectVersion( objver=target_version, objname="QosDscpMarkingRule") + + +@obj_base.VersionedObjectRegistry.register +class QosMinimumBandwidthRule(QosRule): + + db_model = qos_db_model.QosMinimumBandwidthRule + + fields = { + 'min_kbps': obj_fields.IntegerField(nullable=True), + 'direction': common_types.FlowDirectionEnumField(), + } + + rule_type = qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH + + def obj_make_compatible(self, primitive, target_version): + _target_version = versionutils.convert_version_to_tuple(target_version) + if _target_version < (1, 2): + raise exception.IncompatibleObjectVersion( + objver=target_version, + objname="QosMinimumBandwidthRule") diff --git a/neutron/objects/qos/rule_type.py b/neutron/objects/qos/rule_type.py index 6e1da707bf4..a5690b8dbb4 100644 --- a/neutron/objects/qos/rule_type.py +++ b/neutron/objects/qos/rule_type.py @@ -29,8 +29,9 @@ class RuleTypeField(obj_fields.BaseEnumField): @obj_base.VersionedObjectRegistry.register class QosRuleType(base.NeutronObject): # Version 1.0: Initial version - # Version 1.1: Added DscpMarkingRule - VERSION = '1.1' + # Version 1.1: Added QosDscpMarkingRule + # Version 1.2: Added QosMinimumBandwidthRule + VERSION = '1.2' fields = { 'type': RuleTypeField(), diff --git a/neutron/services/qos/qos_consts.py b/neutron/services/qos/qos_consts.py index 3f3d864257e..0e526bf12de 100644 --- a/neutron/services/qos/qos_consts.py +++ b/neutron/services/qos/qos_consts.py @@ -15,7 +15,11 @@ RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth_limit' RULE_TYPE_DSCP_MARKING = 'dscp_marking' -VALID_RULE_TYPES = [RULE_TYPE_BANDWIDTH_LIMIT, RULE_TYPE_DSCP_MARKING] +RULE_TYPE_MINIMUM_BANDWIDTH = 'minimum_bandwidth' +VALID_RULE_TYPES = [RULE_TYPE_BANDWIDTH_LIMIT, + RULE_TYPE_DSCP_MARKING, + RULE_TYPE_MINIMUM_BANDWIDTH, + ] QOS_POLICY_ID = 'qos_policy_id' diff --git a/neutron/tests/etc/policy.json b/neutron/tests/etc/policy.json index 3d9830d610a..ef93ff0c6d0 100644 --- a/neutron/tests/etc/policy.json +++ b/neutron/tests/etc/policy.json @@ -208,6 +208,10 @@ "delete_policy_dscp_marking_rule": "rule:admin_only", "update_policy_dscp_marking_rule": "rule:admin_only", "get_rule_type": "rule:regular_user", + "get_policy_minimum_bandwidth_rule": "rule:regular_user", + "create_policy_minimum_bandwidth_rule": "rule:admin_only", + "delete_policy_minimum_bandwidth_rule": "rule:admin_only", + "update_policy_minimum_bandwidth_rule": "rule:admin_only", "restrict_wildcard": "(not field:rbac_policy:target_tenant=*) or rule:admin_only", "create_rbac_policy": "", diff --git a/neutron/tests/tempest/api/test_qos.py b/neutron/tests/tempest/api/test_qos.py index bece71c4541..ca49758bd03 100644 --- a/neutron/tests/tempest/api/test_qos.py +++ b/neutron/tests/tempest/api/test_qos.py @@ -337,6 +337,13 @@ class QosTestJSON(base.BaseAdminNetworkTest): obtained_policy = self.client.show_qos_policy(policy['id'])['policy'] self.assertEqual(obtained_policy, policy) + @test.idempotent_id('aed8e2a6-22da-421b-89b9-935a2c1a1b50') + def test_policy_create_forbidden_for_regular_tenants(self): + self.assertRaises( + exceptions.Forbidden, + self.client.create_qos_policy, + 'test-policy', 'test policy', False) + class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): @classmethod @@ -434,13 +441,6 @@ class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest): self.create_qos_bandwidth_limit_rule, 'policy', 200, 1337) - @test.idempotent_id('eed8e2a6-22da-421b-89b9-935a2c1a1b50') - def test_policy_create_forbidden_for_regular_tenants(self): - self.assertRaises( - exceptions.Forbidden, - self.client.create_qos_policy, - 'test-policy', 'test policy', False) - @test.idempotent_id('a4a2e7ad-786f-4927-a85a-e545a93bd274') def test_rule_create_forbidden_for_regular_tenants(self): self.assertRaises( @@ -883,6 +883,147 @@ class QosDscpMarkingRuleTestJSON(base.BaseAdminNetworkTest): self.assertNotIn(rule2['id'], rules_ids) +class QosMinimumBandwidthRuleTestJSON(base.BaseAdminNetworkTest): + DIRECTION_EGRESS = "egress" + DIRECTION_INGRESS = "ingress" + RULE_NAME = qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH + "_rule" + RULES_NAME = RULE_NAME + "s" + + @classmethod + @test.requires_ext(extension="qos", service="network") + def resource_setup(cls): + super(QosMinimumBandwidthRuleTestJSON, cls).resource_setup() + + @test.idempotent_id('aa59b00b-3e9c-4787-92f8-93a5cdf5e378') + def test_rule_create(self): + policy = self.create_qos_policy(name='test-policy', + description='test policy', + shared=False) + rule = self.admin_client.create_minimum_bandwidth_rule( + policy_id=policy['id'], min_kbps=1138, + direction=self.DIRECTION_EGRESS)[self.RULE_NAME] + + # Test 'show rule' + retrieved_rule = self.admin_client.show_minimum_bandwidth_rule( + policy['id'], rule['id']) + retrieved_rule = retrieved_rule[self.RULE_NAME] + self.assertEqual(rule['id'], retrieved_rule['id']) + self.assertEqual(1138, retrieved_rule['min_kbps']) + self.assertEqual(self.DIRECTION_EGRESS, retrieved_rule['direction']) + + # Test 'list rules' + rules = self.admin_client.list_minimum_bandwidth_rules(policy['id']) + rules = rules[self.RULES_NAME] + rules_ids = [r['id'] for r in rules] + self.assertIn(rule['id'], rules_ids) + + # Test 'show policy' + retrieved_policy = self.admin_client.show_qos_policy(policy['id']) + policy_rules = retrieved_policy['policy']['rules'] + self.assertEqual(1, len(policy_rules)) + self.assertEqual(rule['id'], policy_rules[0]['id']) + self.assertEqual(qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH, + policy_rules[0]['type']) + + @test.idempotent_id('aa59b00b-ab01-4787-92f8-93a5cdf5e378') + def test_rule_create_fail_for_the_same_type(self): + policy = self.create_qos_policy(name='test-policy', + description='test policy', + shared=False) + self.admin_client.create_minimum_bandwidth_rule( + policy_id=policy['id'], min_kbps=200, + direction=self.DIRECTION_EGRESS) + + self.assertRaises(exceptions.Conflict, + self.admin_client.create_minimum_bandwidth_rule, + policy_id=policy['id'], + min_kbps=201, direction=self.DIRECTION_EGRESS) + + @test.idempotent_id('d6fce764-e511-4fa6-9f86-f4b41cf142cf') + def test_rule_create_fail_for_direction_ingress(self): + policy = self.create_qos_policy(name='test-policy', + description='test policy', + shared=False) + self.assertRaises(exceptions.BadRequest, + self.admin_client.create_minimum_bandwidth_rule, + policy_id=policy['id'], + min_kbps=201, direction=self.DIRECTION_INGRESS) + + @test.idempotent_id('a49a6988-2568-47d2-931e-2dbc858943b3') + def test_rule_update(self): + policy = self.create_qos_policy(name='test-policy', + description='test policy', + shared=False) + rule = self.admin_client.create_minimum_bandwidth_rule( + policy_id=policy['id'], min_kbps=300, + direction=self.DIRECTION_EGRESS)[self.RULE_NAME] + + self.admin_client.update_minimum_bandwidth_rule(policy['id'], + rule['id'], min_kbps=350, direction=self.DIRECTION_EGRESS) + + retrieved_policy = self.admin_client.show_minimum_bandwidth_rule( + policy['id'], rule['id']) + retrieved_policy = retrieved_policy[self.RULE_NAME] + self.assertEqual(350, retrieved_policy['min_kbps']) + self.assertEqual(self.DIRECTION_EGRESS, retrieved_policy['direction']) + + @test.idempotent_id('a7ee6efd-7b33-4a68-927d-275b4f8ba958') + def test_rule_delete(self): + policy = self.create_qos_policy(name='test-policy', + description='test policy', + shared=False) + rule = self.admin_client.create_minimum_bandwidth_rule( + policy['id'], 200, self.DIRECTION_EGRESS)[self.RULE_NAME] + + retrieved_policy = self.admin_client.show_minimum_bandwidth_rule( + policy['id'], rule['id']) + retrieved_policy = retrieved_policy[self.RULE_NAME] + self.assertEqual(rule['id'], retrieved_policy['id']) + + self.admin_client.delete_minimum_bandwidth_rule(policy['id'], + rule['id']) + self.assertRaises(exceptions.NotFound, + self.admin_client.show_minimum_bandwidth_rule, + policy['id'], rule['id']) + + @test.idempotent_id('a211222c-5808-46cb-a961-983bbab6b852') + def test_rule_create_rule_nonexistent_policy(self): + self.assertRaises( + exceptions.NotFound, + self.admin_client.create_minimum_bandwidth_rule, + 'policy', 200, self.DIRECTION_EGRESS) + + @test.idempotent_id('b4a2e7ad-786f-4927-a85a-e545a93bd274') + def test_rule_create_forbidden_for_regular_tenants(self): + self.assertRaises( + exceptions.Forbidden, + self.client.create_minimum_bandwidth_rule, + 'policy', 300, self.DIRECTION_EGRESS) + + @test.idempotent_id('de0bd0c2-54d9-4e29-85f1-cfb36ac3ebe2') + def test_get_rules_by_policy(self): + policy1 = self.create_qos_policy(name='test-policy1', + description='test policy1', + shared=False) + rule1 = self.admin_client.create_minimum_bandwidth_rule( + policy_id=policy1['id'], min_kbps=200, + direction=self.DIRECTION_EGRESS)[self.RULE_NAME] + + policy2 = self.create_qos_policy(name='test-policy2', + description='test policy2', + shared=False) + rule2 = self.admin_client.create_minimum_bandwidth_rule( + policy_id=policy2['id'], min_kbps=5000, + direction=self.DIRECTION_EGRESS)[self.RULE_NAME] + + # Test 'list rules' + rules = self.admin_client.list_minimum_bandwidth_rules(policy1['id']) + rules = rules[self.RULES_NAME] + rules_ids = [r['id'] for r in rules] + self.assertIn(rule1['id'], rules_ids) + self.assertNotIn(rule2['id'], rules_ids) + + class QosSearchCriteriaTest(base.BaseSearchCriteriaTest, base.BaseAdminNetworkTest): diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py index 54a1fc3db8d..033696104e4 100644 --- a/neutron/tests/tempest/services/network/json/network_client.py +++ b/neutron/tests/tempest/services/network/json/network_client.py @@ -55,6 +55,7 @@ class NetworkClientJSON(service_client.RestClient): 'metering_label_rules': 'metering', 'policies': 'qos', 'bandwidth_limit_rules': 'qos', + 'minimum_bandwidth_rules': 'qos', 'rule_types': 'qos', 'rbac-policies': '', } @@ -652,6 +653,52 @@ class NetworkClientJSON(service_client.RestClient): self.expected_success(204, resp.status) return service_client.ResponseBody(resp, body) + def create_minimum_bandwidth_rule(self, policy_id, min_kbps, direction): + uri = '%s/qos/policies/%s/minimum_bandwidth_rules' % ( + self.uri_prefix, policy_id) + post_data = self.serialize({ + 'minimum_bandwidth_rule': { + 'min_kbps': min_kbps, + 'direction': direction + } + }) + resp, body = self.post(uri, post_data) + self.expected_success(201, resp.status) + body = jsonutils.loads(body) + return service_client.ResponseBody(resp, body) + + def list_minimum_bandwidth_rules(self, policy_id): + uri = '%s/qos/policies/%s/minimum_bandwidth_rules' % ( + self.uri_prefix, policy_id) + resp, body = self.get(uri) + body = self.deserialize_single(body) + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def show_minimum_bandwidth_rule(self, policy_id, rule_id): + uri = '%s/qos/policies/%s/minimum_bandwidth_rules/%s' % ( + self.uri_prefix, policy_id, rule_id) + resp, body = self.get(uri) + body = self.deserialize_single(body) + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def update_minimum_bandwidth_rule(self, policy_id, rule_id, **kwargs): + uri = '%s/qos/policies/%s/minimum_bandwidth_rules/%s' % ( + self.uri_prefix, policy_id, rule_id) + post_data = {'minimum_bandwidth_rule': kwargs} + resp, body = self.put(uri, jsonutils.dumps(post_data)) + body = self.deserialize_single(body) + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def delete_minimum_bandwidth_rule(self, policy_id, rule_id): + uri = '%s/qos/policies/%s/minimum_bandwidth_rules/%s' % ( + self.uri_prefix, policy_id, rule_id) + resp, body = self.delete(uri) + self.expected_success(204, resp.status) + return service_client.ResponseBody(resp, body) + def list_qos_rule_types(self): uri = '%s/qos/rule-types' % self.uri_prefix resp, body = self.get(uri) diff --git a/neutron/tests/unit/objects/qos/test_policy.py b/neutron/tests/unit/objects/qos/test_policy.py index 4fdd6bca86b..a68c3508faa 100644 --- a/neutron/tests/unit/objects/qos/test_policy.py +++ b/neutron/tests/unit/objects/qos/test_policy.py @@ -17,10 +17,18 @@ from neutron.db import models_v2 from neutron.objects.db import api as db_api from neutron.objects.qos import policy from neutron.objects.qos import rule +from neutron.services.qos import qos_consts from neutron.tests.unit.objects import test_base from neutron.tests.unit import testlib_api +RULE_OBJ_CLS = { + qos_consts.RULE_TYPE_BANDWIDTH_LIMIT: rule.QosBandwidthLimitRule, + qos_consts.RULE_TYPE_DSCP_MARKING: rule.QosDscpMarkingRule, + qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH: rule.QosMinimumBandwidthRule, +} + + class QosPolicyObjectTestCase(test_base.BaseObjectIfaceTestCase): _test_class = policy.QosPolicy @@ -36,13 +44,19 @@ class QosPolicyObjectTestCase(test_base.BaseObjectIfaceTestCase): self.get_random_fields(rule.QosDscpMarkingRule) for _ in range(3)] + self.db_qos_minimum_bandwidth_rules = [ + self.get_random_fields(rule.QosMinimumBandwidthRule) + for _ in range(3)] + self.model_map = { self._test_class.db_model: self.db_objs, self._test_class.rbac_db_model: [], self._test_class.port_binding_model: [], self._test_class.network_binding_model: [], rule.QosBandwidthLimitRule.db_model: self.db_qos_bandwidth_rules, - rule.QosDscpMarkingRule.db_model: self.db_qos_dscp_rules} + rule.QosDscpMarkingRule.db_model: self.db_qos_dscp_rules, + rule.QosMinimumBandwidthRule.db_model: + self.db_qos_minimum_bandwidth_rules} self.get_object = mock.patch.object( db_api, 'get_object', side_effect=self.fake_get_object).start() @@ -127,17 +141,20 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, policy_obj.create() return policy_obj - def _create_test_policy_with_bwrule(self): + def _create_test_policy_with_rules(self, rule_type, reload_rules=False): 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_fields(obj_cls=obj_cls) + rule_fields['qos_policy_id'] = policy_obj.id + rule_obj = obj_cls(self.context, **rule_fields) + rule_obj.create() + rules.append(rule_obj) - rule_fields = self.get_random_fields( - obj_cls=rule.QosBandwidthLimitRule) - rule_fields['qos_policy_id'] = policy_obj.id - - rule_obj = rule.QosBandwidthLimitRule(self.context, **rule_fields) - rule_obj.create() - - return policy_obj, rule_obj + if reload_rules: + policy_obj.reload_rules() + return policy_obj, rules def test_attach_network_get_network_policy(self): @@ -291,27 +308,30 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, policy_obj.detach_network, self._network['id']) def test_synthetic_rule_fields(self): - policy_obj, rule_obj = self._create_test_policy_with_bwrule() + policy_obj, rule_obj = self._create_test_policy_with_rules( + [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT]) policy_obj = policy.QosPolicy.get_object(self.context, id=policy_obj.id) - self.assertEqual([rule_obj], policy_obj.rules) + self.assertEqual(rule_obj, policy_obj.rules) def test_get_object_fetches_rules_non_lazily(self): - policy_obj, rule_obj = self._create_test_policy_with_bwrule() + policy_obj, rule_obj = self._create_test_policy_with_rules( + [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT]) policy_obj = policy.QosPolicy.get_object(self.context, id=policy_obj.id) - self.assertEqual([rule_obj], policy_obj.rules) + self.assertEqual(rule_obj, policy_obj.rules) primitive = policy_obj.obj_to_primitive() self.assertNotEqual([], (primitive['versioned_object.data']['rules'])) def test_to_dict_returns_rules_as_dicts(self): - policy_obj, rule_obj = self._create_test_policy_with_bwrule() + policy_obj, rule_obj = self._create_test_policy_with_rules( + [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT]) policy_obj = policy.QosPolicy.get_object(self.context, id=policy_obj.id) obj_dict = policy_obj.to_dict() - rule_dict = rule_obj.to_dict() + rule_dict = rule_obj[0].to_dict() # first make sure that to_dict() is still sane and does not return # objects @@ -343,11 +363,12 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, obj.delete() def test_reload_rules_reloads_rules(self): - policy_obj, rule_obj = self._create_test_policy_with_bwrule() + policy_obj, rule_obj = self._create_test_policy_with_rules( + [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT]) self.assertEqual([], policy_obj.rules) policy_obj.reload_rules() - self.assertEqual([rule_obj], policy_obj.rules) + self.assertEqual(rule_obj, policy_obj.rules) def test_get_bound_tenant_ids_returns_set_of_tenant_ids(self): obj = self._create_test_policy() @@ -364,37 +385,40 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, primitive = obj.obj_to_primitive(target_version=version) return policy.QosPolicy.clean_obj_from_primitive(primitive) - def _create_test_policy_with_bw_and_dscp(self): - policy_obj, rule_obj_band = self._create_test_policy_with_bwrule() - - rule_fields = self.get_random_fields(obj_cls=rule.QosDscpMarkingRule) - rule_fields['qos_policy_id'] = policy_obj.id - - rule_obj_dscp = rule.QosDscpMarkingRule(self.context, **rule_fields) - rule_obj_dscp.create() - - policy_obj.reload_rules() - return policy_obj, rule_obj_band, rule_obj_dscp - def test_object_version(self): - policy_obj, rule_obj_band, rule_obj_dscp = ( - self._create_test_policy_with_bw_and_dscp()) + policy_obj, rule_objs = self._create_test_policy_with_rules( + RULE_OBJ_CLS.keys(), reload_rules=True) - policy_obj_v1_1 = self._policy_through_version(policy_obj, '1.1') + policy_obj_v1_2 = self._policy_through_version(policy_obj, '1.2') - self.assertIn(rule_obj_band, policy_obj_v1_1.rules) - self.assertIn(rule_obj_dscp, policy_obj_v1_1.rules) + for rule_obj in rule_objs: + self.assertIn(rule_obj, policy_obj_v1_2.rules) def test_object_version_degradation_1_1_to_1_0(self): #NOTE(mangelajo): we should not check .VERSION, since that's the # local version on the class definition - policy_obj, rule_obj_band, rule_obj_dscp = ( - self._create_test_policy_with_bw_and_dscp()) + policy_obj, rule_objs = self._create_test_policy_with_rules( + [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, + qos_consts.RULE_TYPE_DSCP_MARKING], reload_rules=True) policy_obj_v1_0 = self._policy_through_version(policy_obj, '1.0') - self.assertIn(rule_obj_band, policy_obj_v1_0.rules) - self.assertNotIn(rule_obj_dscp, policy_obj_v1_0.rules) + self.assertIn(rule_objs[0], policy_obj_v1_0.rules) + self.assertNotIn(rule_objs[1], policy_obj_v1_0.rules) + + def test_object_version_degradation_1_2_to_1_1(self): + #NOTE(mangelajo): we should not check .VERSION, since that's the + # local version on the class definition + 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) + + policy_obj_v1_1 = self._policy_through_version(policy_obj, '1.1') + + self.assertIn(rule_objs[0], policy_obj_v1_1.rules) + self.assertIn(rule_objs[1], policy_obj_v1_1.rules) + self.assertNotIn(rule_objs[2], policy_obj_v1_1.rules) def test_filter_by_shared(self): policy_obj = policy.QosPolicy( diff --git a/neutron/tests/unit/objects/qos/test_rule.py b/neutron/tests/unit/objects/qos/test_rule.py index f07a45d2929..d7c6974fc7f 100644 --- a/neutron/tests/unit/objects/qos/test_rule.py +++ b/neutron/tests/unit/objects/qos/test_rule.py @@ -121,3 +121,37 @@ class QosDscpMarkingRuleDbObjectTestCase(test_base.BaseDbObjectTestCase, policy_obj = policy.QosPolicy(self.context, id=generated_qos_policy_id) policy_obj.create() + + +class QosMinimumBandwidthRuleObjectTestCase(test_base.BaseObjectIfaceTestCase): + + _test_class = rule.QosMinimumBandwidthRule + + def test_min_bw_object_version_degradation(self): + min_bw_rule = rule.QosMinimumBandwidthRule() + + for version in ['1.0', '1.1']: + self.assertRaises(exception.IncompatibleObjectVersion, + min_bw_rule.obj_to_primitive, version) + + def test_min_bw_object_version(self): + min_bw_rule = rule.QosMinimumBandwidthRule() + + prim = min_bw_rule.obj_to_primitive('1.2') + + self.assertTrue(prim) + + +class QosMinimumBandwidthRuleDbObjectTestCase(test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase): + + _test_class = rule.QosMinimumBandwidthRule + + def setUp(self): + super(QosMinimumBandwidthRuleDbObjectTestCase, self).setUp() + # Prepare policy to be able to insert a rule + for obj in self.db_objs: + generated_qos_policy_id = obj['qos_policy_id'] + policy_obj = policy.QosPolicy(self.context, + id=generated_qos_policy_id) + policy_obj.create() diff --git a/neutron/tests/unit/objects/test_base.py b/neutron/tests/unit/objects/test_base.py index 2629ef01463..2de041af758 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -343,6 +343,10 @@ def get_random_dscp_mark(): return random.choice(constants.VALID_DSCP_MARKS) +def get_random_direction(): + return random.choice(constants.VALID_DIRECTIONS) + + def get_list_of_random_networks(num=10): for i in range(5): res = [tools.get_random_ip_network() for i in range(num)] @@ -360,6 +364,7 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = { obj_fields.ObjectField: lambda: None, obj_fields.ListOfObjectsField: lambda: [], common_types.DscpMarkField: get_random_dscp_mark, + common_types.FlowDirectionEnumField: get_random_direction, obj_fields.IPNetworkField: tools.get_random_ip_network, common_types.IPNetworkField: tools.get_random_ip_network, common_types.IPNetworkPrefixLenField: tools.get_random_prefixlen, diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 3d6c044985f..25ec6a6e642 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -35,10 +35,11 @@ object_data = { 'NetworkSegment': '1.0-865567a6f70eb85cf33fb7a5575a4eab', 'PortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3', 'AllowedAddressPair': '1.0-9f9186b6f952fbf31d257b0458b852c0', - 'QosBandwidthLimitRule': '1.1-4e44a8f5c2895ab1278399f87b40a13d', - 'QosDscpMarkingRule': '1.1-0313c6554b34fd10c753cb63d638256c', - 'QosRuleType': '1.1-8a53fef4c6a43839d477a85b787d22ce', - 'QosPolicy': '1.1-7c5659e1c1f64395223592d3d3293e22', + 'QosBandwidthLimitRule': '1.2-4e44a8f5c2895ab1278399f87b40a13d', + 'QosDscpMarkingRule': '1.2-0313c6554b34fd10c753cb63d638256c', + 'QosMinimumBandwidthRule': '1.2-314c3419f4799067cc31cc319080adff', + 'QosRuleType': '1.2-e6fd08fcca152c339cbd5e9b94b1b8e7', + 'QosPolicy': '1.2-7c5659e1c1f64395223592d3d3293e22', 'Route': '1.0-a9883a63b416126f9e345523ec09483b', 'SecurityGroup': '1.0-e26b90c409b31fd2e3c6fcec402ac0b9', 'SecurityGroupRule': '1.0-e9b8dace9d48b936c62ad40fe1f339d5', diff --git a/neutron/tests/unit/services/qos/test_qos_plugin.py b/neutron/tests/unit/services/qos/test_qos_plugin.py index f9e5d023e12..2b03413b12a 100644 --- a/neutron/tests/unit/services/qos/test_qos_plugin.py +++ b/neutron/tests/unit/services/qos/test_qos_plugin.py @@ -290,6 +290,59 @@ class TestQosPlugin(base.BaseQosTestCase): self.qos_plugin.get_policy_dscp_marking_rules, self.ctxt, self.policy.id) + def test_get_policy_minimum_bandwidth_rule(self): + with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', + return_value=self.policy): + with mock.patch('neutron.objects.qos.rule.' + 'QosMinimumBandwidthRule.' + 'get_object') as get_object_mock: + self.qos_plugin.get_policy_minimum_bandwidth_rule( + self.ctxt, self.rule.id, self.policy.id) + get_object_mock.assert_called_once_with(self.ctxt, + id=self.rule.id) + + def test_get_policy_minimum_bandwidth_rules_for_policy(self): + with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', + return_value=self.policy): + with mock.patch('neutron.objects.qos.rule.' + 'QosMinimumBandwidthRule.' + 'get_objects') as get_objects_mock: + self.qos_plugin.get_policy_minimum_bandwidth_rules( + self.ctxt, self.policy.id) + get_objects_mock.assert_called_once_with( + self.ctxt, _pager=mock.ANY, qos_policy_id=self.policy.id) + + def test_get_policy_minimum_bandwidth_rules_for_policy_with_filters(self): + with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', + return_value=self.policy): + with mock.patch('neutron.objects.qos.rule.' + 'QosMinimumBandwidthRule.' + 'get_objects') as get_objects_mock: + + filters = {'filter': 'filter_id'} + self.qos_plugin.get_policy_minimum_bandwidth_rules( + self.ctxt, self.policy.id, filters=filters) + get_objects_mock.assert_called_once_with( + self.ctxt, _pager=mock.ANY, + qos_policy_id=self.policy.id, + filter='filter_id') + + def test_get_policy_minimum_bandwidth_rule_for_nonexistent_policy(self): + with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', + return_value=None): + self.assertRaises( + n_exc.QosPolicyNotFound, + self.qos_plugin.get_policy_minimum_bandwidth_rule, + self.ctxt, self.rule.id, self.policy.id) + + def test_get_policy_minimum_bandwidth_rules_for_nonexistent_policy(self): + with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', + return_value=None): + self.assertRaises( + n_exc.QosPolicyNotFound, + self.qos_plugin.get_policy_minimum_bandwidth_rules, + self.ctxt, self.policy.id) + def test_create_policy_rule_for_nonexistent_policy(self): with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', return_value=None): diff --git a/releasenotes/notes/qos-min-egress-bw-rule-b1c80f5675a4c1c3.yaml b/releasenotes/notes/qos-min-egress-bw-rule-b1c80f5675a4c1c3.yaml new file mode 100644 index 00000000000..7753cdde789 --- /dev/null +++ b/releasenotes/notes/qos-min-egress-bw-rule-b1c80f5675a4c1c3.yaml @@ -0,0 +1,10 @@ +--- +features: + - Users can now apply a QoS rule to a port or network to + setup the minimum egress bandwidth per queue and port. + The minimum egress bandwidth rule is applied to each port + individually. +other: + - At the time of writing, Neutron bandwidth booking is not + integrated with Compute scheduler, which means that minimal + bandwidth is not guaranteed but provided as best effort.