Quality of Service support via NSPs

QoS support for PTs inside PTG, as 2 new NSP param types.

The new NSP param types, qos_maxrate and qos_burstrate, map to the
Neutron QoS Policy resource, which then gets associated to a QoS
bandwith limit rule with a certain maximum rate and/or burst rate (in
Kbps) set.

Change-Id: I4a15daf5e0edd76d2d436eac6fdfb6b9f64992b2
This commit is contained in:
Igor Duarte Cardoso 2017-01-28 00:05:17 +00:00
parent 5fa26eb3a6
commit 6b2c15c3b8
23 changed files with 787 additions and 40 deletions

View File

@ -14,4 +14,7 @@ if [[ $ENABLE_APIC_AIM = True || $ENABLE_APIC_AIM_GATE = True ]]; then
ML2_L3_PLUGIN=${ML2_L3_PLUGIN:-apic_aim_l3}
Q_ML2_TENANT_NETWORK_TYPE=${Q_ML2_TENANT_NETWORK_TYPE:-opflex}
else
Q_SERVICE_PLUGIN_CLASSES=${Q_SERVICE_PLUGIN_CLASSES},qos
Q_ML2_PLUGIN_EXT_DRIVERS=${Q_ML2_PLUGIN_EXT_DRIVERS},qos
fi

View File

