Use same instance of iptables_manager in L2 agent and extensions

This commit adds common_agent_extension class which is agent API
for L2 extension drivers used e.g. by Linuxbridge agent.
This is necessary to be able to use instance of iptables_manager
used in firewall driver also in L2 extension drivers (like qos).

This patch refactors little bit iptables_manager code to make possible
to initialize e.g. mangle or nat table on demand, even if iptables
is created as "state_less"

Change-Id: I3b66e49b7f176124e8aea3eb96d0d465f1ab1ea0
Closes-Bug: #1736674
This commit is contained in:
Sławek Kapłoński 2017-12-14 14:51:01 +01:00
parent 59e2c40f14
commit cbee0f9f88
13 changed files with 250 additions and 48 deletions

View File

@ -41,3 +41,14 @@ hardened bridge objects with cookie values allocated for calling extensions::
Bridge objects returned by those methods already have new default cookie values
allocated for extension flows. All flow management methods (add_flow, mod_flow,
...) enforce those allocated cookies.
Linuxbridge agent API
~~~~~~~~~~~~~~~~~~~~~~
* neutron.plugins.ml2.drivers.linuxbridge.agent.linuxbridge_agent_extension_api
The Linux bridge agent extension API object includes a method that returns an
instance of the IptablesManager class, which is used by the L2 agent to manage
security group rules::
#. get_iptables_manager

View File

@ -380,6 +380,10 @@ tc. Details about how it is calculated can be found in
`here <http://unix.stackexchange.com/a/100797>`_. This solution is similar to Open
vSwitch implementation.
The Linux bridge DSCP marking implementation relies on the
linuxbridge_extension_api to request access to the IptablesManager class
and to manage chains in the ``mangle`` table in iptables.
QoS driver design
-----------------

View File

