From a9a1943fde253f7da1c10e47baba9e34b4b1401c Mon Sep 17 00:00:00 2001 From: David Shaughnessy Date: Tue, 1 Mar 2016 18:55:56 +0000 Subject: [PATCH] DSCP QoS rule implementation This patch adds the front end and back end implementation of QoS DSCP. Associated patches that are dependent on this one: * python-neutronclient: https://review.openstack.org/#/c/254280 * openstack-manuals: https://review.openstack.org/#/c/273638 * API Guide: https://review.openstack.org/#/c/275253 * Heat: * Spec: https://review.openstack.org/#/c/272173 * QoSDscpMarkingRule resource: https://review.openstack.org/#/c/277567 * Fullstack tests: https://review.openstack.org/#/c/288392/ APIImpact - The API now supports marking traffic egressing from a VM's dscp field with a valid dscp value. Co-Authored-By: Nate Johnston Co-Authored-By: Victor Howard Co-Authored-By: Margaret Frances Co-Authored-By: James Reeves Co-Authored-By: John Schwarz Needed-By: I25ad60c1b9a66e568276a772b8c496987d9f8299 Needed-By: I881b8f5bc9024c20275bc56062de72a1c70c8321 Needed-By: I48ead4b459183db795337ab729830a1b3c0022da Needed-By: Ib92b172dce48276b90ec75ee5880ddd69040d7c8 Needed-By: I4eb21495e84feea46880caf3360759263e1e8f95 Needed-By: I0ab6a1a0d1430c5791fea1d5b54106c6cc93b937 Partial-Bug: #1468353 Change-Id: Ic3baefe176df05f049a2e06529c58fd65fe6b419 --- doc/source/devref/quality_of_service.rst | 21 ++- etc/policy.json | 4 + neutron/agent/l2/extensions/qos.py | 13 ++ neutron/common/constants.py | 3 + .../alembic_migrations/versions/EXPAND_HEAD | 2 +- .../45f8dd33480b_qos_dscp_db_addition.py | 40 +++++ neutron/db/qos/models.py | 10 ++ neutron/extensions/qos.py | 38 +++++ neutron/objects/common_types.py | 45 ++++++ neutron/objects/qos/policy.py | 14 +- neutron/objects/qos/rule.py | 24 ++- neutron/objects/qos/rule_type.py | 4 +- .../agent/extension_drivers/qos_driver.py | 50 ++++++- .../mech_driver/mech_openvswitch.py | 3 +- neutron/services/qos/qos_consts.py | 3 +- neutron/services/qos/qos_plugin.py | 74 ++++++++- neutron/tests/api/test_qos.py | 141 ++++++++++++++++++ neutron/tests/common/agents/l2_extensions.py | 27 ++++ neutron/tests/etc/policy.json | 4 + neutron/tests/fullstack/resources/config.py | 2 +- .../tests/fullstack/resources/environment.py | 4 +- neutron/tests/fullstack/test_qos.py | 10 +- .../test_ovs_agent_qos_extension.py | 72 ++++++++- .../services/network/json/network_client.py | 44 ++++++ .../unit/agent/l2/extensions/test_qos.py | 8 + neutron/tests/unit/objects/qos/test_policy.py | 57 ++++++- neutron/tests/unit/objects/qos/test_rule.py | 19 +++ neutron/tests/unit/objects/test_base.py | 10 +- .../tests/unit/objects/test_common_types.py | 25 +++- neutron/tests/unit/objects/test_objects.py | 7 +- .../extension_drivers/test_qos_driver.py | 8 + neutron/tests/unit/plugins/ml2/test_plugin.py | 14 +- .../unit/services/qos/test_qos_plugin.py | 83 ++++++++++- .../notes/dscp-qos-77ea9b27d3762e48.yaml | 11 ++ 34 files changed, 855 insertions(+), 39 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/mitaka/expand/45f8dd33480b_qos_dscp_db_addition.py create mode 100644 releasenotes/notes/dscp-qos-77ea9b27d3762e48.yaml diff --git a/doc/source/devref/quality_of_service.rst b/doc/source/devref/quality_of_service.rst index 81bd94d9463..c7ff2a67824 100644 --- a/doc/source/devref/quality_of_service.rst +++ b/doc/source/devref/quality_of_service.rst @@ -157,8 +157,13 @@ Base object class is defined in: For QoS, new neutron objects were implemented: * QosPolicy: directly maps to the conceptual policy resource, as defined above. -* QosBandwidthLimitRule: class that represents the only rule type supported by - initial QoS design. +* QosBandwidthLimitRule: defines the instance-egress bandwidth limit rule + type, characterized by a max kbps and a max burst kbits. +* QosDscpMarkingRule: defines the DSCP rule type, characterized by an even integer + between 0 and 56. These integers are the result of the bits in the DiffServ section + 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. Those are defined in: @@ -299,6 +304,18 @@ That approach is less flexible than linux-htb, Queues and OvS QoS profiles, which we may explore in the future, but which will need to be used in combination with openflow rules. +The Open vSwitch DSCP marking implementation relies on the recent addition +of the ovs_agent_extension_api OVSAgentExtensionAPI to request access to the +integration bridge functions: + +* add_flow +* mod_flow +* delete_flows +* dump_flows_for + +The DSCP markings are in fact configured on the port by means of +openflow rules. + SR-IOV ++++++ diff --git a/etc/policy.json b/etc/policy.json index 44963d61187..148b756b594 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -194,6 +194,10 @@ "create_policy_bandwidth_limit_rule": "rule:admin_only", "delete_policy_bandwidth_limit_rule": "rule:admin_only", "update_policy_bandwidth_limit_rule": "rule:admin_only", + "get_policy_dscp_marking_rule": "rule:regular_user", + "create_policy_dscp_marking_rule": "rule:admin_only", + "delete_policy_dscp_marking_rule": "rule:admin_only", + "update_policy_dscp_marking_rule": "rule:admin_only", "get_rule_type": "rule:regular_user", "restrict_wildcard": "(not field:rbac_policy:target_tenant=*) or rule:admin_only", diff --git a/neutron/agent/l2/extensions/qos.py b/neutron/agent/l2/extensions/qos.py index 20ec764be3f..34521dbac55 100644 --- a/neutron/agent/l2/extensions/qos.py +++ b/neutron/agent/l2/extensions/qos.py @@ -63,6 +63,15 @@ class QosAgentDriver(object): """ self._handle_update_create_rules('create', port, qos_policy) + def consume_api(self, agent_api): + """Consume the AgentAPI instance from the QoSAgentExtension class + + This allows QosAgentDrivers to gain access to resources limited to the + NeutronAgent when this method is overridden. + + :param agent_api: An instance of an agent specific API + """ + def update(self, port, qos_policy): """Apply QoS rules on port. @@ -176,6 +185,7 @@ class QosAgentExtension(agent_extension.AgentCoreResourceExtension): self.resource_rpc = resources_rpc.ResourcesPullRpcApi() self.qos_driver = manager.NeutronManager.load_class_for_provider( 'neutron.qos.agent_drivers', driver_type)() + self.qos_driver.consume_api(self.agent_api) self.qos_driver.initialize() self.policy_map = PortPolicyMap() @@ -183,6 +193,9 @@ class QosAgentExtension(agent_extension.AgentCoreResourceExtension): registry.subscribe(self._handle_notification, resources.QOS_POLICY) self._register_rpc_consumers(connection) + def consume_api(self, agent_api): + self.agent_api = agent_api + def _register_rpc_consumers(self, connection): endpoints = [resources_rpc.ResourcesPushRpcCallback()] for resource_type in self.SUPPORTED_RESOURCES: diff --git a/neutron/common/constants.py b/neutron/common/constants.py index f1ef68a51b6..eee92947033 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -120,6 +120,9 @@ IP_PROTOCOL_MAP = {PROTO_NAME_AH: PROTO_NUM_AH, PROTO_NAME_UDPLITE: PROTO_NUM_UDPLITE, PROTO_NAME_VRRP: PROTO_NUM_VRRP} +VALID_DSCP_MARKS = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, + 36, 38, 40, 46, 48, 56] + # List of ICMPv6 types that should be allowed by default: # Multicast Listener Query (130), # Multicast Listener Report (131), diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 2b79b8e15cc..e796b9d0fef 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -0e66c5227a8a +45f8dd33480b diff --git a/neutron/db/migration/alembic_migrations/versions/mitaka/expand/45f8dd33480b_qos_dscp_db_addition.py b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/45f8dd33480b_qos_dscp_db_addition.py new file mode 100644 index 00000000000..48f2217efa3 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/45f8dd33480b_qos_dscp_db_addition.py @@ -0,0 +1,40 @@ +# Copyright 2015 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 dscp db addition + +Revision ID: 45f8dd33480b +Revises: 0e66c5227a8a +Create Date: 2015-12-03 07:16:24.742290 + +""" + +# revision identifiers, used by Alembic. +revision = '45f8dd33480b' +down_revision = '0e66c5227a8a' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + + op.create_table( + 'qos_dscp_marking_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, unique=True), + sa.Column('dscp_mark', sa.Integer())) diff --git a/neutron/db/qos/models.py b/neutron/db/qos/models.py index 84b93ee2c84..1ec3351ad69 100755 --- a/neutron/db/qos/models.py +++ b/neutron/db/qos/models.py @@ -77,3 +77,13 @@ class QosBandwidthLimitRule(model_base.HasId, model_base.BASEV2): unique=True) max_kbps = sa.Column(sa.Integer) max_burst_kbps = sa.Column(sa.Integer) + + +class QosDscpMarkingRule(models_v2.HasId, model_base.BASEV2): + __tablename__ = 'qos_dscp_marking_rules' + qos_policy_id = sa.Column(sa.String(36), + sa.ForeignKey('qos_policies.id', + ondelete='CASCADE'), + nullable=False, + unique=True) + dscp_mark = sa.Column(sa.Integer) diff --git a/neutron/extensions/qos.py b/neutron/extensions/qos.py index ba9eeaf2225..d3cd6bf52dc 100644 --- a/neutron/extensions/qos.py +++ b/neutron/extensions/qos.py @@ -22,6 +22,7 @@ from neutron.api import extensions from neutron.api.v2 import attributes as attr from neutron.api.v2 import base from neutron.api.v2 import resource_helper +from neutron.common import constants as common_constants from neutron import manager from neutron.plugins.common import constants from neutron.services.qos import qos_consts @@ -78,6 +79,17 @@ SUB_RESOURCE_ATTRIBUTE_MAP = { 'allow_post': True, 'allow_put': True, 'is_visible': True, 'default': 0, 'validate': {'type:non_negative': None}}}) + }, + 'dscp_marking_rules': { + 'parent': {'collection_name': 'policies', + 'member_name': 'policy'}, + 'parameters': dict(QOS_RULE_COMMON_FIELDS, + **{'dscp_mark': { + 'allow_post': True, 'allow_put': True, + 'convert_to': attr.convert_to_int, + 'is_visible': True, 'default': None, + 'validate': {'type:values': common_constants. + VALID_DSCP_MARKS}}}) } } @@ -229,6 +241,32 @@ class QoSPluginBase(service_base.ServicePluginBase): def delete_policy_bandwidth_limit_rule(self, context, rule_id, policy_id): pass + @abc.abstractmethod + def get_policy_dscp_marking_rule(self, context, rule_id, + policy_id, fields=None): + pass + + @abc.abstractmethod + def get_policy_dscp_marking_rules(self, context, policy_id, + filters=None, fields=None, + sorts=None, limit=None, + marker=None, page_reverse=False): + pass + + @abc.abstractmethod + def create_policy_dscp_marking_rule(self, context, policy_id, + dscp_marking_rule): + pass + + @abc.abstractmethod + def update_policy_dscp_marking_rule(self, context, rule_id, policy_id, + dscp_marking_rule): + pass + + @abc.abstractmethod + def delete_policy_dscp_marking_rule(self, context, rule_id, policy_id): + pass + @abc.abstractmethod def get_rule_types(self, context, filters=None, fields=None, sorts=None, limit=None, diff --git a/neutron/objects/common_types.py b/neutron/objects/common_types.py index ad27327bc61..76404f08f37 100644 --- a/neutron/objects/common_types.py +++ b/neutron/objects/common_types.py @@ -12,7 +12,9 @@ # under the License. from oslo_versionedobjects import fields as obj_fields +import six +from neutron._i18n import _ from neutron.common import constants @@ -27,3 +29,46 @@ class IPV6ModeEnumField(obj_fields.BaseEnumField): def __init__(self, **kwargs): self.AUTO_TYPE = IPV6ModeEnum() super(IPV6ModeEnumField, self).__init__(**kwargs) + + +class IntegerEnum(obj_fields.Integer): + def __init__(self, valid_values=None, **kwargs): + if not valid_values: + msg = _("No possible values specified") + raise ValueError(msg) + for value in valid_values: + if not isinstance(value, six.integer_types): + msg = _("Possible value %s is not an integer") % value + raise ValueError(msg) + self._valid_values = valid_values + super(IntegerEnum, self).__init__(**kwargs) + + def _validate_value(self, value): + if not isinstance(value, six.integer_types): + msg = _("Field value %s is not an integer") % value + raise ValueError(msg) + if value not in self._valid_values: + msg = ( + _("Field value %(value)s is not in the list " + "of valid values: %(values)s") % + {'value': value, 'values': self._valid_values} + ) + raise ValueError(msg) + + def coerce(self, obj, attr, value): + self._validate_value(value) + return super(IntegerEnum, self).coerce(obj, attr, value) + + def stringify(self, value): + self._validate_value(value) + return super(IntegerEnum, self).stringify(value) + + +class DscpMark(IntegerEnum): + def __init__(self, valid_values=None, **kwargs): + super(DscpMark, self).__init__( + valid_values=constants.VALID_DSCP_MARKS) + + +class DscpMarkField(obj_fields.AutoTypedField): + AUTO_TYPE = DscpMark() diff --git a/neutron/objects/qos/policy.py b/neutron/objects/qos/policy.py index 5aa9269d4c8..fdc9b985be5 100644 --- a/neutron/objects/qos/policy.py +++ b/neutron/objects/qos/policy.py @@ -15,6 +15,7 @@ import itertools +from oslo_utils import versionutils from oslo_versionedobjects import base as obj_base from oslo_versionedobjects import fields as obj_fields from six import add_metaclass @@ -36,7 +37,8 @@ from neutron.objects import rbac_db @add_metaclass(rbac_db.RbacNeutronMetaclass) class QosPolicy(base.NeutronDbObject): # Version 1.0: Initial version - VERSION = '1.0' + # Version 1.1: QosDscpMarkingRule introduced + VERSION = '1.1' # required by RbacNeutronMetaclass rbac_db_model = QosPolicyRBAC @@ -206,3 +208,13 @@ class QosPolicy(base.NeutronDbObject): cls._get_bound_tenant_ids(context.session, qosport, port, qosport.port_id, policy_id)) return set(bound_tenants) + + def obj_make_compatible(self, primitive, target_version): + _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']) diff --git a/neutron/objects/qos/rule.py b/neutron/objects/qos/rule.py index 1cf90eb04ed..9903185e2a5 100644 --- a/neutron/objects/qos/rule.py +++ b/neutron/objects/qos/rule.py @@ -25,8 +25,11 @@ from neutron.common import utils from neutron.db import api as db_api from neutron.db.qos import models as qos_db_model from neutron.objects import base +from neutron.objects import common_types from neutron.services.qos import qos_consts +DSCP_MARK = 'dscp_mark' + def get_rules(context, qos_policy_id): all_rules = [] @@ -42,6 +45,13 @@ def get_rules(context, qos_policy_id): @six.add_metaclass(abc.ABCMeta) class QosRule(base.NeutronDbObject): + # Version 1.0: Initial version, only BandwidthLimitRule + # 1.1: Added DscpMarkingRule + # + #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' fields = { 'id': obj_fields.UUIDField(), @@ -77,8 +87,6 @@ class QosRule(base.NeutronDbObject): @obj_base.VersionedObjectRegistry.register class QosBandwidthLimitRule(QosRule): - # Version 1.0: Initial version - VERSION = '1.0' db_model = qos_db_model.QosBandwidthLimitRule @@ -88,3 +96,15 @@ class QosBandwidthLimitRule(QosRule): } rule_type = qos_consts.RULE_TYPE_BANDWIDTH_LIMIT + + +@obj_base.VersionedObjectRegistry.register +class QosDscpMarkingRule(QosRule): + + db_model = qos_db_model.QosDscpMarkingRule + + fields = { + DSCP_MARK: common_types.DscpMarkField(), + } + + rule_type = qos_consts.RULE_TYPE_DSCP_MARK diff --git a/neutron/objects/qos/rule_type.py b/neutron/objects/qos/rule_type.py index bb5d9bd7407..8014a2edd45 100644 --- a/neutron/objects/qos/rule_type.py +++ b/neutron/objects/qos/rule_type.py @@ -29,8 +29,10 @@ class RuleTypeField(obj_fields.BaseEnumField): @obj_base.VersionedObjectRegistry.register class QosRuleType(base.NeutronObject): # Version 1.0: Initial version - VERSION = '1.0' + # Version 1.1: Added DscpMarkingRule + VERSION = '1.1' + #TODO(davidsha) add obj_make_compatible and associated tests. fields = { 'type': RuleTypeField(), } diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/extension_drivers/qos_driver.py b/neutron/plugins/ml2/drivers/openvswitch/agent/extension_drivers/qos_driver.py index bf7147cc027..eadb05238c5 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/extension_drivers/qos_driver.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/extension_drivers/qos_driver.py @@ -14,7 +14,6 @@ from oslo_config import cfg -from neutron.agent.common import ovs_lib from neutron.agent.l2.extensions import qos from neutron.plugins.ml2.drivers.openvswitch.mech_driver import ( mech_openvswitch) @@ -29,9 +28,14 @@ class QosOVSAgentDriver(qos.QosAgentDriver): super(QosOVSAgentDriver, self).__init__() self.br_int_name = cfg.CONF.OVS.integration_bridge self.br_int = None + self.agent_api = None + + def consume_api(self, agent_api): + self.agent_api = agent_api def initialize(self): - self.br_int = ovs_lib.OVSBridge(self.br_int_name) + self.br_int = self.agent_api.request_int_br() + self.cookie = self.br_int.default_cookie def create_bandwidth_limit(self, port, rule): self.update_bandwidth_limit(port, rule) @@ -48,3 +52,45 @@ class QosOVSAgentDriver(qos.QosAgentDriver): def delete_bandwidth_limit(self, port): port_name = port['vif_port'].port_name self.br_int.delete_egress_bw_limit_for_port(port_name) + + def create_dscp_marking(self, port, rule): + self.update_dscp_marking(port, rule) + + def update_dscp_marking(self, port, rule): + port_name = port['vif_port'].port_name + port = self.br_int.get_port_ofport(port_name) + mark = rule.dscp_mark + #mark needs to be bit shifted 2 left to not overwrite the + #lower 2 bits of type of service packet header. + #source: man ovs-ofctl (/mod_nw_tos) + mark = str(mark << 2) + + # reg2 is a metadata field that does not alter packets. + # By loading a value into this field and checking if the value is + # altered it allows the packet to be resubmitted and go through + # the flow table again to be identified by other flows. + flows = self.br_int.dump_flows_for(cookie=self.cookie, table=0, + in_port=port, reg2=0) + if not flows: + actions = ("mod_nw_tos:" + mark + ",load:55->NXM_NX_REG2[0..5]," + + "resubmit(,0)") + self.br_int.add_flow(in_port=port, table=0, priority=65535, + reg2=0, actions=actions) + else: + for flow in flows: + actions = str(flow).partition("actions=")[2] + acts = actions.split(',') + # mod_nw_tos = modify type of service header + # This is the second byte of the IPv4 packet header. + # DSCP makes up the upper 6 bits of this header field. + actions = "mod_nw_tos:" + mark + "," + actions += ','.join([act for act in acts + if "mod_nw_tos:" not in act]) + self.br_int.mod_flows(reg2=0, in_port=port, table=0, + actions=actions) + + def delete_dscp_marking(self, port): + port_name = port['vif_port'].port_name + port = self.br_int.get_port_ofport(port_name) + + self.br_int.delete_flows(in_port=port, table=0, reg2=0) diff --git a/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py b/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py index da3f6377c94..5ac14f0a6a8 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py +++ b/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py @@ -41,7 +41,8 @@ class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase): network. """ - supported_qos_rule_types = [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT] + supported_qos_rule_types = [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, + qos_consts.RULE_TYPE_DSCP_MARK] def __init__(self): sg_enabled = securitygroups_rpc.is_firewall_enabled() diff --git a/neutron/services/qos/qos_consts.py b/neutron/services/qos/qos_consts.py index 3eb78d517d5..50ec27d7593 100644 --- a/neutron/services/qos/qos_consts.py +++ b/neutron/services/qos/qos_consts.py @@ -14,6 +14,7 @@ # under the License. RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth_limit' -VALID_RULE_TYPES = [RULE_TYPE_BANDWIDTH_LIMIT] +RULE_TYPE_DSCP_MARK = 'dscp_marking' +VALID_RULE_TYPES = [RULE_TYPE_BANDWIDTH_LIMIT, RULE_TYPE_DSCP_MARK] QOS_POLICY_ID = 'qos_policy_id' diff --git a/neutron/services/qos/qos_plugin.py b/neutron/services/qos/qos_plugin.py index 80475161fe4..181798cf1cd 100644 --- a/neutron/services/qos/qos_plugin.py +++ b/neutron/services/qos/qos_plugin.py @@ -81,7 +81,7 @@ class QoSPlugin(qos.QoSPluginBase): page_reverse=False): return policy_object.QosPolicy.get_objects(context, **filters) - #TODO(QoS): Consider adding a proxy catch-all for rules, so + #TODO(mangelajo): need to add a proxy catch-all for rules, so # we capture the API function call, and just pass # the rule type as a parameter removing lots of # future code duplication when we have more rules. @@ -159,6 +159,78 @@ class QoSPlugin(qos.QoSPluginBase): return rule_object.QosBandwidthLimitRule.get_objects(context, **filters) + @db_base_plugin_common.convert_result_to_dict + def create_policy_dscp_marking_rule(self, context, policy_id, + dscp_marking_rule): + with db_api.autonested_transaction(context.session): + # first, validate that we have access to the policy + policy = self._get_policy_obj(context, policy_id) + rule = rule_object.QosDscpMarkingRule( + context, qos_policy_id=policy_id, + **dscp_marking_rule['dscp_marking_rule']) + rule.create() + policy.reload_rules() + self.notification_driver_manager.update_policy(context, policy) + return rule + + @db_base_plugin_common.convert_result_to_dict + def update_policy_dscp_marking_rule(self, context, rule_id, policy_id, + dscp_marking_rule): + with db_api.autonested_transaction(context.session): + # first, validate that we have access to the policy + policy = self._get_policy_obj(context, policy_id) + # check if the rule belong to the policy + policy.get_rule_by_id(rule_id) + rule = rule_object.QosDscpMarkingRule( + context, id=rule_id) + rule.obj_reset_changes() + for k, v in dscp_marking_rule['dscp_marking_rule'].items(): + if k != 'id': + setattr(rule, k, v) + rule.update() + policy.reload_rules() + self.notification_driver_manager.update_policy(context, policy) + return rule + + def delete_policy_dscp_marking_rule(self, context, rule_id, policy_id): + # make sure we will have a policy object to push resource update + with db_api.autonested_transaction(context.session): + # first, validate that we have access to the policy + policy = self._get_policy_obj(context, policy_id) + rule = policy.get_rule_by_id(rule_id) + rule.delete() + policy.reload_rules() + self.notification_driver_manager.update_policy(context, policy) + + @db_base_plugin_common.filter_fields + @db_base_plugin_common.convert_result_to_dict + def get_policy_dscp_marking_rule(self, context, rule_id, + policy_id, fields=None): + # make sure we have access to the policy when fetching the rule + with db_api.autonested_transaction(context.session): + # first, validate that we have access to the policy + self._get_policy_obj(context, policy_id) + rule = rule_object.QosDscpMarkingRule.get_object( + context, id=rule_id) + if not rule: + raise n_exc.QosRuleNotFound(policy_id=policy_id, rule_id=rule_id) + return rule + + @db_base_plugin_common.filter_fields + @db_base_plugin_common.convert_result_to_dict + def get_policy_dscp_marking_rules(self, context, policy_id, + filters=None, fields=None, + sorts=None, limit=None, + marker=None, page_reverse=False): + # make sure we have access to the policy when fetching rules + with db_api.autonested_transaction(context.session): + # first, validate that we have access to the policy + self._get_policy_obj(context, policy_id) + filters = filters or dict() + filters[qos_consts.QOS_POLICY_ID] = policy_id + return rule_object.QosDscpMarkingRule.get_objects(context, + **filters) + # TODO(QoS): enforce rule types when accessing rule objects @db_base_plugin_common.filter_fields @db_base_plugin_common.convert_result_to_dict diff --git a/neutron/tests/api/test_qos.py b/neutron/tests/api/test_qos.py index 031d5e0f223..c45fc0ab426 100644 --- a/neutron/tests/api/test_qos.py +++ b/neutron/tests/api/test_qos.py @@ -704,3 +704,144 @@ class RbacSharedQosPoliciesTest(base.BaseAdminNetworkTest): # make sure the rbac-policy is invisible to the tenant for which it's # being shared self.assertFalse(self.client.list_rbac_policies()['rbac_policies']) + + +class QosDscpMarkingRuleTestJSON(base.BaseAdminNetworkTest): + VALID_DSCP_MARK1 = 56 + VALID_DSCP_MARK2 = 48 + + @classmethod + def resource_setup(cls): + super(QosDscpMarkingRuleTestJSON, cls).resource_setup() + if not test.is_extension_enabled('qos', 'network'): + msg = "qos extension not enabled." + raise cls.skipException(msg) + + @test.attr(type='smoke') + @test.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.admin_client.create_dscp_marking_rule( + policy['id'], self.VALID_DSCP_MARK1)['dscp_marking_rule'] + + # Test 'show rule' + retrieved_rule = self.admin_client.show_dscp_marking_rule( + policy['id'], rule['id']) + retrieved_rule = retrieved_rule['dscp_marking_rule'] + self.assertEqual(rule['id'], retrieved_rule['id']) + self.assertEqual(self.VALID_DSCP_MARK1, retrieved_rule['dscp_mark']) + + # Test 'list rules' + rules = self.admin_client.list_dscp_marking_rules(policy['id']) + rules = rules['dscp_marking_rules'] + 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_DSCP_MARK, + policy_rules[0]['type']) + + @test.attr(type='smoke') + @test.idempotent_id('8a59b00b-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_dscp_marking_rule( + policy['id'], self.VALID_DSCP_MARK1)['dscp_marking_rule'] + + self.assertRaises(exceptions.Conflict, + self.admin_client.create_dscp_marking_rule, + policy_id=policy['id'], + dscp_mark=self.VALID_DSCP_MARK2) + + @test.attr(type='smoke') + @test.idempotent_id('149a6988-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_dscp_marking_rule( + policy['id'], self.VALID_DSCP_MARK1)['dscp_marking_rule'] + + self.admin_client.update_dscp_marking_rule( + policy['id'], rule['id'], dscp_mark=self.VALID_DSCP_MARK2) + + retrieved_policy = self.admin_client.show_dscp_marking_rule( + policy['id'], rule['id']) + retrieved_policy = retrieved_policy['dscp_marking_rule'] + self.assertEqual(self.VALID_DSCP_MARK2, retrieved_policy['dscp_mark']) + + @test.attr(type='smoke') + @test.idempotent_id('67ee6efd-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_dscp_marking_rule( + policy['id'], self.VALID_DSCP_MARK1)['dscp_marking_rule'] + + retrieved_policy = self.admin_client.show_dscp_marking_rule( + policy['id'], rule['id']) + retrieved_policy = retrieved_policy['dscp_marking_rule'] + self.assertEqual(rule['id'], retrieved_policy['id']) + + self.admin_client.delete_dscp_marking_rule(policy['id'], rule['id']) + self.assertRaises(exceptions.NotFound, + self.admin_client.show_dscp_marking_rule, + policy['id'], rule['id']) + + @test.attr(type='smoke') + @test.idempotent_id('f211222c-5808-46cb-a961-983bbab6b852') + def test_rule_create_rule_nonexistent_policy(self): + self.assertRaises( + exceptions.NotFound, + self.admin_client.create_dscp_marking_rule, + 'policy', self.VALID_DSCP_MARK1) + + @test.attr(type='smoke') + @test.idempotent_id('a4a2e7ad-786f-4927-a85a-e545a93bd274') + def test_rule_create_forbidden_for_regular_tenants(self): + self.assertRaises( + exceptions.Forbidden, + self.client.create_dscp_marking_rule, + 'policy', self.VALID_DSCP_MARK1) + + @test.attr(type='smoke') + @test.idempotent_id('33646b08-4f05-4493-a48a-bde768a18533') + def test_invalid_rule_create(self): + policy = self.create_qos_policy(name='test-policy', + description='test policy', + shared=False) + self.assertRaises( + exceptions.BadRequest, + self.admin_client.create_dscp_marking_rule, + policy['id'], 58) + + @test.attr(type='smoke') + @test.idempotent_id('ce0bd0c2-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_dscp_marking_rule( + policy1['id'], self.VALID_DSCP_MARK1)['dscp_marking_rule'] + + policy2 = self.create_qos_policy(name='test-policy2', + description='test policy2', + shared=False) + rule2 = self.admin_client.create_dscp_marking_rule( + policy2['id'], self.VALID_DSCP_MARK2)['dscp_marking_rule'] + + # Test 'list rules' + rules = self.admin_client.list_dscp_marking_rules(policy1['id']) + rules = rules['dscp_marking_rules'] + rules_ids = [r['id'] for r in rules] + self.assertIn(rule1['id'], rules_ids) + self.assertNotIn(rule2['id'], rules_ids) diff --git a/neutron/tests/common/agents/l2_extensions.py b/neutron/tests/common/agents/l2_extensions.py index 11b354eeb3b..2f80bcbe7a3 100644 --- a/neutron/tests/common/agents/l2_extensions.py +++ b/neutron/tests/common/agents/l2_extensions.py @@ -16,6 +16,18 @@ from neutron.agent.linux import utils as agent_utils +def extract_mod_nw_tos_action(flows): + tos_mark = None + if flows: + flow_list = flows.splitlines() + for flow in flow_list: + if 'mod_nw_tos' in flow: + actions = flow.partition('actions=')[2] + after_mod = actions.partition('mod_nw_tos:')[2] + tos_mark = int(after_mod.partition(',')[0]) + return tos_mark + + def wait_until_bandwidth_limit_rule_applied(bridge, port_vif, rule): def _bandwidth_limit_rule_applied(): bw_rule = bridge.get_egress_bw_limit_for_port(port_vif) @@ -25,3 +37,18 @@ def wait_until_bandwidth_limit_rule_applied(bridge, port_vif, rule): return bw_rule == expected agent_utils.wait_until_true(_bandwidth_limit_rule_applied) + + +def wait_until_dscp_marking_rule_applied(bridge, port_vif, rule): + def _dscp_marking_rule_applied(): + port_num = bridge.get_port_ofport(port_vif) + + flows = bridge.dump_flows_for(table='0', in_port=str(port_num)) + dscp_mark = extract_mod_nw_tos_action(flows) + + expected = None + if rule: + expected = rule + return dscp_mark == expected + + agent_utils.wait_until_true(_dscp_marking_rule_applied) diff --git a/neutron/tests/etc/policy.json b/neutron/tests/etc/policy.json index 44963d61187..148b756b594 100644 --- a/neutron/tests/etc/policy.json +++ b/neutron/tests/etc/policy.json @@ -194,6 +194,10 @@ "create_policy_bandwidth_limit_rule": "rule:admin_only", "delete_policy_bandwidth_limit_rule": "rule:admin_only", "update_policy_bandwidth_limit_rule": "rule:admin_only", + "get_policy_dscp_marking_rule": "rule:regular_user", + "create_policy_dscp_marking_rule": "rule:admin_only", + "delete_policy_dscp_marking_rule": "rule:admin_only", + "update_policy_dscp_marking_rule": "rule:admin_only", "get_rule_type": "rule:regular_user", "restrict_wildcard": "(not field:rbac_policy:target_tenant=*) or rule:admin_only", diff --git a/neutron/tests/fullstack/resources/config.py b/neutron/tests/fullstack/resources/config.py index b67fccf72bb..0d40498aef5 100644 --- a/neutron/tests/fullstack/resources/config.py +++ b/neutron/tests/fullstack/resources/config.py @@ -114,7 +114,7 @@ class ML2ConfigFixture(ConfigFixture): super(ML2ConfigFixture, self).__init__( env_desc, host_desc, temp_dir, base_filename='ml2_conf.ini') - mechanism_drivers = 'openvswitch,linuxbridge' + mechanism_drivers = self.env_desc.mech_drivers if self.env_desc.l2_pop: mechanism_drivers += ',l2population' diff --git a/neutron/tests/fullstack/resources/environment.py b/neutron/tests/fullstack/resources/environment.py index 51a9980aad0..5e8ca56906a 100644 --- a/neutron/tests/fullstack/resources/environment.py +++ b/neutron/tests/fullstack/resources/environment.py @@ -35,11 +35,13 @@ class EnvironmentDescription(object): Does the setup, as a whole, support tunneling? How about l2pop? """ - def __init__(self, network_type='vxlan', l2_pop=True, qos=False): + def __init__(self, network_type='vxlan', l2_pop=True, qos=False, + mech_drivers='openvswitch,linuxbridge'): self.network_type = network_type self.l2_pop = l2_pop self.qos = qos self.network_range = None + self.mech_drivers = mech_drivers @property def tunneling_enabled(self): diff --git a/neutron/tests/fullstack/test_qos.py b/neutron/tests/fullstack/test_qos.py index 10d5c1b5f45..00a41b94661 100644 --- a/neutron/tests/fullstack/test_qos.py +++ b/neutron/tests/fullstack/test_qos.py @@ -163,8 +163,16 @@ class TestQoSWithL2Agent(base.BaseFullStackTestCase): class TestQoSWithL2Population(base.BaseFullStackTestCase): def setUp(self): + # We limit this test to using the openvswitch mech driver, because DSCP + # is presently not implemented for Linux Bridge. The 'rule_types' API + # call only returns rule types that are supported by all configured + # mech drivers. So in a fullstack scenario, where both the OVS and the + # Linux Bridge mech drivers are configured, the DSCP rule type will be + # unavailable since it is not implemented in Linux Bridge. + mech_driver = 'openvswitch' host_desc = [] # No need to register agents for this test case - env_desc = environment.EnvironmentDescription(qos=True, l2_pop=True) + env_desc = environment.EnvironmentDescription(qos=True, l2_pop=True, + mech_drivers=mech_driver) env = environment.Environment(env_desc, host_desc) super(TestQoSWithL2Population, self).setUp(env) diff --git a/neutron/tests/functional/agent/l2/extensions/test_ovs_agent_qos_extension.py b/neutron/tests/functional/agent/l2/extensions/test_ovs_agent_qos_extension.py index 9b73e733636..580395cf3ff 100644 --- a/neutron/tests/functional/agent/l2/extensions/test_ovs_agent_qos_extension.py +++ b/neutron/tests/functional/agent/l2/extensions/test_ovs_agent_qos_extension.py @@ -29,6 +29,18 @@ from neutron.tests.functional.agent.l2 import base TEST_POLICY_ID1 = "a2d72369-4246-4f19-bd3c-af51ec8d70cd" TEST_POLICY_ID2 = "46ebaec0-0570-43ac-82f6-60d2b03168c5" +TEST_DSCP_MARK_1 = 14 +TEST_DSCP_MARK_2 = 30 +TEST_DSCP_MARKING_RULE_1 = rule.QosDscpMarkingRule( + context=None, + qos_policy_id=TEST_POLICY_ID1, + id="9f126d84-551a-4dcf-bb01-0e9c0df0c793", + dscp_mark=TEST_DSCP_MARK_1) +TEST_DSCP_MARKING_RULE_2 = rule.QosDscpMarkingRule( + context=None, + qos_policy_id=TEST_POLICY_ID2, + id="7f126d84-551a-4dcf-bb01-0e9c0df0c793", + dscp_mark=TEST_DSCP_MARK_2) TEST_BW_LIMIT_RULE_1 = rule.QosBandwidthLimitRule( context=None, qos_policy_id=TEST_POLICY_ID1, @@ -48,8 +60,12 @@ class OVSAgentQoSExtensionTestFramework(base.OVSAgentTestFramework): super(OVSAgentQoSExtensionTestFramework, self).setUp() self.config.set_override('extensions', ['qos'], 'agent') self._set_pull_mock() - self.set_test_qos_rules(TEST_POLICY_ID1, [TEST_BW_LIMIT_RULE_1]) - self.set_test_qos_rules(TEST_POLICY_ID2, [TEST_BW_LIMIT_RULE_2]) + self.set_test_qos_rules(TEST_POLICY_ID1, + [TEST_BW_LIMIT_RULE_1, + TEST_DSCP_MARKING_RULE_1]) + self.set_test_qos_rules(TEST_POLICY_ID2, + [TEST_BW_LIMIT_RULE_2, + TEST_DSCP_MARKING_RULE_2]) def _set_pull_mock(self): @@ -107,6 +123,27 @@ class OVSAgentQoSExtensionTestFramework(base.OVSAgentTestFramework): l2_extensions.wait_until_bandwidth_limit_rule_applied( self.agent.int_br, port['vif_name'], rule) + def _assert_dscp_marking_rule_is_set(self, port, dscp_rule): + port_num = self.agent.int_br._get_port_ofport(port['vif_name']) + + flows = self.agent.int_br.dump_flows_for(table='0', + in_port=str(port_num)) + tos_mark = l2_extensions.extract_mod_nw_tos_action(flows) + self.assertEqual(dscp_rule.dscp_mark << 2, tos_mark) + + def _assert_dscp_marking_rule_not_set(self, port): + port_num = self.agent.int_br._get_port_ofport(port['vif_name']) + + flows = self.agent.int_br.dump_flows_for(table='0', + in_port=str(port_num)) + + tos_mark = l2_extensions.extract_mod_nw_tos_action(flows) + self.assertIsNone(tos_mark) + + def wait_until_dscp_marking_rule_applied(self, port, dscp_mark): + l2_extensions.wait_until_dscp_marking_rule_applied( + self.agent.int_br, port['vif_name'], dscp_mark) + def _create_port_with_qos(self): port_dict = self._create_test_port_dict() port_dict['qos_policy_id'] = TEST_POLICY_ID1 @@ -150,6 +187,37 @@ class TestOVSAgentQosExtension(OVSAgentQoSExtensionTestFramework): self._assert_bandwidth_limit_rule_not_set(self.ports[2]) + def test_port_creation_with_dscp_marking(self): + """Make sure dscp marking rules are set in low level to ports.""" + + self.setup_agent_and_ports( + port_dicts=self.create_test_ports(amount=1, + policy_id=TEST_POLICY_ID1)) + self.wait_until_ports_state(self.ports, up=True) + + for port in self.ports: + self._assert_dscp_marking_rule_is_set( + port, TEST_DSCP_MARKING_RULE_1) + + def test_port_creation_with_different_dscp_markings(self): + """Make sure different types of policies end on the right ports.""" + + port_dicts = self.create_test_ports(amount=3) + + port_dicts[0]['qos_policy_id'] = TEST_POLICY_ID1 + port_dicts[1]['qos_policy_id'] = TEST_POLICY_ID2 + + self.setup_agent_and_ports(port_dicts) + self.wait_until_ports_state(self.ports, up=True) + + self._assert_dscp_marking_rule_is_set(self.ports[0], + TEST_DSCP_MARKING_RULE_1) + + self._assert_dscp_marking_rule_is_set(self.ports[1], + TEST_DSCP_MARKING_RULE_2) + + self._assert_dscp_marking_rule_not_set(self.ports[2]) + def test_simple_port_policy_update(self): self.setup_agent_and_ports( port_dicts=self.create_test_ports(amount=1, diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py index 0795d58b935..18f33e78e67 100644 --- a/neutron/tests/tempest/services/network/json/network_client.py +++ b/neutron/tests/tempest/services/network/json/network_client.py @@ -699,6 +699,50 @@ class NetworkClientJSON(service_client.RestClient): self.expected_success(204, resp.status) return service_client.ResponseBody(resp, body) + def create_dscp_marking_rule(self, policy_id, dscp_mark): + uri = '%s/qos/policies/%s/dscp_marking_rules' % ( + self.uri_prefix, policy_id) + post_data = self.serialize( + {'dscp_marking_rule': { + 'dscp_mark': dscp_mark} + }) + resp, body = self.post(uri, post_data) + self.expected_success(201, resp.status) + body = json.loads(body) + return service_client.ResponseBody(resp, body) + + def list_dscp_marking_rules(self, policy_id): + uri = '%s/qos/policies/%s/dscp_marking_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_dscp_marking_rule(self, policy_id, rule_id): + uri = '%s/qos/policies/%s/dscp_marking_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_dscp_marking_rule(self, policy_id, rule_id, **kwargs): + uri = '%s/qos/policies/%s/dscp_marking_rules/%s' % ( + self.uri_prefix, policy_id, rule_id) + post_data = {'dscp_marking_rule': kwargs} + resp, body = self.put(uri, json.dumps(post_data)) + body = self.deserialize_single(body) + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def delete_dscp_marking_rule(self, policy_id, rule_id): + uri = '%s/qos/policies/%s/dscp_marking_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/agent/l2/extensions/test_qos.py b/neutron/tests/unit/agent/l2/extensions/test_qos.py index de1b1a499f3..16e2f6e248a 100755 --- a/neutron/tests/unit/agent/l2/extensions/test_qos.py +++ b/neutron/tests/unit/agent/l2/extensions/test_qos.py @@ -25,7 +25,11 @@ from neutron.common import exceptions from neutron import context from neutron.objects.qos import policy from neutron.objects.qos import rule +from neutron.plugins.ml2.drivers.openvswitch.agent import ( + ovs_agent_extension_api as ovs_ext_api) from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants +from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.ovs_ofctl import ( + ovs_bridge) from neutron.services.qos import qos_consts from neutron.tests import base @@ -128,6 +132,10 @@ class QosExtensionBaseTestCase(base.BaseTestCase): self.qos_ext = qos.QosAgentExtension() self.context = context.get_admin_context() self.connection = mock.Mock() + self.agent_api = ovs_ext_api.OVSAgentExtensionAPI( + ovs_bridge.OVSAgentBridge('br-int'), + ovs_bridge.OVSAgentBridge('br-tun')) + self.qos_ext.consume_api(self.agent_api) # Don't rely on used driver mock.patch( diff --git a/neutron/tests/unit/objects/qos/test_policy.py b/neutron/tests/unit/objects/qos/test_policy.py index 80a8b25ee28..e455ff2da81 100644 --- a/neutron/tests/unit/objects/qos/test_policy.py +++ b/neutron/tests/unit/objects/qos/test_policy.py @@ -31,12 +31,17 @@ class QosPolicyObjectTestCase(test_base.BaseObjectIfaceTestCase): self.get_random_fields(rule.QosBandwidthLimitRule) for _ in range(3)] + self.db_qos_dscp_rules = [ + self.get_random_fields(rule.QosDscpMarkingRule) + 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.QosBandwidthLimitRule.db_model: self.db_qos_bandwidth_rules, + rule.QosDscpMarkingRule.db_model: self.db_qos_dscp_rules} self.get_object = mock.patch.object( db_api, 'get_object', side_effect=self.fake_get_object).start() @@ -121,7 +126,7 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, policy_obj.create() return policy_obj - def _create_test_policy_with_rule(self): + def _create_test_policy_with_bwrule(self): policy_obj = self._create_test_policy() rule_fields = self.get_random_fields( @@ -227,21 +232,22 @@ 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_rule() + policy_obj, rule_obj = self._create_test_policy_with_bwrule() policy_obj = policy.QosPolicy.get_object(self.context, id=policy_obj.id) 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_rule() + policy_obj, rule_obj = self._create_test_policy_with_bwrule() policy_obj = policy.QosPolicy.get_object(self.context, id=policy_obj.id) + 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_rule() + policy_obj, rule_obj = self._create_test_policy_with_bwrule() policy_obj = policy.QosPolicy.get_object(self.context, id=policy_obj.id) @@ -278,7 +284,7 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, obj.delete() def test_reload_rules_reloads_rules(self): - policy_obj, rule_obj = self._create_test_policy_with_rule() + policy_obj, rule_obj = self._create_test_policy_with_bwrule() self.assertEqual([], policy_obj.rules) policy_obj.reload_rules() @@ -293,3 +299,42 @@ class QosPolicyDbObjectTestCase(test_base.BaseDbObjectTestCase, obj.detach_port(self._port['id']) obj.delete() + + @staticmethod + def _policy_through_version(obj, version): + 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_v1_1 = self._policy_through_version(policy_obj, '1.1') + + self.assertIn(rule_obj_band, policy_obj_v1_1.rules) + self.assertIn(rule_obj_dscp, policy_obj_v1_1.rules) + self.assertEqual(policy_obj.VERSION, '1.1') + + #TODO(davidsha) add testing for object version incrementation + def test_object_version_degradation_1_1_to_1_0(self): + policy_obj, rule_obj_band, rule_obj_dscp = ( + self._create_test_policy_with_bw_and_dscp()) + + 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) + #NOTE(mangelajo): we should not check .VERSION, since that's the + # local version on the class definition diff --git a/neutron/tests/unit/objects/qos/test_rule.py b/neutron/tests/unit/objects/qos/test_rule.py index e73778287e6..aa9468a580a 100644 --- a/neutron/tests/unit/objects/qos/test_rule.py +++ b/neutron/tests/unit/objects/qos/test_rule.py @@ -84,3 +84,22 @@ class QosBandwidthLimitRuleDbObjectTestCase(test_base.BaseDbObjectTestCase, policy_obj = policy.QosPolicy(self.context, id=generated_qos_policy_id) policy_obj.create() + + +class QosDscpMarkingRuleObjectTestCase(test_base.BaseObjectIfaceTestCase): + + _test_class = rule.QosDscpMarkingRule + + +class QosDscpMarkingRuleDbObjectTestCase(test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase): + + _test_class = rule.QosDscpMarkingRule + + def setUp(self): + super(QosDscpMarkingRuleDbObjectTestCase, self).setUp() + # Prepare policy to be able to insert a rule + generated_qos_policy_id = self.db_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 d2a8cf6222e..28cef311254 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -11,6 +11,7 @@ # under the License. import copy +import random import mock from oslo_db import exception as obj_exc @@ -19,11 +20,13 @@ from oslo_versionedobjects import base as obj_base from oslo_versionedobjects import fields as obj_fields from oslo_versionedobjects import fixture +from neutron.common import constants from neutron.common import exceptions as n_exc from neutron.common import utils as common_utils from neutron import context from neutron.db import models_v2 from neutron.objects import base +from neutron.objects import common_types from neutron.objects.db import api as obj_db_api from neutron.tests import base as test_base from neutron.tests import tools @@ -182,13 +185,18 @@ class FakeNeutronObjectCompositePrimaryKeyWithId(base.NeutronDbObject): synthetic_fields = ['obj_field'] +def get_random_dscp_mark(): + return random.choice(constants.VALID_DSCP_MARKS) + + FIELD_TYPE_VALUE_GENERATOR_MAP = { obj_fields.BooleanField: tools.get_random_boolean, obj_fields.IntegerField: tools.get_random_integer, obj_fields.StringField: tools.get_random_string, obj_fields.UUIDField: uuidutils.generate_uuid, obj_fields.ObjectField: lambda: None, - obj_fields.ListOfObjectsField: lambda: [] + obj_fields.ListOfObjectsField: lambda: [], + common_types.DscpMarkField: get_random_dscp_mark, } diff --git a/neutron/tests/unit/objects/test_common_types.py b/neutron/tests/unit/objects/test_common_types.py index a2b967edc69..45d5b8d06bd 100644 --- a/neutron/tests/unit/objects/test_common_types.py +++ b/neutron/tests/unit/objects/test_common_types.py @@ -11,6 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. +import abc + from neutron.common import constants from neutron.objects import common_types from neutron.tests import base as test_base @@ -40,9 +42,9 @@ class TestField(object): self.assertEqual(out_val, self.field.from_primitive( ObjectLikeThing, 'attr', prim_val)) + @abc.abstractmethod def test_stringify(self): - for in_val, out_val in self.coerce_good_values: - self.assertEqual("'%s'" % in_val, self.field.stringify(in_val)) + '''This test should validate stringify() format for new field types.''' def test_stringify_invalid(self): for in_val in self.coerce_bad_values: @@ -58,3 +60,22 @@ class IPV6ModeEnumFieldTest(test_base.BaseTestCase, TestField): self.coerce_bad_values = ['6', 4, 'type', 'slaacc'] self.to_primitive_values = self.coerce_good_values self.from_primitive_values = self.coerce_good_values + + def test_stringify(self): + for in_val, out_val in self.coerce_good_values: + self.assertEqual("'%s'" % in_val, self.field.stringify(in_val)) + + +class DscpMarkFieldTest(test_base.BaseTestCase, TestField): + def setUp(self): + super(DscpMarkFieldTest, self).setUp() + self.field = common_types.DscpMarkField() + self.coerce_good_values = [(val, val) + for val in constants.VALID_DSCP_MARKS] + self.coerce_bad_values = ['6', 'str', [], {}, object()] + self.to_primitive_values = self.coerce_good_values + self.from_primitive_values = self.coerce_good_values + + def test_stringify(self): + for in_val, out_val in self.coerce_good_values: + self.assertEqual("%s" % in_val, self.field.stringify(in_val)) diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 08257cf8f55..1eeea617abf 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -26,9 +26,10 @@ from neutron.tests import tools # NOTE: The hashes in this list should only be changed if they come with a # corresponding version bump in the affected objects. object_data = { - 'QosBandwidthLimitRule': '1.0-4e44a8f5c2895ab1278399f87b40a13d', - 'QosRuleType': '1.0-d0df298d49eeffab91af18d1a4cf7eaf', - 'QosPolicy': '1.0-721fa60ea8f0e8f15d456d6e917dfe59', + 'QosBandwidthLimitRule': '1.1-4e44a8f5c2895ab1278399f87b40a13d', + 'QosDscpMarkingRule': '1.1-0313c6554b34fd10c753cb63d638256c', + 'QosRuleType': '1.1-8a53fef4c6a43839d477a85b787d22ce', + 'QosPolicy': '1.1-721fa60ea8f0e8f15d456d6e917dfe59', } diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/extension_drivers/test_qos_driver.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/extension_drivers/test_qos_driver.py index 5e19c0f369a..f832bb61321 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/extension_drivers/test_qos_driver.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/extension_drivers/test_qos_driver.py @@ -16,8 +16,12 @@ from oslo_utils import uuidutils from neutron import context from neutron.objects.qos import policy from neutron.objects.qos import rule +from neutron.plugins.ml2.drivers.openvswitch.agent import ( + ovs_agent_extension_api as ovs_ext_api) from neutron.plugins.ml2.drivers.openvswitch.agent.extension_drivers import ( qos_driver) +from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.ovs_ofctl import ( + ovs_bridge) from neutron.tests.unit.plugins.ml2.drivers.openvswitch.agent import ( ovs_test_base) @@ -28,6 +32,10 @@ class QosOVSAgentDriverTestCase(ovs_test_base.OVSAgentConfigTestBase): super(QosOVSAgentDriverTestCase, self).setUp() self.context = context.get_admin_context() self.qos_driver = qos_driver.QosOVSAgentDriver() + self.agent_api = ovs_ext_api.OVSAgentExtensionAPI( + ovs_bridge.OVSAgentBridge('br-int'), + ovs_bridge.OVSAgentBridge('br-tun')) + self.qos_driver.consume_api(self.agent_api) self.qos_driver.initialize() self.qos_driver.br_int = mock.Mock() self.qos_driver.br_int.get_egress_bw_limit_for_port = mock.Mock( diff --git a/neutron/tests/unit/plugins/ml2/test_plugin.py b/neutron/tests/unit/plugins/ml2/test_plugin.py index 22c47d42d58..23d4f338bb5 100644 --- a/neutron/tests/unit/plugins/ml2/test_plugin.py +++ b/neutron/tests/unit/plugins/ml2/test_plugin.py @@ -172,9 +172,10 @@ class TestMl2SupportedQosRuleTypes(Ml2PluginV2TestCase): # make sure both plugins have the same supported qos rule types for mock_ in mocks: mock_.return_value = qos_consts.VALID_RULE_TYPES - self.assertEqual( - qos_consts.VALID_RULE_TYPES, - self.driver.mechanism_manager.supported_qos_rule_types) + for rule in qos_consts.VALID_RULE_TYPES: + self.assertIn( + rule, + self.driver.mechanism_manager.supported_qos_rule_types) @mock.patch.object(mech_test.TestMechanismDriver, 'supported_qos_rule_types', @@ -187,9 +188,10 @@ class TestMl2SupportedQosRuleTypes(Ml2PluginV2TestCase): return_value=False) def test_rule_types_with_driver_that_does_not_implement_binding(self, *mocks): - self.assertEqual( - qos_consts.VALID_RULE_TYPES, - self.driver.mechanism_manager.supported_qos_rule_types) + for rule in qos_consts.VALID_RULE_TYPES: + self.assertIn( + rule, + self.driver.mechanism_manager.supported_qos_rule_types) class TestMl2BasicGet(test_plugin.TestBasicGet, diff --git a/neutron/tests/unit/services/qos/test_qos_plugin.py b/neutron/tests/unit/services/qos/test_qos_plugin.py index 40c70c4cbb9..bc6d22fb07a 100644 --- a/neutron/tests/unit/services/qos/test_qos_plugin.py +++ b/neutron/tests/unit/services/qos/test_qos_plugin.py @@ -50,18 +50,19 @@ class TestQosPlugin(base.BaseQosTestCase): self.qos_plugin.notification_driver_manager = mock.Mock() self.ctxt = context.Context('fake_user', 'fake_tenant') - policy_id = uuidutils.generate_uuid() self.policy_data = { - 'policy': {'id': policy_id, + 'policy': {'id': uuidutils.generate_uuid(), 'tenant_id': uuidutils.generate_uuid(), 'name': 'test-policy', 'description': 'Test policy description', 'shared': True}} self.rule_data = { - 'bandwidth_limit_rule': {'id': policy_id, + 'bandwidth_limit_rule': {'id': uuidutils.generate_uuid(), 'max_kbps': 100, - 'max_burst_kbps': 150}} + 'max_burst_kbps': 150}, + 'dscp_marking_rule': {'id': uuidutils.generate_uuid(), + 'dscp_mark': 16}} self.policy = policy_object.QosPolicy( self.ctxt, **self.policy_data['policy']) @@ -69,6 +70,9 @@ class TestQosPlugin(base.BaseQosTestCase): self.rule = rule_object.QosBandwidthLimitRule( self.ctxt, **self.rule_data['bandwidth_limit_rule']) + self.dscp_rule = rule_object.QosDscpMarkingRule( + self.ctxt, **self.rule_data['dscp_marking_rule']) + def _validate_notif_driver_params(self, method_name): method = getattr(self.qos_plugin.notification_driver_manager, method_name) @@ -197,6 +201,77 @@ class TestQosPlugin(base.BaseQosTestCase): self.qos_plugin.get_policy_bandwidth_limit_rules, self.ctxt, self.policy.id) + def test_create_policy_dscp_marking_rule(self): + _policy = policy_object.QosPolicy( + self.ctxt, **self.policy_data['policy']) + with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', + return_value=_policy): + setattr(_policy, "rules", [self.dscp_rule]) + self.qos_plugin.create_policy_dscp_marking_rule( + self.ctxt, self.policy.id, self.rule_data) + self._validate_notif_driver_params('update_policy') + + def test_update_policy_dscp_marking_rule(self): + _policy = policy_object.QosPolicy( + self.ctxt, **self.policy_data['policy']) + with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', + return_value=_policy): + setattr(_policy, "rules", [self.dscp_rule]) + self.qos_plugin.update_policy_dscp_marking_rule( + self.ctxt, self.dscp_rule.id, self.policy.id, self.rule_data) + self._validate_notif_driver_params('update_policy') + + def test_delete_policy_dscp_marking_rule(self): + _policy = policy_object.QosPolicy( + self.ctxt, **self.policy_data['policy']) + with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', + return_value=_policy): + setattr(_policy, "rules", [self.dscp_rule]) + self.qos_plugin.delete_policy_dscp_marking_rule( + self.ctxt, self.dscp_rule.id, self.policy.id) + self._validate_notif_driver_params('update_policy') + + def test_get_policy_dscp_marking_rules(self): + with mock.patch('neutron.objects.qos.policy.QosPolicy.get_object', + return_value=self.policy): + with mock.patch('neutron.objects.qos.rule.' + 'QosDscpMarkingRule.' + 'get_objects') as get_object_mock: + self.qos_plugin.get_policy_dscp_marking_rules( + self.ctxt, self.policy.id) + get_object_mock.assert_called_once_with( + self.ctxt, qos_policy_id=self.policy.id) + + def test_get_policy_dscp_marking_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.' + 'QosDscpMarkingRule.' + 'get_objects') as get_object_mock: + + filters = {'filter': 'filter_id'} + self.qos_plugin.get_policy_dscp_marking_rules( + self.ctxt, self.policy.id, filters=filters) + get_object_mock.assert_called_once_with( + self.ctxt, qos_policy_id=self.policy.id, + filter='filter_id') + + def test_get_policy_dscp_marking_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_dscp_marking_rule, + self.ctxt, self.dscp_rule.id, self.policy.id) + + def test_get_policy_dscp_marking_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_dscp_marking_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/dscp-qos-77ea9b27d3762e48.yaml b/releasenotes/notes/dscp-qos-77ea9b27d3762e48.yaml new file mode 100644 index 00000000000..85664d9cbdc --- /dev/null +++ b/releasenotes/notes/dscp-qos-77ea9b27d3762e48.yaml @@ -0,0 +1,11 @@ +--- +prelude: > + A new rule has been added to the API that allows for tagging + traffic with DSCP values. This is currently supported by the + Open vSwitch QoS driver. +features: + - Neutron can apply a QoS rule to ports that mark outgoing + traffic's type of service packet header field. + - The Open vSwitch Neutron agent has been extended to mark the type of + service packet header field of packets egressing from the VM when the + QoS rule has been applied.