DSCP packet marking support in Linuxbridge agent

Linuxbridge agent uses iptable rules in POSTROUTING chain
in the mangle table to mark outgoing packets with the
DSCP mark value configured by the user in QoS policy.

DocImpact: DSCP Marking rule support is extended to the
           Linuxbridge L2 agent

Closes-Bug: #1644369

Change-Id: I47e44cb2e67ab73bd5ee0aa4cca47cb3d07e43f3
This commit is contained in:
Sławek Kapłoński 2016-11-23 22:14:30 +00:00
parent b9d0a5b885
commit fd3bf3327c
7 changed files with 286 additions and 55 deletions

View File

@ -18,7 +18,9 @@ from oslo_log import log
from neutron._i18n import _LI
from neutron.agent.l2.extensions import qos
from neutron.agent.linux import iptables_manager
from neutron.agent.linux import tc_lib
import neutron.common.constants as const
from neutron.plugins.ml2.drivers.linuxbridge.mech_driver import (
mech_linuxbridge)
@ -31,8 +33,28 @@ class QosLinuxbridgeAgentDriver(qos.QosAgentDriver):
mech_linuxbridge.LinuxbridgeMechanismDriver.supported_qos_rule_types
)
IPTABLES_DIRECTION = {const.INGRESS_DIRECTION: 'physdev-out',
const.EGRESS_DIRECTION: 'physdev-in'}
IPTABLES_DIRECTION_PREFIX = {const.INGRESS_DIRECTION: "i",
const.EGRESS_DIRECTION: "o"}
def initialize(self):
LOG.info(_LI("Initializing Linux bridge QoS extension"))
self.iptables_manager = iptables_manager.IptablesManager(use_ipv6=True)
def _dscp_chain_name(self, direction, device):
return iptables_manager.get_chain_name(
"qos-%s%s" % (self.IPTABLES_DIRECTION_PREFIX[direction],
device[3:]))
def _dscp_rule(self, direction, device):
return ('-m physdev --%s %s --physdev-is-bridged '
'-j $%s') % (self.IPTABLES_DIRECTION[direction],
device,
self._dscp_chain_name(direction, device))
def _dscp_rule_tag(self, device):
return "dscp-%s" % device
@log_helpers.log_method_call
def create_bandwidth_limit(self, port, rule):
@ -53,6 +75,74 @@ class QosLinuxbridgeAgentDriver(qos.QosAgentDriver):
tc_wrapper = self._get_tc_wrapper(port)
tc_wrapper.delete_filters_bw_limit()
@log_helpers.log_method_call
def create_dscp_marking(self, port, rule):
with self.iptables_manager.defer_apply():
self._set_outgoing_qos_chain_for_port(port)
self._set_dscp_mark_rule(port, rule.dscp_mark)
@log_helpers.log_method_call
def update_dscp_marking(self, port, rule):
with self.iptables_manager.defer_apply():
self._delete_dscp_mark_rule(port)
self._set_outgoing_qos_chain_for_port(port)
self._set_dscp_mark_rule(port, rule.dscp_mark)
@log_helpers.log_method_call
def delete_dscp_marking(self, port):
with self.iptables_manager.defer_apply():
self._delete_dscp_mark_rule(port)
self._delete_outgoing_qos_chain_for_port(port)
def _set_outgoing_qos_chain_for_port(self, port):
chain_name = self._dscp_chain_name(
const.EGRESS_DIRECTION, port['device'])
chain_rule = self._dscp_rule(
const.EGRESS_DIRECTION, port['device'])
self.iptables_manager.ipv4['mangle'].add_chain(chain_name)
self.iptables_manager.ipv6['mangle'].add_chain(chain_name)
self.iptables_manager.ipv4['mangle'].add_rule('POSTROUTING',
chain_rule)
self.iptables_manager.ipv6['mangle'].add_rule('POSTROUTING',
chain_rule)
def _delete_outgoing_qos_chain_for_port(self, port):
chain_name = self._dscp_chain_name(
const.EGRESS_DIRECTION, port['device'])
chain_rule = self._dscp_rule(
const.EGRESS_DIRECTION, port['device'])
if self._qos_chain_is_empty(port, 4):
self.iptables_manager.ipv4['mangle'].remove_chain(chain_name)
self.iptables_manager.ipv4['mangle'].remove_rule('POSTROUTING',
chain_rule)
if self._qos_chain_is_empty(port, 6):
self.iptables_manager.ipv6['mangle'].remove_chain(chain_name)
self.iptables_manager.ipv6['mangle'].remove_rule('POSTROUTING',
chain_rule)
def _set_dscp_mark_rule(self, port, dscp_value):
chain_name = self._dscp_chain_name(
const.EGRESS_DIRECTION, port['device'])
rule = "-j DSCP --set-dscp %s" % dscp_value
self.iptables_manager.ipv4['mangle'].add_rule(
chain_name, rule, tag=self._dscp_rule_tag(port['device']))
self.iptables_manager.ipv6['mangle'].add_rule(
chain_name, rule, tag=self._dscp_rule_tag(port['device']))
def _delete_dscp_mark_rule(self, port):
self.iptables_manager.ipv4['mangle'].clear_rules_by_tag(
self._dscp_rule_tag(port['device']))
self.iptables_manager.ipv6['mangle'].clear_rules_by_tag(
self._dscp_rule_tag(port['device']))
def _qos_chain_is_empty(self, port, ip_version=4):
chain_name = self._dscp_chain_name(
const.EGRESS_DIRECTION, port['device'])
rules_in_chain = self.iptables_manager.get_chain(
"mangle", chain_name, ip_version=ip_version)
return len(rules_in_chain) == 0
def _get_tc_wrapper(self, port):
return tc_lib.TcCommand(
port['device'],

View File

@ -32,7 +32,8 @@ class LinuxbridgeMechanismDriver(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_MARKING]
def __init__(self):
sg_enabled = securitygroups_rpc.is_firewall_enabled()

View File

@ -16,6 +16,7 @@
import re
from neutron.agent.linux import async_process
from neutron.agent.linux import iptables_manager
from neutron.common import utils as common_utils
@ -38,6 +39,15 @@ def extract_mod_nw_tos_action(flows):
return tos_mark
def extract_dscp_value_from_iptables_rules(rules):
pattern = (r"^-A neutron-linuxbri-qos-.* -j DSCP "
"--set-dscp (?P<dscp_value>0x[A-Fa-f0-9]+)$")
for rule in rules:
m = re.match(pattern, rule)
if m:
return int(m.group("dscp_value"), 16)
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)
@ -49,7 +59,7 @@ def wait_until_bandwidth_limit_rule_applied(bridge, port_vif, rule):
common_utils.wait_until_true(_bandwidth_limit_rule_applied)
def wait_until_dscp_marking_rule_applied(bridge, port_vif, rule):
def wait_until_dscp_marking_rule_applied_ovs(bridge, port_vif, rule):
def _dscp_marking_rule_applied():
port_num = bridge.get_port_ofport(port_vif)
@ -64,6 +74,20 @@ def wait_until_dscp_marking_rule_applied(bridge, port_vif, rule):
common_utils.wait_until_true(_dscp_marking_rule_applied)
def wait_until_dscp_marking_rule_applied_linuxbridge(
namespace, port_vif, expected_rule):
iptables = iptables_manager.IptablesManager(
namespace=namespace)
def _dscp_marking_rule_applied():
mangle_rules = iptables.get_rules_for_table("mangle")
dscp_mark = extract_dscp_value_from_iptables_rules(mangle_rules)
return dscp_mark == expected_rule
common_utils.wait_until_true(_dscp_marking_rule_applied)
def wait_for_dscp_marked_packet(sender_vm, receiver_vm, dscp_mark):
cmd = ["tcpdump", "-i", receiver_vm.port.name, "-nlt"]
if dscp_mark:

View File

@ -45,14 +45,18 @@ DSCP_MARK = 16
class BaseQoSRuleTestCase(object):
of_interface = None
ovsdb_interface = None
number_of_hosts = 1
def setUp(self):
host_desc = [environment.HostDescription(
l3_agent=False,
of_interface=self.of_interface,
ovsdb_interface=self.ovsdb_interface,
l2_agent_type=self.l2_agent_type)]
env_desc = environment.EnvironmentDescription(qos=True)
host_desc = [
environment.HostDescription(
l3_agent=False,
of_interface=self.of_interface,
ovsdb_interface=self.ovsdb_interface,
l2_agent_type=self.l2_agent_type
) for _ in range(self.number_of_hosts)]
env_desc = environment.EnvironmentDescription(
qos=True)
env = environment.Environment(env_desc, host_desc)
super(BaseQoSRuleTestCase, self).setUp(env)
@ -95,6 +99,9 @@ class BaseQoSRuleTestCase(object):
class _TestBwLimitQoS(BaseQoSRuleTestCase):
number_of_hosts = 1
def _wait_for_bw_rule_removed(self, vm):
# No values are provided when port doesn't have qos policy
self._wait_for_bw_rule_applied(vm, None, None)
@ -172,36 +179,9 @@ class TestBwLimitQoSLinuxbridge(_TestBwLimitQoS, base.BaseFullStackTestCase):
lambda: tc.get_filters_bw_limits() == (limit, burst))
class TestDscpMarkingQoSOvs(BaseQoSRuleTestCase, base.BaseFullStackTestCase):
scenarios = fullstack_utils.get_ovs_interface_scenarios()
l2_agent_type = constants.AGENT_TYPE_OVS
class _TestDscpMarkingQoS(BaseQoSRuleTestCase):
def setUp(self):
host_desc = [
environment.HostDescription(
l3_agent=False,
of_interface=self.of_interface,
ovsdb_interface=self.ovsdb_interface,
l2_agent_type=self.l2_agent_type
) for _ in range(2)]
env_desc = environment.EnvironmentDescription(
qos=True)
env = environment.Environment(env_desc, host_desc)
super(BaseQoSRuleTestCase, self).setUp(env)
self.tenant_id = uuidutils.generate_uuid()
self.network = self.safe_client.create_network(self.tenant_id,
'network-test')
self.subnet = self.safe_client.create_subnet(
self.tenant_id, self.network['id'],
cidr='10.0.0.0/24',
gateway_ip='10.0.0.1',
name='subnet-test',
enable_dhcp=False)
def _wait_for_dscp_marking_rule_applied(self, vm, dscp_mark):
l2_extensions.wait_until_dscp_marking_rule_applied(
vm.bridge, vm.port.name, dscp_mark)
number_of_hosts = 2
def _wait_for_dscp_marking_rule_removed(self, vm):
self._wait_for_dscp_marking_rule_applied(vm, None)
@ -272,19 +252,29 @@ class TestDscpMarkingQoSOvs(BaseQoSRuleTestCase, base.BaseFullStackTestCase):
sender, receiver, DSCP_MARK)
class TestDscpMarkingQoSOvs(_TestDscpMarkingQoS, base.BaseFullStackTestCase):
scenarios = fullstack_utils.get_ovs_interface_scenarios()
l2_agent_type = constants.AGENT_TYPE_OVS
def _wait_for_dscp_marking_rule_applied(self, vm, dscp_mark):
l2_extensions.wait_until_dscp_marking_rule_applied_ovs(
vm.bridge, vm.port.name, dscp_mark)
class TestDscpMarkingQoSLinuxbridge(_TestDscpMarkingQoS,
base.BaseFullStackTestCase):
l2_agent_type = constants.AGENT_TYPE_LINUXBRIDGE
def _wait_for_dscp_marking_rule_applied(self, vm, dscp_mark):
l2_extensions.wait_until_dscp_marking_rule_applied_linuxbridge(
vm.host.host_namespace, vm.port.name, dscp_mark)
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,
mech_drivers=mech_driver)
env_desc = environment.EnvironmentDescription(qos=True, l2_pop=True)
env = environment.Environment(env_desc, host_desc)
super(TestQoSWithL2Population, self).setUp(env)