@ -24,6 +24,7 @@ function gbp_configure_neutron {
iniset $NEUTRON_CONF quotas quota_security_group_rule "-1"
iniset $NEUTRON_CONF quotas quota_router "-1"
iniset $NEUTRON_CONF quotas quota_floatingip "-1"
iniset $NEUTRON_CONF agent extensions "qos"
}
function nfp_configure_neutron {
@ -41,12 +42,12 @@ function nfp_configure_neutron {
fi
iniset $NEUTRON_CONF nfp_node_driver is_service_admin_owned "True"
iniset $NEUTRON_CONF nfp_node_driver svc_management_ptg_name "svc_management_ptg"
#extn_drivers=$(iniget $NEUTRON_ML2_CONF ml2 extension_drivers)
#if [[ -n $extn_drivers ]];then
# iniset $NEUTRON_ML2_CONF ml2 extension_drivers $extn_drivers,port_security
#else
# iniset $NEUTRON_ML2_CONF ml2 extension_drivers port_security
#fi
extn_drivers=$(iniget $NEUTRON_ML2_CONF ml2 extension_drivers)
if [[ -n $extn_drivers ]];then
iniset $NEUTRON_ML2_CONF ml2 extension_drivers $extn_drivers,port_security
else
iniset $NEUTRON_ML2_CONF ml2 extension_drivers port_security
fi
}
function configure_nfp_loadbalancer {

View File

@ -0,0 +1,55 @@
..
This work is licensed under a Creative Commons Attribution 3.0 Unported
License.
http://creativecommons.org/licenses/by/3.0/legalcode
Neutron QoS Support
===================
Quality of Service (QoS) support is available in GBP using the Neutron API
(via the GBP Neutron Resource Mapping Driver).
This feature can be used by creating Network Service Policies (NSP) with the
following NSP parameter types:
- `qos_maxrate`: maximum bandwidth limiting rate for a Policy Target (in Kbps)
- `qos_burstrate`: burst bandwidth limiting rate for a Policy Target (in Kbps)
When a NSP contains one or both of the previous parameter types, and is
associated to a Policy Target Group (PTG), all Policy Targets part of that PTG
will individually inherit the QoS definitions intended. In other words, each
Policy Target will have their bandwidth limited by the amounts specified in
`qos_maxrate` and `qos_burstrate`, independently of the network activity in
other Policy Targets of the same PTG.
The QoS NSP parameter types expect a numerical value bound by Neutron's QoS.
Resource Mapping Driver
-----------------------
When a NSP contains one or both of the supported parameter types, and a
respective numerical value, the Resource Mapping Driver will automatically be
called to create one QoS Policy resource and one QoS Bandwidth Limit Rule
resource, via Neutron's REST API.
The Resource Mapping Driver includes the NSP Manager as a mixin, which
provides the data model and methods for creating mappings from GBP to QoS
resources in Neutron, as explained in more detail below.
This driver also handles the association of each Policy Target (part of a PTG
having a NSP that includes QoS parameters) to the actual QoS Policies that
have been created in Neutron. The Neutron Ports already mapped to Policy
Targets will be updated to include the correct Neutron QoS Policy that was
previously created and mapped to GBP.
NSP Manager
-----------
A special resource exists in GBP, `ServicePolicyQosPolicyMapping`, which keeps
track of the mapping between NSPs in GBP and QoS Policies in Neutron, and is
specifically defined inside the NSP Manager file (`nsp_manager.py`).
Furthermore, `NetworkServicePolicyMappingMixin` includes the database
operations for creating, deleting and reading these mappings.
DevStack Support
----------------
The GBP DevStack plugin will automatically enable QoS support.

View File

@ -176,6 +176,18 @@ class LocalAPI(object):
raise exc.GroupPolicyDeploymentError()
return l3_plugin
@property
def _qos_plugin(self):
# Probably as well:
# REVISIT(rkukura): Need initialization method after all
# plugins are loaded to grab and store plugin.
plugins = manager.NeutronManager.get_service_plugins()
qos_plugin = plugins.get(pconst.QOS)
if not qos_plugin:
LOG.error(_LE("No QoS service plugin found."))
raise exc.GroupPolicyDeploymentError()
return qos_plugin
@property
def _group_policy_plugin(self):
# REVISIT(rkukura): Need initialization method after all
@ -231,6 +243,15 @@ class LocalAPI(object):
# method will be invoked in the API layer.
return obj
def _create_resource_qos(self, plugin, context, resource,
param, attrs):
action = 'create_' + resource
obj_creator = getattr(plugin, action)
if resource == "policy_bandwidth_limit_rule":
resource = resource[7:] # in the body, policy_ should be removed
obj = obj_creator(context, param, {resource: attrs})
return obj
def _update_resource(self, plugin, context, resource, resource_id, attrs,
do_notify=True):
# REVISIT(rkukura): Check authorization?
@ -246,6 +267,12 @@ class LocalAPI(object):
obj_deleter = getattr(plugin, action)
obj_deleter(context, resource_id)
def _delete_resource_qos(self, plugin, context, resource,
resource_id, second_id):
action = 'delete_' + resource
obj_deleter = getattr(plugin, action)
obj_deleter(context, resource_id, second_id)
def _get_resource(self, plugin, context, resource, resource_id):
obj_getter = getattr(plugin, 'get_' + resource)
obj = obj_getter(context, resource_id)
@ -517,6 +544,41 @@ class LocalAPI(object):
return self._get_resource(self._group_policy_plugin, plugin_context,
'l2_policy', l2p_id)
def _get_qos_policy(self, plugin_context, qos_policy_id):
return self._get_resource(self._qos_plugin, plugin_context,
'policy', qos_policy_id)
def _create_qos_policy(self, plugin_context, attrs):
return self._create_resource(self._qos_plugin, plugin_context,
'policy', attrs)
def _delete_qos_policy(self, plugin_context, qos_policy_id):
try:
self._delete_resource(self._qos_plugin,
plugin_context, 'policy', qos_policy_id)
except n_exc.QosPolicyNotFound:
LOG.warning(_LW('QoS Policy %s already deleted'), qos_policy_id)
def _get_qos_rules(self, plugin_context, filters=None):
filters = filters or {}
return self._get_resources(self._qos_plugin, plugin_context,
'policy_bandwidth_limit_rules', filters)
def _create_qos_rule(self, plugin_context, qos_policy_id, attrs):
return self._create_resource_qos(self._qos_plugin,
plugin_context,
'policy_bandwidth_limit_rule',
qos_policy_id, attrs)
def _delete_qos_rule(self, plugin_context, rule_id, qos_policy_id):
try:
self._delete_resource_qos(self._qos_plugin,
plugin_context,
'policy_bandwidth_limit_rule',
rule_id, qos_policy_id)
except n_exc.QosRuleNotFound:
LOG.warning(_LW('QoS Rule %s already deleted'), rule_id)
def _get_l2_policies(self, plugin_context, filters=None):
filters = filters or {}
return self._get_resources(self._group_policy_plugin, plugin_context,

View File

@ -1 +1 @@
c460c5682e74
da6a25bbcfa8

View File

@ -0,0 +1,48 @@
# 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.
#
"""initial_qos_via_nsp
Revision ID: da6a25bbcfa8
Revises: c460c5682e74
Create Date: 2017-03-27 00:00:00.000000
"""
# revision identifiers, used by Alembic.
revision = 'da6a25bbcfa8'
down_revision = 'c460c5682e74'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'gpm_qos_policy_mappings',
sa.Column('service_policy_id', sa.String(length=36), nullable=False),
sa.Column('qos_policy_id', sa.String(length=36), nullable=False),
sa.PrimaryKeyConstraint('service_policy_id'),
sa.ForeignKeyConstraint(['service_policy_id'],
['gp_network_service_policies.id'],
ondelete='CASCADE',
name='gbp_qos_policy_mapping_nsp_fk'),
sa.ForeignKeyConstraint(['qos_policy_id'],
['qos_policies.id'],
ondelete='RESTRICT',
name='gbp_qos_policy_mapping_qosp_fk')
)
def downgrade():
op.drop_table('gpm_qos_policy_mappings')

View File

@ -210,6 +210,8 @@ gp_supported_protocols = [None, nlib_const.PROTO_NAME_TCP,
gp_network_service_param_types = [
gp_constants.GP_NETWORK_SVC_PARAM_TYPE_IP_SINGLE,
gp_constants.GP_NETWORK_SVC_PARAM_TYPE_IP_POOL,
gp_constants.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX, # expects integer value
gp_constants.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST, # expects integer value
gp_constants.GP_NETWORK_SVC_PARAM_TYPE_STRING]
gp_network_service_param_keys = [
gp_constants.GP_NETWORK_SVC_PARAM_TYPE,
@ -320,7 +322,9 @@ def _validate_network_svc_params(data, key_specs=None):
d['type'])
LOG.debug(msg)
return msg
if d['type'] != gp_constants.GP_NETWORK_SVC_PARAM_TYPE_STRING:
if d['type'] not in (gp_constants.GP_NETWORK_SVC_PARAM_TYPE_STRING,
gp_constants.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX,
gp_constants.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST):
if d['value'] not in gp_network_service_param_values:
msg = _("Network service param value '%s' is not "
"supported") % d['value']

View File

@ -34,6 +34,8 @@ GP_NETWORK_SVC_PARAM_VALUE = 'value'
GP_NETWORK_SVC_PARAM_TYPE_IP_SINGLE = 'ip_single'
GP_NETWORK_SVC_PARAM_TYPE_IP_POOL = 'ip_pool'
GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX = 'qos_maxrate'
GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST = 'qos_burstrate'
GP_NETWORK_SVC_PARAM_TYPE_STRING = 'string'
GP_NETWORK_SVC_PARAM_VALUE_SELF_SUBNET = 'self_subnet'

View File

@ -234,9 +234,13 @@ class MultipleRedirectActionsNotSupportedForPRS(GroupPolicyBadRequest):
class InvalidNetworkServiceParameters(GroupPolicyBadRequest):
message = _("Resource Mapping Driver currently supports only one "
"parameter of type: ip_single and value: self_subnet and one "
"parameter of type ip_single or ip_pool and value nat_pool")
message = _("Resource Mapping Driver currently supports Network Service "
"Parameters with the following types and values: "
"type: 'ip_single' value: 'self_subnet'; "
"type: 'ip_pool' value: 'nat_pool'; "
"type: 'ip_single' value: 'nat_pool'; "
"type: 'qos_maxrate' value: numerical of intended Kbps; "
"type: 'qos_burstrate' value: numerical of intended Kbps.")
class ESSubnetRequiredForNatPool(GroupPolicyBadRequest):

View File

@ -60,6 +60,24 @@ class PolicyTargetFloatingIPMapping(model_base.BASEV2):
primary_key=True)
class ServicePolicyQosPolicyMapping(model_base.BASEV2):
"""Mapping of a NSP to a Neutron QoS Policy."""
__tablename__ = 'gpm_qos_policy_mappings'
service_policy_id = sa.Column(
sa.String(36),
sa.ForeignKey('gp_network_service_policies.id',
ondelete='CASCADE'),
nullable=False,
primary_key=True
)
qos_policy_id = sa.Column(
sa.String(36),
sa.ForeignKey('qos_policies.id',
ondelete='RESTRICT'),
nullable=False
)
class NetworkServicePolicyMappingMixin(object):
def _set_policy_ipaddress_mapping(self, session, service_policy_id,
@ -132,3 +150,20 @@ class NetworkServicePolicyMappingMixin(object):
policy_target_id=policy_target_id).all()
for fip_mapping in fip_mappings:
session.delete(fip_mapping)
def _get_nsp_qos_mapping(self, session, service_policy_id):
with session.begin(subtransactions=True):
return (session.query(ServicePolicyQosPolicyMapping).
filter_by(service_policy_id=service_policy_id).first())
def _set_nsp_qos_mapping(self, session, service_policy_id, qos_policy_id):
with session.begin(subtransactions=True):
mapping = ServicePolicyQosPolicyMapping(
service_policy_id=service_policy_id,
qos_policy_id=qos_policy_id)
session.add(mapping)
def _delete_nsp_qos_mapping(self, session, mapping):
if mapping:
with session.begin(subtransactions=True):
session.delete(mapping)

View File

@ -928,17 +928,37 @@ class ImplicitResourceOperations(local_api.LocalAPI,
def _validate_nsp_parameters(self, context):
nsp = context.current
nsp_params = nsp.get("network_service_params")
supported_nsp_pars = {"ip_single": ["self_subnet", "nat_pool"],
"ip_pool": "nat_pool"}
if (nsp_params and len(nsp_params) > 2 or len(nsp_params) == 2 and
nsp_params[0] == nsp_params[1]):
supported_static_nsp_pars = {
gconst.GP_NETWORK_SVC_PARAM_TYPE_IP_SINGLE: [
gconst.GP_NETWORK_SVC_PARAM_VALUE_SELF_SUBNET,
gconst.GP_NETWORK_SVC_PARAM_VALUE_NAT_POOL],
gconst.GP_NETWORK_SVC_PARAM_TYPE_IP_POOL: [
gconst.GP_NETWORK_SVC_PARAM_VALUE_NAT_POOL]}
# for params without a static value - later evaluation needed:
supported_flexible_nsp_params = (
gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST,
gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX)
# validate unique param types:
types_inside = set((d['type'] for d in nsp_params))
if len(types_inside) != len(nsp_params):
raise exc.InvalidNetworkServiceParameters()
for params in nsp_params:
type = params.get("type")
value = params.get("value")
if (type not in supported_nsp_pars or
value not in supported_nsp_pars[type]):
raise exc.InvalidNetworkServiceParameters()
type_ = params.get("type")
value_ = params.get("value")
if (type_ not in supported_flexible_nsp_params):
if (type_ not in supported_static_nsp_pars or
value_ not in supported_static_nsp_pars[type_]):
raise exc.InvalidNetworkServiceParameters()
else:
try:
if int(value_) < 0:
raise exc.InvalidNetworkServiceParameters()
except ValueError:
raise exc.InvalidNetworkServiceParameters()
def _validate_in_use_by_nsp(self, context):
# We do not allow ES update for L3p when it is used by NSP
@ -1338,6 +1358,88 @@ class ResourceMappingDriver(api.PolicyDriver, ImplicitResourceOperations,
if l3p['tenant_id'] != context.current['tenant_id']:
raise exc.CrossTenantL2PolicyL3PolicyNotSupported()
def _associate_qosp_to_pt(self, context):
ptg_id = context.current['policy_target_group_id']
ptg = context._plugin.get_policy_target_group(
context._plugin_context, ptg_id)
network_service_policy_id = ptg.get(
"network_service_policy_id")
if not network_service_policy_id:
return
nsp = context._plugin.get_network_service_policy(
context._plugin_context, network_service_policy_id)
nsp_params = nsp.get("network_service_params")
# Check if at least a QoS NSP p. is defined (a QoS policy was created)
for nsp_parameter in nsp_params:
if nsp_parameter["type"] in (
gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX,
gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST):
# get QoS Policy associated to NSP
mapping = self._get_nsp_qos_mapping(
context._plugin_context.session,
network_service_policy_id)
# apply QoS policy to PT's Neutron port
port_id = context.current['port_id']
port = {attributes.PORT:
{'qos_policy_id': mapping['qos_policy_id']}}
self._core_plugin.update_port(context._plugin_context,
port_id, port)
break
def _disassociate_qosp_from_pt(self, context, pt_id):
try:
policy_target = context._plugin.get_policy_target(
context._plugin_context, pt_id)
except gp_ext.PolicyTargetNotFound:
LOG.warning(_LW("Attempted to fetch deleted Service Target (QoS)"))
else:
port_id = policy_target['port_id']
port = {attributes.PORT: {'qos_policy_id': None}}
self._core_plugin.update_port(context._plugin_context,
port_id, port)
def _cleanup_network_service_policy(self, context, ptg,
ipaddress=None, fip_maps=None):
super(ResourceMappingDriver, self)._cleanup_network_service_policy(
context, ptg, ipaddress, fip_maps)
for pt in ptg['policy_targets']:
self._disassociate_qosp_from_pt(context, pt)
def _handle_network_service_policy(self, context):
network_service_policy_id = context.current.get(
"network_service_policy_id")
if not network_service_policy_id:
return
super(ResourceMappingDriver, self)._handle_network_service_policy(
context)
nsp = context._plugin.get_network_service_policy(
context._plugin_context, network_service_policy_id)
nsp_params = nsp.get("network_service_params")
for nsp_parameter in nsp_params:
if nsp_parameter["type"] in (
gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX,
gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST):
# get PTs/ports
policy_targets = context.current['policy_targets']
policy_targets = context._plugin.get_policy_targets(
context._plugin_context, filters={'id': policy_targets})
# get QoS Policy associated to NSP
mapping = self._get_nsp_qos_mapping(
context._plugin_context.session,
nsp['id'])
# apply QoS policy to each PT's Neutron port
for pt in policy_targets:
port_id = pt['port_id']
port = {attributes.PORT:
{'qos_policy_id': mapping['qos_policy_id']}}
self._core_plugin.update_port(context._plugin_context,
port_id, port)
@property
def resource_owner_tenant_id(self):
if not self._resource_owner_tenant_id:
@ -1404,6 +1506,7 @@ class ResourceMappingDriver(api.PolicyDriver, ImplicitResourceOperations,
self._assoc_ptg_sg_to_pt(context, context.current['id'],
context.current['policy_target_group_id'])
self._associate_fip_to_pt(context)
self._associate_qosp_to_pt(context)
if context.current.get('proxy_gateway'):
self._set_proxy_gateway_routes(context, context.current)
@ -1890,6 +1993,48 @@ class ResourceMappingDriver(api.PolicyDriver, ImplicitResourceOperations,
for sg in sg_list:
self._delete_sg(context._plugin_context, sg)
@log.log_method_call
def create_network_service_policy_precommit(self, context):
self._validate_nsp_parameters(context)
@log.log_method_call
def create_network_service_policy_postcommit(self, context):
p = context.current['network_service_params']
max = burst = 0
setting_qos = False
# assumes single value per parameter type, as the API currently states
params = {p[n]['type']: p[n]['value'] for n in range(len(p))}
# check for QoS param types..
if gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX in params:
max = params[gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX]
setting_qos = True
if gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST in params:
burst = params[gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST]
setting_qos = True
# ..and create needed Neutron resources
if setting_qos:
qos_policy_id = self._create_implicit_qos_policy(context)
nsp_id = context.current['id']
self._create_implicit_qos_rule(context, qos_policy_id, max, burst)
self._set_nsp_qos_mapping(context._plugin_context.session,
nsp_id,
qos_policy_id)
@log.log_method_call
def delete_network_service_policy_precommit(self, context):
nsp = context.current
mapping = self._get_nsp_qos_mapping(context._plugin_context.session,
nsp['id'])
if mapping:
qos_policy_id = mapping['qos_policy_id']
context.current['qos_policy_id'] = qos_policy_id
@log.log_method_call
def delete_network_service_policy_postcommit(self, context):
qos_policy_id = context.current['qos_policy_id']
if qos_policy_id:
self._delete_ptg_qos_policy(context, qos_policy_id)
def create_external_segment_precommit(self, context):
if context.current['subnet_id']:
subnet = self._get_subnet(context._plugin_context,
@ -2896,3 +3041,29 @@ class ResourceMappingDriver(api.PolicyDriver, ImplicitResourceOperations,
master_mac = master_port['mac_address']
master_ips = [x['ip_address'] for x in master_port['fixed_ips']]
return master_mac, master_ips
def _create_implicit_qos_policy(self, context):
attrs = {
'name': 'gbp_' + context.current['name'],
'description': 'Group-Based Policy QoS policy',
'tenant_id': context.current['tenant_id']}
qos_policy = self._create_qos_policy(context._plugin_context, attrs)
qos_policy_id = qos_policy['id']
return qos_policy_id
def _delete_ptg_qos_policy(self, context, qos_policy_id):
qos_rules = self._get_qos_rules(context._plugin_context, qos_policy_id)
with context._plugin_context.session.begin(subtransactions=True):
for qos_rule in qos_rules:
self._delete_qos_rule(context._plugin_context,
qos_rule['id'], qos_policy_id)
self._delete_qos_policy(context._plugin_context, qos_policy_id)
def _create_implicit_qos_rule(self, context, qos_policy_id, max, burst):
attrs = {
'max_kbps': max,
'max_burst_kbps': burst}
qos_rule = self._create_qos_rule(context._plugin_context,
qos_policy_id, attrs)
qos_rule_id = qos_rule['id']
return qos_rule_id

View File

@ -18,6 +18,7 @@ import webob.exc
import mock
from neutron.api import extensions
from neutron.api.rpc.callbacks.producer import registry
from neutron import context
from neutron.db import api as db_api
from neutron import manager
@ -351,6 +352,8 @@ class GroupPolicyDbTestCase(GroupPolicyDBTestBase,
extensions.append_api_extensions_path(
gbpservice.neutron.extensions.__path__)
service_plugins['flavors_plugin_name'] =\
'neutron.services.flavors.flavors_plugin.FlavorsPlugin'
super(GroupPolicyDbTestCase, self).setUp(
plugin=core_plugin, ext_mgr=ext_mgr,
service_plugins=service_plugins
@ -373,6 +376,7 @@ class GroupPolicyDbTestCase(GroupPolicyDBTestBase,
def tearDown(self):
self._unset_notification_mocks()
registry.clear()
super(GroupPolicyDbTestCase, self).tearDown()

View File

@ -43,7 +43,7 @@ class GroupPolicyMappingDbTestCase(tgpdb.GroupPolicyDbTestCase,
test_l3.L3NatTestCaseMixin):
def setUp(self, core_plugin=None, l3_plugin=None, gp_plugin=None,
service_plugins=None, sc_plugin=None):
service_plugins=None, sc_plugin=None, qos_plugin=None):
if not gp_plugin:
gp_plugin = DB_GP_PLUGIN_KLASS
if not service_plugins:
@ -53,6 +53,8 @@ class GroupPolicyMappingDbTestCase(tgpdb.GroupPolicyDbTestCase,
'flavors_plugin.FlavorsPlugin',
'servicechain_plugin': sc_plugin or SC_PLUGIN_KLASS}
service_plugins['l3_plugin_name'] = l3_plugin or "router"
if qos_plugin:
service_plugins['qos_plugin_name'] = qos_plugin
super(GroupPolicyMappingDbTestCase, self).setUp(
core_plugin=core_plugin, gp_plugin=gp_plugin,
service_plugins=service_plugins

View File

@ -95,7 +95,7 @@ class AIMBaseTestCase(test_nr_base.CommonNeutronBaseTestCase,
_extension_path = None
def setUp(self, policy_drivers=None, core_plugin=None, ml2_options=None,
l3_plugin=None, sc_plugin=None, **kwargs):
l3_plugin=None, sc_plugin=None, qos_plugin=None, **kwargs):
core_plugin = core_plugin or ML2PLUS_PLUGIN
if not l3_plugin:
l3_plugin = "apic_aim_l3"
@ -123,7 +123,7 @@ class AIMBaseTestCase(test_nr_base.CommonNeutronBaseTestCase,
super(AIMBaseTestCase, self).setUp(
policy_drivers=policy_drivers, core_plugin=core_plugin,
ml2_options=ml2_opts, l3_plugin=l3_plugin,
sc_plugin=sc_plugin)
sc_plugin=sc_plugin, qos_plugin=qos_plugin)
aim_model_base.Base.metadata.create_all(self.engine)
self.db_session = db_api.get_session()
self.initialize_db_config(self.db_session)

View File

@ -101,7 +101,7 @@ class ApicMappingTestCase(
def setUp(self, sc_plugin=None, nat_enabled=True,
pre_existing_l3out=False, default_agent_conf=True,
ml2_options=None, single_tenant_mode=False):
ml2_options=None, single_tenant_mode=False, qos_plugin=None):
self.saved_apicapi = sys.modules["apicapi"]
sys.modules["apicapi"] = mock.Mock()
if default_agent_conf:
@ -142,7 +142,7 @@ class ApicMappingTestCase(
nova_client.return_value = vm
super(ApicMappingTestCase, self).setUp(
policy_drivers=['implicit_policy', 'apic', 'chain_mapping'],
ml2_options=ml2_opts, sc_plugin=sc_plugin)
ml2_options=ml2_opts, sc_plugin=sc_plugin, qos_plugin=qos_plugin)
engine = db_api.get_engine()
model_base.BASEV2.metadata.create_all(engine)
plugin = manager.NeutronManager.get_plugin()

View File

@ -33,7 +33,8 @@ class ExtensionDriverTestBase(test_plugin.GroupPolicyPluginTestCase):
_extension_path = os.path.dirname(os.path.abspath(test_ext.__file__))
def setUp(self, policy_drivers=None, core_plugin=None,
l3_plugin=None, ml2_options=None, sc_plugin=None):
l3_plugin=None, ml2_options=None,
sc_plugin=None, qos_plugin=None):
config.cfg.CONF.set_override('extension_drivers',
self._extension_drivers,
group='group_policy')
@ -42,7 +43,8 @@ class ExtensionDriverTestBase(test_plugin.GroupPolicyPluginTestCase):
'api_extensions_path', self._extension_path)
super(ExtensionDriverTestBase, self).setUp(
core_plugin=core_plugin, l3_plugin=l3_plugin,
ml2_options=ml2_options, sc_plugin=sc_plugin)
ml2_options=ml2_options, sc_plugin=sc_plugin,
qos_plugin=qos_plugin)
class ExtensionDriverTestCase(ExtensionDriverTestBase):

View File

@ -60,7 +60,7 @@ def get_status_for_test(self, context):
class GroupPolicyPluginTestBase(tgpmdb.GroupPolicyMappingDbTestCase):
def setUp(self, core_plugin=None, l3_plugin=None, gp_plugin=None,
ml2_options=None, sc_plugin=None):
ml2_options=None, sc_plugin=None, qos_plugin=None):
if not gp_plugin:
gp_plugin = GP_PLUGIN_KLASS
ml2_opts = ml2_options or {'mechanism_drivers': ['openvswitch'],
@ -71,7 +71,8 @@ class GroupPolicyPluginTestBase(tgpmdb.GroupPolicyMappingDbTestCase):
super(GroupPolicyPluginTestBase, self).setUp(core_plugin=core_plugin,
l3_plugin=l3_plugin,
gp_plugin=gp_plugin,
sc_plugin=sc_plugin)
sc_plugin=sc_plugin,
qos_plugin=qos_plugin)
def _create_l2_policy_on_shared(self, **kwargs):
l3p = self.create_l3_policy(shared=True)['l3_policy']

View File

@ -34,7 +34,7 @@ CORE_PLUGIN = ('gbpservice.neutron.tests.unit.services.grouppolicy.'
class CommonNeutronBaseTestCase(test_plugin.GroupPolicyPluginTestBase):
def setUp(self, policy_drivers=None, core_plugin=None, l3_plugin=None,
ml2_options=None, sc_plugin=None):
ml2_options=None, sc_plugin=None, qos_plugin=None):
core_plugin = core_plugin or ML2PLUS_PLUGIN
policy_drivers = policy_drivers or ['neutron_resources']
config.cfg.CONF.set_override('policy_drivers',
@ -46,7 +46,8 @@ class CommonNeutronBaseTestCase(test_plugin.GroupPolicyPluginTestBase):
super(CommonNeutronBaseTestCase, self).setUp(core_plugin=core_plugin,
l3_plugin=l3_plugin,
ml2_options=ml2_options,
sc_plugin=sc_plugin)
sc_plugin=sc_plugin,
qos_plugin=qos_plugin)
engine = db_api.get_engine()
model_base.BASEV2.metadata.create_all(engine)
res = mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.'

View File

@ -20,6 +20,7 @@ import mock
import netaddr
from neutron import context as nctx
from neutron.db import api as db_api
from neutron.db.qos import models as qos_models
from neutron.extensions import external_net as external_net
from neutron.extensions import securitygroup as ext_sg
from neutron import manager
@ -70,8 +71,9 @@ CORE_PLUGIN = ('gbpservice.neutron.tests.unit.services.grouppolicy.'
class ResourceMappingTestCase(test_plugin.GroupPolicyPluginTestCase):
def setUp(self, policy_drivers=None,
core_plugin=n_test_plugin.PLUGIN_NAME, ml2_options=None,
sc_plugin=None):
core_plugin=n_test_plugin.PLUGIN_NAME,
ml2_options=None,
sc_plugin=None, qos_plugin=None):
policy_drivers = policy_drivers or ['implicit_policy',
'resource_mapping',
'chain_mapping']
@ -81,9 +83,16 @@ class ResourceMappingTestCase(test_plugin.GroupPolicyPluginTestCase):
sc_cfg.cfg.CONF.set_override('servicechain_drivers',
['dummy'], group='servicechain')
config.cfg.CONF.set_override('allow_overlapping_ips', True)
ml2_opts = ml2_options or {
'mechanism_drivers': ['openvswitch'],
'extension_drivers': ['qos', 'port_security']
}
super(ResourceMappingTestCase, self).setUp(core_plugin=core_plugin,
ml2_options=ml2_options,
sc_plugin=sc_plugin)
ml2_options=ml2_opts,
sc_plugin=sc_plugin,
qos_plugin=qos_plugin)
engine = db_api.get_engine()
model_base.BASEV2.metadata.create_all(engine)
res = mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.'
@ -184,6 +193,25 @@ class ResourceMappingTestCase(test_plugin.GroupPolicyPluginTestCase):
return (resource_mapping.ResourceMappingDriver.
_get_policy_rule_set_sg_mapping(ctx.session, prs_id))
def _get_nsp_qosp_mapping(self, nsp_id):
ctx = nctx.get_admin_context()
with ctx.session.begin(subtransactions=True):
return (ctx.session.query(
nsp_manager.ServicePolicyQosPolicyMapping).
filter_by(service_policy_id=nsp_id).first())
def _get_qos_policy(self, qos_policy_id):
ctx = nctx.get_admin_context()
with ctx.session.begin(subtransactions=True):
return (ctx.session.query(qos_models.QosPolicy).
filter_by(id=qos_policy_id).first())
def _get_qos_rules(self, qos_policy_id):
ctx = nctx.get_admin_context()
with ctx.session.begin(subtransactions=True):
return (ctx.session.query(qos_models.QosBandwidthLimitRule).
filter_by(qos_policy_id=qos_policy_id).all())
def _create_ssh_allow_rule(self):
return self._create_tcp_allow_rule('22')
@ -3839,6 +3867,10 @@ class TestPolicyRule(ResourceMappingTestCase):
class TestNetworkServicePolicy(ResourceMappingTestCase):
def setUp(self):
qos_plugin = 'qos'
super(TestNetworkServicePolicy, self).setUp(qos_plugin=qos_plugin)
def test_create_nsp_multiple_ptgs(self):
nsp = self.create_network_service_policy(
network_service_params=[
@ -3880,6 +3912,32 @@ class TestNetworkServicePolicy(ResourceMappingTestCase):
{"type": "ip_single", "value": "self_subnet", "name": "vip"},
{"type": "ip_single", "value": "self_subnet", "name": "vip"}],
expected_res_status=webob.exc.HTTPBadRequest.code)
self.create_network_service_policy(
network_service_params=[
{"type": gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX,
"value": "abcd", "name": "qos"}],
expected_res_status=webob.exc.HTTPBadRequest.code)
self.create_network_service_policy(
network_service_params=[
{"type": gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST,
"value": "efgh", "name": "qos"}],
expected_res_status=webob.exc.HTTPBadRequest.code)
self.create_network_service_policy(
network_service_params=[
{"type": gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX,
"value": "1000", "name": "qos"},
{"type": gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX,
"value": "2000", "name": "qos"}
],
expected_res_status=webob.exc.HTTPBadRequest.code)
self.create_network_service_policy(
network_service_params=[
{"type": gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST,
"value": "1000", "name": "qos"},
{"type": gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST,
"value": "2000", "name": "qos"}
],
expected_res_status=webob.exc.HTTPBadRequest.code)
def test_nsp_cleanup_on_unset(self):
ptg = self.create_policy_target_group(
@ -4290,6 +4348,161 @@ class TestNetworkServicePolicy(ResourceMappingTestCase):
self._verify_update_ptg_with_nsp(ptg['id'], nsp2['id'], subnet)
self._verify_update_ptg_with_nsp(ptg['id'], nsp['id'], subnet)
def test_nsp_creation_for_qos_both_rates(self):
nsp = self._create_nsp_with_qos_both_rates()
qos_rules = self._create_nsp(nsp)
self._verify_qos_rule(qos_rules, 1337, 2674)
def test_nsp_creation_for_qos_maxrate(self):
nsp = self._create_nsp_with_qos_maxrate()
qos_rules = self._create_nsp(nsp)
self._verify_qos_rule(qos_rules, 1337)
def test_nsp_deletion_for_qos_maxrate(self):
nsp = self._create_nsp_with_qos_maxrate()
# before deleting anything, get all needed IDs
mapping = self._get_nsp_qosp_mapping(nsp['id'])
qos_policy_id = mapping['qos_policy_id']
self.delete_network_service_policy(
nsp['id'],
expected_res_status=webob.exc.HTTPNoContent.code)
self.show_network_service_policy(
nsp['id'],
expected_res_status=webob.exc.HTTPNotFound.code)
mapping = self._get_nsp_qosp_mapping(nsp['id'])
qosp = self._get_qos_policy(qos_policy_id)
qos_rules = self._get_qos_rules(qos_policy_id)
# verify everything is gone
self.assertIsNone(mapping)
self.assertIsNone(qosp)
self.assertEqual([], qos_rules)
def test_create_ptg_with_ports_existing_nsp_qos(self):
nsp = self._create_nsp_with_qos_both_rates()
mapping = self._get_nsp_qosp_mapping(nsp['id'])
ptg = self.create_policy_target_group(
name="ptg1",
network_service_policy_id=nsp['id'],
expected_res_status=webob.exc.HTTPCreated.code
)['policy_target_group']
pt1 = self.create_policy_target(
policy_target_group_id=ptg['id'])['policy_target']
pt2 = self.create_policy_target(
policy_target_group_id=ptg['id'])['policy_target']
port1 = self._get_object('ports', pt1['port_id'],
self.api)['port']
port2 = self._get_object('ports', pt2['port_id'],
self.api)['port']
# verify that the respective ports acquired the expected QoS Policy
self.assertEqual(mapping['qos_policy_id'], port1['qos_policy_id'])
self.assertEqual(mapping['qos_policy_id'], port2['qos_policy_id'])
def test_update_ptg_with_ports_add_nsp(self):
nsp = self._create_nsp_with_qos_both_rates()
mapping = self._get_nsp_qosp_mapping(nsp['id'])
ptg = self.create_policy_target_group(
name="ptg1")['policy_target_group']
pt1 = self.create_policy_target(
policy_target_group_id=ptg['id'])['policy_target']
pt2 = self.create_policy_target(
policy_target_group_id=ptg['id'])['policy_target']
self.update_policy_target_group(
ptg['id'],
network_service_policy_id = nsp['id'],
expected_res_status = webob.exc.HTTPOk.code)
port1 = self._get_object('ports', pt1['port_id'],
self.api)['port']
port2 = self._get_object('ports', pt2['port_id'],
self.api)['port']
# verify that the respective ports acquired the expected QoS Policy
self.assertEqual(mapping['qos_policy_id'], port1['qos_policy_id'])
self.assertEqual(mapping['qos_policy_id'], port2['qos_policy_id'])
def test_update_ptg_with_ports_remove_nsp(self):
nsp = self._create_nsp_with_qos_both_rates()
ptg = self.create_policy_target_group(
name="ptg1",
network_service_policy_id=nsp['id'],
expected_res_status=webob.exc.HTTPCreated.code
)['policy_target_group']
pt1 = self.create_policy_target(
policy_target_group_id=ptg['id'])['policy_target']
pt2 = self.create_policy_target(
policy_target_group_id=ptg['id'])['policy_target']
self.update_policy_target_group(
ptg['id'],
network_service_policy_id=None,
expected_res_status=webob.exc.HTTPOk.code)
port1 = self._get_object('ports', pt1['port_id'],
self.api)['port']
port2 = self._get_object('ports', pt2['port_id'],
self.api)['port']
# verify that the respective ports don't have a QoS Policy assigned
self.assertIsNone(port1['qos_policy_id'])
self.assertIsNone(port2['qos_policy_id'])
def _create_nsp(self, nsp):
# check if mapping was successfully created
mapping = self._get_nsp_qosp_mapping(nsp['id'])
qos_policy_id = mapping['qos_policy_id']
# check if qos policy is correctly created
qosp = self._get_qos_policy(qos_policy_id)
self._verify_qos_policy(qosp)
# check if respective qos rule is correctly created
qos_rules = self._get_qos_rules(qos_policy_id)
return qos_rules
def _create_nsp_with_qos_maxrate(self):
return self.create_network_service_policy(
name="testname",
network_service_params=[
{"type": gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX,
"value": "1337",
"name": "maxrate_only"}],
expected_res_status=webob.exc.HTTPCreated.code)[
'network_service_policy']
def _create_nsp_with_qos_both_rates(self):
return self.create_network_service_policy(
name="testname",
network_service_params=[
{"type": gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_BURST,
"value": "2674",
"name": "burstrate"},
{"type": gconst.GP_NETWORK_SVC_PARAM_TYPE_QOS_MAX,
"value": "1337",
"name": "maxrate"}],
expected_res_status=webob.exc.HTTPCreated.code)[
'network_service_policy']
def _verify_qos_policy(self, qos_policy):
self.assertEqual("gbp_testname", qos_policy['name'])
def _verify_qos_rule(self, qos_rules, max, burst=0):
self.assertEqual(1, len(qos_rules))
single_qos_rule = qos_rules[0]
self.assertEqual(1337, single_qos_rule['max_kbps'])
if burst:
self.assertEqual(2674, single_qos_rule['max_burst_kbps'])
def _verify_update_ptg_with_nsp(self, ptg_id, nsp_id, ptg_subnet_no_nsp):
ptg_subnet_id = ptg_subnet_no_nsp['id']
initial_allocation_pool = ptg_subnet_no_nsp['allocation_pools']

View File

@ -51,12 +51,15 @@ class ResourceMappingStitchingPlumberGBPTestCase(
'extension_drivers', ['proxy_group'], group='group_policy')
cfg.CONF.set_override('node_plumber', 'stitching_plumber',
group='node_composition_plugin')
ml2_opts = {'mechanism_drivers': ['stitching_gbp']}
ml2_opts = {'mechanism_drivers': ['stitching_gbp'],
'extension_drivers': ['qos']}
host_agents = mock.patch('neutron.plugins.ml2.driver_context.'
'PortContext.host_agents').start()
host_agents.return_value = [self.agent_conf]
qos_plugin = 'qos'
super(ResourceMappingStitchingPlumberGBPTestCase, self).setUp(
sc_plugin=base.SC_PLUGIN_KLASS, ml2_options=ml2_opts)
sc_plugin=base.SC_PLUGIN_KLASS, ml2_options=ml2_opts,
qos_plugin=qos_plugin)
def get_plumbing_info(context):
return info_mapping.get(context.current_profile['service_type'])

View File

@ -0,0 +1,126 @@
#!/usr/bin/env bash
# **gbp.sh**
# Sanity check that gbp started if enabled
echo "*********************************************************************"
echo "Begin DevStack Exercise: $0"
echo "*********************************************************************"
# Settings
# ========
# This script exits on an error so that errors don't compound and you see
# only the first error that occurred.
set -o errexit
# Keep track of the current directory
EXERCISE_DIR=$(cd $(dirname "$0") && pwd)
TOP_DIR=$(cd $EXERCISE_DIR/..; pwd)
# Import common functions
source $TOP_DIR/functions
# Import configuration
source $TOP_DIR/openrc
# Import exercise configuration
source $TOP_DIR/exerciserc
source $TOP_DIR/openrc demo demo
# Print the commands being run so that we can see the command that triggers
# an error. It is also useful for following allowing as the install occurs.
set -o xtrace
function confirm_server_active {
local VM_UUID=$1
if ! timeout $ACTIVE_TIMEOUT sh -c "while ! nova show $VM_UUID | grep status | grep -q ACTIVE; do sleep 1; done"; then
echo "server '$VM_UUID' did not become active!"
false
fi
}
# Create allow action that can used in several rules
gbp policy-action-create allow --action-type allow
# Create ICMP rule
gbp policy-classifier-create icmp-traffic --protocol icmp --direction bi
gbp policy-rule-create ping-policy-rule --classifier icmp-traffic --actions allow
# ICMP policy-rule-set
gbp policy-rule-set-create icmp-policy-rule-set --policy-rules ping-policy-rule
# ====== PROJECT OPERATION ======
# PTGs creation
gbp group-create limited
gbp group-create unlimited
# PT creation
PORT1=$(gbp policy-target-create port1-pt --policy-target-group limited | awk "/port_id/ {print \$4}")
PORT2=$(gbp policy-target-create port2-pt --policy-target-group limited | awk "/port_id/ {print \$4}")
PORT3=$(gbp policy-target-create port3-pt --policy-target-group unlimited | awk "/port_id/ {print \$4}")
PORT4=$(gbp policy-target-create port4-pt --policy-target-group unlimited | awk "/port_id/ {print \$4}")
PORT1_VM_UUID=`nova boot --flavor m1.tiny --image $DEFAULT_IMAGE_NAME --nic port-id=$PORT1 port1-vm | grep ' id ' | cut -d"|" -f3 | sed 's/ //g'`
die_if_not_set $LINENO PORT1_VM_UUID "Failure launching port1-vm"
confirm_server_active $PORT1_VM_UUID
PORT2_VM_UUID=`nova boot --flavor m1.tiny --image $DEFAULT_IMAGE_NAME --nic port-id=$PORT2 port2-vm | grep ' id ' | cut -d"|" -f3 | sed 's/ //g'`
die_if_not_set $LINENO PORT2_VM_UUID "Failure launching port2-vm"
confirm_server_active $PORT2_VM_UUID
PORT3_VM_UUID=`nova boot --flavor m1.tiny --image $DEFAULT_IMAGE_NAME --nic port-id=$PORT3 port3-vm | grep ' id ' | cut -d"|" -f3 | sed 's/ //g'`
die_if_not_set $LINENO PORT3_VM_UUID "Failure launching port3-vm"
confirm_server_active $PORT3_VM_UUID
PORT4_VM_UUID=`nova boot --flavor m1.tiny --image $DEFAULT_IMAGE_NAME --nic port-id=$PORT4 port4-vm | grep ' id ' | cut -d"|" -f3 | sed 's/ //g'`
die_if_not_set $LINENO PORT4_VM_UUID "Failure launching port4-vm"
confirm_server_active $PORT4_VM_UUID
####CHECKPOINT: No traffic flows between groups and no QoS applied
# policy-rule-set Association
gbp group-update limited --consumed-policy-rule-sets "icmp-policy-rule-set"
gbp group-update unlimited --provided-policy-rule-sets "icmp-policy-rule-set"
####CHECKPOINT: ICMP now flows between each group, but still no QoS applied
# Create Network Service Policy that includes QoS parameters
gbp network-service-policy-create --network-service-params type=qos_burstrate,name=qos_burstrate,value=500 --network-service-params type=qos_maxrate,name=qos_maxrate,value=8000 "qos"
# Limit every PT in the limited PTG by associating the "qos" NSP created right before
gbp group-update limited --network-service-policy "qos"
####CHECKPOINT: Both port1-pt and port2-pt will not be able to exceed 8 Mbps with a burst rate of 500 Kb
nova delete port4-vm
nova delete port3-vm
nova delete port2-vm
nova delete port1-vm
if ! timeout $TERMINATE_TIMEOUT sh -c "while nova list | grep -q ACTIVE; do sleep 1; done"; then
die $LINENO "Some VMs failed to shutdown"
fi
gbp policy-target-delete port4-pt
gbp policy-target-delete port3-pt
gbp policy-target-delete port2-pt
gbp policy-target-delete port1-pt
gbp group-delete unlimited
gbp group-delete limited
gbp policy-rule-set-delete icmp-policy-rule-set
gbp policy-rule-delete ping-policy-rule
gbp policy-classifier-delete icmp-traffic
gbp policy-action-delete allow
set +o xtrace
echo "*********************************************************************"
echo "SUCCESS: End DevStack Exercise: $0"
echo "*********************************************************************"

View File

@ -6,7 +6,7 @@ RABBIT_PASSWORD=abc123
SERVICE_PASSWORD=$ADMIN_PASSWORD
SERVICE_TOKEN=abc123
Q_SERVICE_PLUGIN_CLASSES=neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,group_policy,ncp
Q_SERVICE_PLUGIN_CLASSES=neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,group_policy,ncp,qos
# Using group-policy branches
@ -125,6 +125,9 @@ quota_security_group_rule = -1
quota_router = -1
quota_floatingip = -1
[agent]
extensions = qos
[[post-config|/etc/neutron/plugins/ml2/ml2_conf.ini]]
[ml2]
extension_drivers = port_security
extension_drivers = qos,port_security

View File

@ -6,7 +6,7 @@ RABBIT_PASSWORD=abc123
SERVICE_PASSWORD=$ADMIN_PASSWORD
SERVICE_TOKEN=abc123
Q_SERVICE_PLUGIN_CLASSES=neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,group_policy,ncp
Q_SERVICE_PLUGIN_CLASSES=neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,group_policy,ncp,qos
# Using group-policy branches
@ -97,3 +97,10 @@ quota_security_group = -1
quota_security_group_rule = -1
quota_router = -1
quota_floatingip = -1
[agent]
extensions = qos
[[post-config|/etc/neutron/plugins/ml2/ml2_conf.ini]]
[ml2]
extension_drivers = qos