@ -333,31 +333,66 @@ class IptablesManager(object):
tables['filter'].add_rule('neutron-filter-top', '-j $local',
wrap=False)
self.ipv4.update({'raw': IptablesTable(binary_name=self.wrap_name)})
self.ipv6.update({'raw': IptablesTable(binary_name=self.wrap_name)})
# Wrap the built-in chains
builtin_chains = {4: {'filter': ['INPUT', 'OUTPUT', 'FORWARD']},
6: {'filter': ['INPUT', 'OUTPUT', 'FORWARD']}}
builtin_chains[4].update({'raw': ['PREROUTING', 'OUTPUT']})
builtin_chains[6].update({'raw': ['PREROUTING', 'OUTPUT']})
self._configure_builtin_chains(builtin_chains)
if not state_less:
self.ipv4.update(
{'mangle': IptablesTable(binary_name=self.wrap_name)})
builtin_chains[4].update(
{'mangle': ['PREROUTING', 'INPUT', 'FORWARD', 'OUTPUT',
'POSTROUTING']})
self.ipv6.update(
{'mangle': IptablesTable(binary_name=self.wrap_name)})
builtin_chains[6].update(
{'mangle': ['PREROUTING', 'INPUT', 'FORWARD', 'OUTPUT',
'POSTROUTING']})
self.ipv4.update(
{'nat': IptablesTable(binary_name=self.wrap_name)})
builtin_chains[4].update({'nat': ['PREROUTING',
'OUTPUT', 'POSTROUTING']})
self.initialize_mangle_table()
self.initialize_nat_table()
self.ipv4.update({'raw': IptablesTable(binary_name=self.wrap_name)})
builtin_chains[4].update({'raw': ['PREROUTING', 'OUTPUT']})
self.ipv6.update({'raw': IptablesTable(binary_name=self.wrap_name)})
builtin_chains[6].update({'raw': ['PREROUTING', 'OUTPUT']})
def initialize_mangle_table(self):
self.ipv4.update(
{'mangle': IptablesTable(binary_name=self.wrap_name)})
self.ipv6.update(
{'mangle': IptablesTable(binary_name=self.wrap_name)})
builtin_chains = {
4: {'mangle': ['PREROUTING', 'INPUT', 'FORWARD', 'OUTPUT',
'POSTROUTING']},
6: {'mangle': ['PREROUTING', 'INPUT', 'FORWARD', 'OUTPUT',
'POSTROUTING']}}
self._configure_builtin_chains(builtin_chains)
# Add a mark chain to mangle PREROUTING chain. It is used to
# identify ingress packets from a certain interface.
self.ipv4['mangle'].add_chain('mark')
self.ipv4['mangle'].add_rule('PREROUTING', '-j $mark')
def initialize_nat_table(self):
self.ipv4.update(
{'nat': IptablesTable(binary_name=self.wrap_name)})
builtin_chains = {
4: {'nat': ['PREROUTING', 'OUTPUT', 'POSTROUTING']}}
self._configure_builtin_chains(builtin_chains)
# Add a neutron-postrouting-bottom chain. It's intended to be
# shared among the various neutron components. We set it as the
# last chain of POSTROUTING chain.
self.ipv4['nat'].add_chain('neutron-postrouting-bottom', wrap=False)
self.ipv4['nat'].add_rule(
'POSTROUTING', '-j neutron-postrouting-bottom', wrap=False)
# We add a snat chain to the shared neutron-postrouting-bottom
# chain so that it's applied last.
self.ipv4['nat'].add_chain('snat')
self.ipv4['nat'].add_rule('neutron-postrouting-bottom',
'-j $snat', wrap=False,
comment=ic.SNAT_OUT)
# And then we add a float-snat chain and jump to first thing in
# the snat chain.
self.ipv4['nat'].add_chain('float-snat')
self.ipv4['nat'].add_rule('snat', '-j $float-snat')
def _configure_builtin_chains(self, builtin_chains):
for ip_version in builtin_chains:
if ip_version == 4:
tables = self.ipv4
@ -370,33 +405,6 @@ class IptablesManager(object):
tables[table].add_rule(chain, '-j $%s' %
(chain), wrap=False)
if not state_less:
# Add a neutron-postrouting-bottom chain. It's intended to be
# shared among the various neutron components. We set it as the
# last chain of POSTROUTING chain.
self.ipv4['nat'].add_chain('neutron-postrouting-bottom',
wrap=False)
self.ipv4['nat'].add_rule('POSTROUTING',
'-j neutron-postrouting-bottom',
wrap=False)
# We add a snat chain to the shared neutron-postrouting-bottom
# chain so that it's applied last.
self.ipv4['nat'].add_chain('snat')
self.ipv4['nat'].add_rule('neutron-postrouting-bottom',
'-j $snat', wrap=False,
comment=ic.SNAT_OUT)
# And then we add a float-snat chain and jump to first thing in
# the snat chain.
self.ipv4['nat'].add_chain('float-snat')
self.ipv4['nat'].add_rule('snat', '-j $float-snat')
# Add a mark chain to mangle PREROUTING chain. It is used to
# identify ingress packets from a certain interface.
self.ipv4['mangle'].add_chain('mark')
self.ipv4['mangle'].add_rule('PREROUTING', '-j $mark')
def get_tables(self, ip_version):
return {4: self.ipv4, 6: self.ipv6}[ip_version]

View File

@ -157,6 +157,13 @@ class CommonAgentManagerBase(object):
It must reflect the CommonAgentManagerRpcCallBackBase Interface.
"""
@abc.abstractmethod
def get_agent_api(self, **kwargs):
"""Get L2 extensions drivers API interface class.
:return: instance of the class containing Agent Extension API
"""
@abc.abstractmethod
def get_rpc_consumers(self):
"""Get a list of topics for which an RPC consumer should be created

View File

@ -173,8 +173,9 @@ class CommonAgentLoop(service.Service):
ext_manager.register_opts(cfg.CONF)
self.ext_manager = (
ext_manager.L2AgentExtensionsManager(cfg.CONF))
agent_api = self.mgr.get_agent_api(sg_agent=self.sg_agent)
self.ext_manager.initialize(
connection, self.mgr.get_extension_driver_type())
connection, self.mgr.get_extension_driver_type(), agent_api)
def _clean_network_ports(self, device):
for netid, ports_list in self.network_ports.items():

View File