View File

@ -141,7 +141,7 @@ class OVSAgentQoSExtensionTestFramework(base.OVSAgentTestFramework):
self.assertIsNone(tos_mark)
def wait_until_dscp_marking_rule_applied(self, port, dscp_mark):
l2_extensions.wait_until_dscp_marking_rule_applied(
l2_extensions.wait_until_dscp_marking_rule_applied_ovs(
self.agent.int_br, port['vif_name'], dscp_mark)
def _create_port_with_qos(self):

View File

@ -26,6 +26,7 @@ from neutron.tests import base
TEST_LATENCY_VALUE = 100
DSCP_VALUE = 32
class QosLinuxbridgeAgentDriverTestCase(base.BaseTestCase):
@ -35,7 +36,8 @@ class QosLinuxbridgeAgentDriverTestCase(base.BaseTestCase):
cfg.CONF.set_override("tbf_latency", TEST_LATENCY_VALUE, "QOS")
self.qos_driver = qos_driver.QosLinuxbridgeAgentDriver()
self.qos_driver.initialize()
self.rule = self._create_bw_limit_rule_obj()
self.rule_bw_limit = self._create_bw_limit_rule_obj()
self.rule_dscp_marking = self._create_dscp_marking_rule_obj()
self.port = self._create_fake_port(uuidutils.generate_uuid())
def _create_bw_limit_rule_obj(self):
@ -46,32 +48,150 @@ class QosLinuxbridgeAgentDriverTestCase(base.BaseTestCase):
rule_obj.obj_reset_changes()
return rule_obj
def _create_dscp_marking_rule_obj(self):
rule_obj = rule.QosDscpMarkingRule()
rule_obj.id = uuidutils.generate_uuid()
rule_obj.dscp_mark = DSCP_VALUE
rule_obj.obj_reset_changes()
return rule_obj
def _create_fake_port(self, policy_id):
return {'qos_policy_id': policy_id,
'network_qos_policy_id': None,
'device': 'fake_tap'}
def test_create_rule(self):
def _dscp_mark_chain_name(self, device):
return "qos-o%s" % device[3:]
def _dscp_postrouting_rule(self, device):
return ("-m physdev --physdev-in %s --physdev-is-bridged "
"-j $qos-o%s") % (device, device[3:])
def _dscp_rule(self, dscp_mark_value):
return "-j DSCP --set-dscp %s" % dscp_mark_value
def _dscp_rule_tag(self, device):
return "dscp-%s" % device
def test_create_bandwidth_limit(self):
with mock.patch.object(
tc_lib.TcCommand, "set_filters_bw_limit"
) as set_bw_limit:
self.qos_driver.create_bandwidth_limit(self.port, self.rule)
self.qos_driver.create_bandwidth_limit(self.port,
self.rule_bw_limit)
set_bw_limit.assert_called_once_with(
self.rule.max_kbps, self.rule.max_burst_kbps,
self.rule_bw_limit.max_kbps, self.rule_bw_limit.max_burst_kbps,
)
def test_update_rule(self):
def test_update_bandwidth_limit(self):
with mock.patch.object(
tc_lib.TcCommand, "update_filters_bw_limit"
) as update_bw_limit:
self.qos_driver.update_bandwidth_limit(self.port, self.rule)
self.qos_driver.update_bandwidth_limit(self.port,
self.rule_bw_limit)
update_bw_limit.assert_called_once_with(
self.rule.max_kbps, self.rule.max_burst_kbps,
self.rule_bw_limit.max_kbps, self.rule_bw_limit.max_burst_kbps,
)
def test_delete_rule(self):
def test_delete_bandwidth_limit(self):
with mock.patch.object(
tc_lib.TcCommand, "delete_filters_bw_limit"
) as delete_bw_limit:
self.qos_driver.delete_bandwidth_limit(self.port)
delete_bw_limit.assert_called_once_with()
def test_create_dscp_marking(self):
expected_calls = [
mock.call.add_chain(
self._dscp_mark_chain_name(self.port['device'])),
mock.call.add_rule(
"POSTROUTING",
self._dscp_postrouting_rule(self.port['device'])),
mock.call.add_rule(
self._dscp_mark_chain_name(self.port['device']),
self._dscp_rule(DSCP_VALUE),
tag=self._dscp_rule_tag(self.port['device'])
)
]
with mock.patch.object(
self.qos_driver, "iptables_manager") as iptables_manager:
iptables_manager.ip4['mangle'] = mock.Mock()
iptables_manager.ip6['mangle'] = mock.Mock()
self.qos_driver.create_dscp_marking(
self.port, self.rule_dscp_marking)
iptables_manager.ipv4['mangle'].assert_has_calls(expected_calls)
iptables_manager.ipv6['mangle'].assert_has_calls(expected_calls)
def test_update_dscp_marking(self):
expected_calls = [
mock.call.clear_rules_by_tag(
self._dscp_rule_tag(self.port['device'])),
mock.call.add_chain(
self._dscp_mark_chain_name(self.port['device'])),
mock.call.add_rule(
"POSTROUTING",
self._dscp_postrouting_rule(self.port['device'])),
mock.call.add_rule(
self._dscp_mark_chain_name(self.port['device']),
self._dscp_rule(DSCP_VALUE),
tag=self._dscp_rule_tag(self.port['device'])
)
]
with mock.patch.object(
self.qos_driver, "iptables_manager") as iptables_manager:
iptables_manager.ip4['mangle'] = mock.Mock()
iptables_manager.ip6['mangle'] = mock.Mock()
self.qos_driver.update_dscp_marking(
self.port, self.rule_dscp_marking)
iptables_manager.ipv4['mangle'].assert_has_calls(expected_calls)
iptables_manager.ipv6['mangle'].assert_has_calls(expected_calls)
def test_delete_dscp_marking_chain_empty(self):
dscp_chain_name = self._dscp_mark_chain_name(self.port['device'])
expected_calls = [
mock.call.clear_rules_by_tag(
self._dscp_rule_tag(self.port['device'])),
mock.call.remove_chain(
dscp_chain_name),
mock.call.remove_rule(
"POSTROUTING",
self._dscp_postrouting_rule(self.port['device']))
]
with mock.patch.object(
self.qos_driver, "iptables_manager") as iptables_manager:
iptables_manager.ip4['mangle'] = mock.Mock()
iptables_manager.ip6['mangle'] = mock.Mock()
iptables_manager.get_chain = mock.Mock(return_value=[])
self.qos_driver.delete_dscp_marking(self.port)
iptables_manager.ipv4['mangle'].assert_has_calls(expected_calls)
iptables_manager.ipv6['mangle'].assert_has_calls(expected_calls)
iptables_manager.get_chain.assert_has_calls([
mock.call("mangle", dscp_chain_name, ip_version=4),
mock.call("mangle", dscp_chain_name, ip_version=6)
])
def test_delete_dscp_marking_chain_not_empty(self):
dscp_chain_name = self._dscp_mark_chain_name(self.port['device'])
expected_calls = [
mock.call.clear_rules_by_tag(
self._dscp_rule_tag(self.port['device'])),
]
with mock.patch.object(
self.qos_driver, "iptables_manager") as iptables_manager:
iptables_manager.ip4['mangle'] = mock.Mock()
iptables_manager.ip6['mangle'] = mock.Mock()
iptables_manager.get_chain = mock.Mock(
return_value=["some other rule"])
self.qos_driver.delete_dscp_marking(self.port)
iptables_manager.ipv4['mangle'].assert_has_calls(expected_calls)
iptables_manager.ipv6['mangle'].assert_has_calls(expected_calls)
iptables_manager.get_chain.assert_has_calls([
mock.call("mangle", dscp_chain_name, ip_version=4),
mock.call("mangle", dscp_chain_name, ip_version=6)
])
iptables_manager.ipv4['mangle'].remove_chain.assert_not_called()
iptables_manager.ipv4['mangle'].remove_rule.assert_not_called()

View File

@ -0,0 +1,6 @@
---
prelude: >
The LinuxBridge agent now supports QoS DSCP marking.
features:
- The LinuxBridge agent can now configure DSCP marking for packets outgoing
for ports with QoS policy.