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 <nate_johnston@cable.comcast.com> Co-Authored-By: Victor Howard <victor.r.howard@gmail.com> Co-Authored-By: Margaret Frances <margaret_frances@cable.comcast.com> Co-Authored-By: James Reeves <james.reeves5546@gmail.com> Co-Authored-By: John Schwarz <jschwarz@redhat.com> Needed-By: I25ad60c1b9a66e568276a772b8c496987d9f8299 Needed-By: I881b8f5bc9024c20275bc56062de72a1c70c8321 Needed-By: I48ead4b459183db795337ab729830a1b3c0022da Needed-By: Ib92b172dce48276b90ec75ee5880ddd69040d7c8 Needed-By: I4eb21495e84feea46880caf3360759263e1e8f95 Needed-By: I0ab6a1a0d1430c5791fea1d5b54106c6cc93b937 Partial-Bug: #1468353 Change-Id: Ic3baefe176df05f049a2e06529c58fd65fe6b419
This commit is contained in:
parent
1e3f3fb266
commit
a9a1943fde
@ -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
|
||||
++++++
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
@ -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),
|
||||
|
@ -1 +1 @@
|
||||
0e66c5227a8a
|
||||
45f8dd33480b
|
||||
|
@ -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()))
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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'])
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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',
|
||||
}
|
||||
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
11
releasenotes/notes/dscp-qos-77ea9b27d3762e48.yaml
Normal file
11
releasenotes/notes/dscp-qos-77ea9b27d3762e48.yaml
Normal file
@ -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.
|
Loading…
Reference in New Issue
Block a user