@ -20,6 +20,7 @@ from oslo_log import log
from neutron.agent.l2.extensions import qos_linux as qos
from neutron.agent.linux import iptables_manager
from neutron.agent.linux import tc_lib
from neutron.common import ipv6_utils
from neutron.services.qos.drivers.linuxbridge import driver
LOG = log.getLogger(__name__)
@ -39,10 +40,26 @@ class QosLinuxbridgeAgentDriver(qos.QosLinuxAgentDriver):
IPTABLES_DIRECTION_PREFIX = {const.INGRESS_DIRECTION: "i",
const.EGRESS_DIRECTION: "o"}
def __init__(self):
super(QosLinuxbridgeAgentDriver, self).__init__()
self.iptables_manager = None
self.agent_api = None
self.tbf_latency = cfg.CONF.QOS.tbf_latency
def consume_api(self, agent_api):
self.agent_api = agent_api
def initialize(self):
LOG.info("Initializing Linux bridge QoS extension")
self.iptables_manager = iptables_manager.IptablesManager(use_ipv6=True)
self.tbf_latency = cfg.CONF.QOS.tbf_latency
if self.agent_api:
self.iptables_manager = self.agent_api.get_iptables_manager()
if not self.iptables_manager:
# If agent_api can't provide iptables_manager, it can be
# created here for extension needs
self.iptables_manager = iptables_manager.IptablesManager(
state_less=True,
use_ipv6=ipv6_utils.is_enabled_and_bind_by_default())
self.iptables_manager.initialize_mangle_table()
def _dscp_chain_name(self, direction, device):
return iptables_manager.get_chain_name(

View File

@ -0,0 +1,32 @@
# Copyright 2017 OVH SAS
# All Rights Reserved.
#
# 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.
class LinuxbridgeAgentExtensionAPI(object):
'''Implements the Agent API for L2 agent.
Extensions can gain access to this API by overriding the consume_api
method which has been added to the AgentExtension class.
'''
def __init__(self, iptables_manager):
super(LinuxbridgeAgentExtensionAPI, self).__init__()
self.iptables_manager = iptables_manager
def get_iptables_manager(self):
"""Allows extensions to get an iptables manager, used by agent,
to use for managing extension specific iptables rules
"""
return self.iptables_manager

View File

@ -51,6 +51,8 @@ from neutron.plugins.ml2.drivers.linuxbridge.agent.common \
import constants as lconst
from neutron.plugins.ml2.drivers.linuxbridge.agent.common \
import utils as lb_utils
from neutron.plugins.ml2.drivers.linuxbridge.agent import \
linuxbridge_agent_extension_api as agent_extension_api
from neutron.plugins.ml2.drivers.linuxbridge.agent \
import linuxbridge_capabilities
@ -62,6 +64,13 @@ BRIDGE_NAME_PREFIX = "brq"
MAX_VLAN_POSTFIX_LEN = 5
VXLAN_INTERFACE_PREFIX = "vxlan-"
IPTABLES_DRIVERS = [
'iptables',
'iptables_hybrid',
'neutron.agent.linux.iptables_firewall.IptablesFirewallDriver',
'neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver'
]
class LinuxBridgeManager(amb.CommonAgentManagerBase):
def __init__(self, bridge_mappings, interface_mappings):
@ -71,6 +80,7 @@ class LinuxBridgeManager(amb.CommonAgentManagerBase):
self.validate_interface_mappings()
self.validate_bridge_mappings()
self.ip = ip_lib.IPWrapper()
self.agent_api = None
# VXLAN related parameters:
self.local_ip = cfg.CONF.VXLAN.local_ip
self.vxlan_mode = lconst.VXLAN_NONE
@ -787,6 +797,21 @@ class LinuxBridgeManager(amb.CommonAgentManagerBase):
def get_rpc_callbacks(self, context, agent, sg_agent):
return LinuxBridgeRpcCallbacks(context, agent, sg_agent)
def get_agent_api(self, **kwargs):
if self.agent_api:
return self.agent_api
sg_agent = kwargs.get("sg_agent")
iptables_manager = self._get_iptables_manager(sg_agent)
self.agent_api = agent_extension_api.LinuxbridgeAgentExtensionAPI(
iptables_manager)
return self.agent_api
def _get_iptables_manager(self, sg_agent):
if not sg_agent:
return None
if cfg.CONF.SECURITYGROUP.firewall_driver in IPTABLES_DRIVERS:
return sg_agent.firewall.iptables
def get_rpc_consumers(self):
consumers = [[topics.PORT, topics.UPDATE],
[topics.NETWORK, topics.DELETE],

View File

@ -143,6 +143,9 @@ class MacvtapManager(amb.CommonAgentManagerBase):
def get_rpc_callbacks(self, context, agent, sg_agent):
return MacvtapRPCCallBack(context, agent, sg_agent)
def get_agent_api(self, **kwargs):
pass
def get_rpc_consumers(self):
consumers = [[topics.PORT, topics.UPDATE],
[topics.NETWORK, topics.DELETE],

View File

@ -1334,3 +1334,15 @@ class IptablesManagerStateLessTestCase(base.BaseTestCase):
def test_mangle_not_found(self):
self.assertNotIn('mangle', self.iptables.ipv4)
def test_initialize_mangle_table(self):
iptables = iptables_manager.IptablesManager(state_less=True)
iptables.initialize_mangle_table()
self.assertIn('mangle', iptables.ipv4)
self.assertNotIn('nat', iptables.ipv4)
def test_initialize_nat_table(self):
iptables = iptables_manager.IptablesManager(state_less=True)
iptables.initialize_nat_table()
self.assertIn('nat', iptables.ipv4)
self.assertNotIn('mangle', iptables.ipv4)

View File

@ -78,6 +78,43 @@ class QosLinuxbridgeAgentDriverTestCase(base.BaseTestCase):
def _dscp_rule_tag(self, device):
return "dscp-%s" % device
def test_initialize_iptables_manager_passed_through_api(self):
iptables_manager = mock.Mock()
qos_drv = qos_driver.QosLinuxbridgeAgentDriver()
with mock.patch.object(
qos_drv, "agent_api"
) as agent_api, mock.patch(
"neutron.agent.linux.iptables_manager.IptablesManager"
) as IptablesManager:
agent_api.get_iptables_manager.return_value = (
iptables_manager)
qos_drv.initialize()
self.assertEqual(iptables_manager, qos_drv.iptables_manager)
self.assertNotEqual(IptablesManager(), qos_drv.iptables_manager)
iptables_manager.initialize_mangle_table.assert_called_once_with()
def test_initialize_iptables_manager_not_passed_through_api(self):
qos_drv = qos_driver.QosLinuxbridgeAgentDriver()
with mock.patch.object(
qos_drv, "agent_api"
) as agent_api, mock.patch(
"neutron.agent.linux.iptables_manager.IptablesManager"
) as IptablesManager:
agent_api.get_iptables_manager.return_value = None
qos_drv.initialize()
self.assertEqual(IptablesManager(), qos_drv.iptables_manager)
IptablesManager().initialize_mangle_table.assert_called_once_with()
def test_initialize_iptables_manager_no_agent_api(self):
qos_drv = qos_driver.QosLinuxbridgeAgentDriver()
with mock.patch(
"neutron.agent.linux.iptables_manager.IptablesManager"
) as IptablesManager:
qos_driver.agent_api = None
qos_drv.initialize()
self.assertEqual(IptablesManager(), qos_drv.iptables_manager)
IptablesManager().initialize_mangle_table.assert_called_once_with()
def test_create_egress_bandwidth_limit(self):
with mock.patch.object(
tc_lib.TcCommand, "set_filters_bw_limit"

View File

@ -0,0 +1,33 @@
# Copyright 2017 OVH SAS
# All Rights Reserved.
#
# 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.
import mock
from neutron.plugins.ml2.drivers.linuxbridge.agent import \
linuxbridge_agent_extension_api as ext_api
from neutron.tests import base
class TestLinuxbridgeAgentExtensionAPI(base.BaseTestCase):
def setUp(self):
super(TestLinuxbridgeAgentExtensionAPI, self).setUp()
self.iptables_manager = mock.Mock()
self.extension_api = ext_api.LinuxbridgeAgentExtensionAPI(
self.iptables_manager)
def test_get_iptables_manager(self):
self.assertEqual(self.iptables_manager,
self.extension_api.get_iptables_manager())

View File

@ -0,0 +1,12 @@
---
features:
- |
L2 agents based on ``ML2`` ``_common_agent`` have now the L2 extension API
available. This API can be used by L2 extension drivers to request
resources from the L2 agent.
It is used, for example, to pass an instance of the ``IptablesManager``
to the ``Linuxbridge`` L2 agent ``QoS extension driver``.
fixes:
- |
Fixes bug 1736674, security group rules are now properly applied
by ``Linuxbridge L2 agent`` with ``QoS extension driver`` enabled.