diff --git a/neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/qos_driver.py b/neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/qos_driver.py index 2d2b88d3741..90e71ea527d 100644 --- a/neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/qos_driver.py +++ b/neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/qos_driver.py @@ -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'], diff --git a/neutron/plugins/ml2/drivers/linuxbridge/mech_driver/mech_linuxbridge.py b/neutron/plugins/ml2/drivers/linuxbridge/mech_driver/mech_linuxbridge.py index 4c7337abee8..c1eb7b3ee81 100644 --- a/neutron/plugins/ml2/drivers/linuxbridge/mech_driver/mech_linuxbridge.py +++ b/neutron/plugins/ml2/drivers/linuxbridge/mech_driver/mech_linuxbridge.py @@ -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() diff --git a/neutron/tests/common/agents/l2_extensions.py b/neutron/tests/common/agents/l2_extensions.py index 158daaa8971..92a7f82810f 100644 --- a/neutron/tests/common/agents/l2_extensions.py +++ b/neutron/tests/common/agents/l2_extensions.py @@ -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 (?P0x[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: diff --git a/neutron/tests/fullstack/test_qos.py b/neutron/tests/fullstack/test_qos.py index 58dd4286746..3c6239b52be 100644 --- a/neutron/tests/fullstack/test_qos.py +++ b/neutron/tests/fullstack/test_qos.py @@ -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) diff --git a/neutron/tests/functional/agent/l2/extensions/test_ovs_agent_qos_extension.py b/neutron/tests/functional/agent/l2/extensions/test_ovs_agent_qos_extension.py index 7492be4d364..b95e1c6f862 100644 --- a/neutron/tests/functional/agent/l2/extensions/test_ovs_agent_qos_extension.py +++ b/neutron/tests/functional/agent/l2/extensions/test_ovs_agent_qos_extension.py @@ -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): diff --git a/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/test_qos_driver.py b/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/test_qos_driver.py index fefce9c6068..46ec14eb8e6 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/test_qos_driver.py +++ b/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/test_qos_driver.py @@ -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() diff --git a/releasenotes/notes/Dscp-marking-for-linuxbridge-agent-e765d0d934fa4017.yaml b/releasenotes/notes/Dscp-marking-for-linuxbridge-agent-e765d0d934fa4017.yaml new file mode 100644 index 00000000000..40126132d76 --- /dev/null +++ b/releasenotes/notes/Dscp-marking-for-linuxbridge-agent-e765d0d934fa4017.yaml @@ -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.