Add router_factory to l3-agent and L3 extension API

Currently, most implementations override the L3NatAgent class itself
for their own logic since there is no proper interface to extend
RouterInfo class. This adds unnecessary complexity for developers
who just want to extend router mechanism instead of whole RPC.

Add a RouterFactory class that developer can registers RouterInfo class
and delegate it for RouterInfo creation. Seperate functions and variables
which currently used externally to abstract class from RouterInfo, so that
extension can use the basic interface.

Provide the router registration function to the l3 extension API so that
extension can extend RouterInfo itself which correspond to each features
(ha, distribtued, ha + distributed)

Depends-On: https://review.openstack.org/#/c/620348/
Closes-Bug: #1804634
Partially-Implements: blueprint openflow-based-dvr
Change-Id: I1eff726900a8e67596814ca9a5f392938f154d7b
This commit is contained in:
Yang Youseok 2018-11-23 15:55:02 +09:00
parent bb9edb25b0
commit ec875b42b6
11 changed files with 261 additions and 84 deletions

View File

@ -195,6 +195,40 @@ class L3PluginApi(object):
return cctxt.call(context, 'get_host_ha_router_count', host=self.host)
class RouterFactory(object):
def __init__(self):
self._routers = {}
def register(self, features, router_cls):
"""Register router class which implements BaseRouterInfo
Features which is a list of strings converted to frozenset internally
for key uniqueness.
:param features: a list of strings of router's features
:param router_cls: a router class which implements BaseRouterInfo
"""
self._routers[frozenset(features)] = router_cls
def create(self, features, **kwargs):
"""Create router instance with registered router class
:param features: a list of strings of router's features
:param kwargs: arguments for router class
:returns: a router instance which implements BaseRouterInfo
:raises: n_exc.RouterNotFoundInRouterFactory
"""
try:
router = self._routers[frozenset(features)]
return router(**kwargs)
except KeyError:
exc = l3_exc.RouterNotFoundInRouterFactory(
router_id=kwargs['router_id'], features=features)
LOG.exception(exc.msg)
raise exc
@profiler.trace_cls("l3-agent")
class L3NATAgent(ha.AgentMixin,
dvr.AgentMixin,
@ -224,6 +258,8 @@ class L3NATAgent(ha.AgentMixin,
else:
self.conf = cfg.CONF
self.router_info = {}
self.router_factory = RouterFactory()
self._register_router_cls(self.router_factory)
self._check_config_params()
@ -329,6 +365,21 @@ class L3NATAgent(ha.AgentMixin,
except Exception:
LOG.exception('update_all_ha_network_port_statuses failed')
def _register_router_cls(self, factory):
factory.register([], legacy_router.LegacyRouter)
factory.register(['ha'], ha_router.HaRouter)
if self.conf.agent_mode == lib_const.L3_AGENT_MODE_DVR_SNAT:
factory.register(['distributed'],
dvr_router.DvrEdgeRouter)
factory.register(['ha', 'distributed'],
dvr_edge_ha_router.DvrEdgeHaRouter)
else:
factory.register(['distributed'],
dvr_local_router.DvrLocalRouter)
factory.register(['ha', 'distributed'],
dvr_local_router.DvrLocalRouter)
def _check_config_params(self):
"""Check items in configuration files.
@ -376,7 +427,6 @@ class L3NATAgent(ha.AgentMixin,
raise Exception(msg)
def _create_router(self, router_id, router):
args = []
kwargs = {
'agent': self,
'router_id': router_id,
@ -386,9 +436,15 @@ class L3NATAgent(ha.AgentMixin,
'interface_driver': self.driver,
}
features = []
if router.get('distributed'):
features.append('distributed')
kwargs['host'] = self.host
if router.get('ha'):
features.append('ha')
kwargs['state_change_callback'] = self.enqueue_state_change
if router.get('distributed') and router.get('ha'):
# Case 1: If the router contains information about the HA interface
# and if the requesting agent is a DVR_SNAT agent then go ahead
@ -399,22 +455,12 @@ class L3NATAgent(ha.AgentMixin,
# that needs to provision a router namespace because of a DVR
# service port (e.g. DHCP). So go ahead and create a regular DVR
# edge router.
if (self.conf.agent_mode == lib_const.L3_AGENT_MODE_DVR_SNAT and
router.get(lib_const.HA_INTERFACE_KEY) is not None):
kwargs['state_change_callback'] = self.enqueue_state_change
return dvr_edge_ha_router.DvrEdgeHaRouter(*args, **kwargs)
if (not router.get(lib_const.HA_INTERFACE_KEY) or
self.conf.agent_mode != lib_const.L3_AGENT_MODE_DVR_SNAT):
features.remove('ha')
kwargs.pop('state_change_callback')
if router.get('distributed'):
if self.conf.agent_mode == lib_const.L3_AGENT_MODE_DVR_SNAT:
return dvr_router.DvrEdgeRouter(*args, **kwargs)
else:
return dvr_local_router.DvrLocalRouter(*args, **kwargs)
if router.get('ha'):
kwargs['state_change_callback'] = self.enqueue_state_change
return ha_router.HaRouter(*args, **kwargs)
return legacy_router.LegacyRouter(*args, **kwargs)
return self.router_factory.create(features, **kwargs)
@lockutils.synchronized('resize_greenpool')
def _resize_process_pool(self):
@ -487,7 +533,8 @@ class L3NATAgent(ha.AgentMixin,
def init_extension_manager(self, connection):
l3_ext_manager.register_opts(self.conf)
self.agent_api = l3_ext_api.L3AgentExtensionAPI(self.router_info)
self.agent_api = l3_ext_api.L3AgentExtensionAPI(self.router_info,
self.router_factory)
self.l3_ext_manager = (
l3_ext_manager.L3AgentExtensionsManager(self.conf))
self.l3_ext_manager.initialize(

View File

@ -26,8 +26,9 @@ class L3AgentExtensionAPI(object):
agent's RouterInfo object.
'''
def __init__(self, router_info):
def __init__(self, router_info, router_factory):
self._router_info = router_info
self._router_factory = router_factory
def _local_namespaces(self):
local_ns_list = ip_lib.list_network_namespaces()
@ -68,3 +69,9 @@ class L3AgentExtensionAPI(object):
def get_router_info(self, router_id):
"""Return RouterInfo for the given router id."""
return self._router_info.get(router_id)
def register_router(self, features, router_cls):
"""Register router class with the given features. This is for the
plugin to ovrride with their own ``router_info`` class.
"""
self._router_factory.register(features, router_cls)

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import abc
import collections
import netaddr
@ -19,6 +20,7 @@ from neutron_lib import constants as lib_constants
from neutron_lib.exceptions import l3 as l3_exc
from neutron_lib.utils import helpers
from oslo_log import log as logging
import six
from neutron._i18n import _
from neutron.agent.l3 import namespaces
@ -41,7 +43,8 @@ ADDRESS_SCOPE_MARK_ID_MAX = 2048
DEFAULT_ADDRESS_SCOPE = "noscope"
class RouterInfo(object):
@six.add_metaclass(abc.ABCMeta)
class BaseRouterInfo(object):
def __init__(self,
agent,
@ -52,16 +55,88 @@ class RouterInfo(object):
use_ipv6=False):
self.agent = agent
self.router_id = router_id
self.agent_conf = agent_conf
self.ex_gw_port = None
# Invoke the setter for establishing initial SNAT action
self._snat_enabled = None
self.fip_map = {}
self.router = router
self.agent_conf = agent_conf
self.driver = interface_driver
self.use_ipv6 = use_ipv6
self.internal_ports = []
self.ns_name = None
self.process_monitor = None
def initialize(self, process_monitor):
"""Initialize the router on the system.
This differs from __init__ in that this method actually affects the
system creating namespaces, starting processes, etc. The other merely
initializes the python object. This separates in-memory object
initialization from methods that actually go do stuff to the system.
:param process_monitor: The agent's process monitor instance.
"""
self.process_monitor = process_monitor
@property
def router(self):
return self._router
@router.setter
def router(self, value):
self._router = value
if not self._router:
return
# enable_snat by default if it wasn't specified by plugin
self._snat_enabled = self._router.get('enable_snat', True)
@abc.abstractmethod
def delete(self, agent):
pass
@abc.abstractmethod
def process(self, agent):
"""Process updates to this router
This method is the point where the agent requests that updates be
applied to this router.
:param agent: Passes the agent in order to send RPC messages.
"""
pass
def get_ex_gw_port(self):
return self.router.get('gw_port')
def get_gw_ns_name(self):
return self.ns_name
def get_internal_device_name(self, port_id):
return (INTERNAL_DEV_PREFIX + port_id)[:self.driver.DEV_NAME_LEN]
def get_external_device_name(self, port_id):
return (EXTERNAL_DEV_PREFIX + port_id)[:self.driver.DEV_NAME_LEN]
def get_external_device_interface_name(self, ex_gw_port):
return self.get_external_device_name(ex_gw_port['id'])
class RouterInfo(BaseRouterInfo):
def __init__(self,
agent,
router_id,
router,
agent_conf,
interface_driver,
use_ipv6=False):
super(RouterInfo, self).__init__(agent, router_id, router, agent_conf,
interface_driver, use_ipv6)
self.ex_gw_port = None
self.fip_map = {}
self.pd_subnets = {}
self.floating_ips = set()
# Invoke the setter for establishing initial SNAT action
self.router = router
self.use_ipv6 = use_ipv6
ns = self.create_router_namespace_object(
router_id, agent_conf, interface_driver, use_ipv6)
self.router_namespace = ns
@ -76,8 +151,6 @@ class RouterInfo(object):
self.initialize_address_scope_iptables()
self.initialize_metadata_iptables()
self.routes = []
self.driver = interface_driver
self.process_monitor = None
# radvd is a neutron.agent.linux.ra.DaemonMonitor
self.radvd = None
self.centralized_port_forwarding_fip_set = set()
@ -85,16 +158,7 @@ class RouterInfo(object):
self.qos_gateway_ips = set()
def initialize(self, process_monitor):
"""Initialize the router on the system.
This differs from __init__ in that this method actually affects the
system creating namespaces, starting processes, etc. The other merely
initializes the python object. This separates in-memory object
initialization from methods that actually go do stuff to the system.
:param process_monitor: The agent's process monitor instance.
"""
self.process_monitor = process_monitor
super(RouterInfo, self).initialize(process_monitor)
self.radvd = ra.DaemonMonitor(self.router_id,
self.ns_name,
process_monitor,
@ -108,33 +172,9 @@ class RouterInfo(object):
return namespaces.RouterNamespace(
router_id, agent_conf, iface_driver, use_ipv6)
@property
def router(self):
return self._router
@router.setter
def router(self, value):
self._router = value
if not self._router:
return
# enable_snat by default if it wasn't specified by plugin
self._snat_enabled = self._router.get('enable_snat', True)
def is_router_master(self):
return True
def get_internal_device_name(self, port_id):
return (INTERNAL_DEV_PREFIX + port_id)[:self.driver.DEV_NAME_LEN]
def get_external_device_name(self, port_id):
return (EXTERNAL_DEV_PREFIX + port_id)[:self.driver.DEV_NAME_LEN]
def get_external_device_interface_name(self, ex_gw_port):
return self.get_external_device_name(ex_gw_port['id'])
def get_gw_ns_name(self):
return self.ns_name
def _update_routing_table(self, operation, route, namespace):
cmd = ['ip', 'route', operation, 'to', route['destination'],
'via', route['nexthop']]
@ -159,9 +199,6 @@ class RouterInfo(object):
LOG.debug("Removed route entry is '%s'", route)
self.update_routing_table('delete', route)
def get_ex_gw_port(self):
return self.router.get('gw_port')
def get_floating_ips(self):
"""Filter Floating IPs to be hosted on this agent."""
return self.router.get(lib_constants.FLOATINGIP_KEY, [])
@ -1170,13 +1207,6 @@ class RouterInfo(object):
@common_utils.exception_logger()
def process(self):
"""Process updates to this router
This method is the point where the agent requests that updates be
applied to this router.
:param agent: Passes the agent in order to send RPC messages.
"""
LOG.debug("Process updates, router %s", self.router['id'])
self.centralized_port_forwarding_fip_set = set(self.router.get(
'port_forwardings_fip_set', set()))

View File

@ -579,6 +579,10 @@ class TestDvrRouter(framework.L3AgentTestFramework):
self._add_fip_agent_gw_port_info_to_router(router,
external_gw_port)
# Router creation is delegated to router_factory. We have to
# re-register here so that factory can find override agent mode
# normally.
self.agent._register_router_cls(self.agent.router_factory)
return router
def _get_fip_agent_gw_port_for_router(

View File

@ -128,7 +128,7 @@ class QosExtensionBaseTestCase(test_agent.BasicRouterOperationsFramework):
'L3AgentExtensionAPI.get_router_info').start()
self.get_router_info.side_effect = _mock_get_router_info
self.agent_api = l3_ext_api.L3AgentExtensionAPI(None)
self.agent_api = l3_ext_api.L3AgentExtensionAPI(None, None)
self.fip_qos_ext.consume_api(self.agent_api)

View File

@ -129,7 +129,7 @@ class QosExtensionBaseTestCase(test_agent.BasicRouterOperationsFramework):
'L3AgentExtensionAPI.get_router_info').start()
self.get_router_info.side_effect = _mock_get_router_info
self.agent_api = l3_ext_api.L3AgentExtensionAPI(None)
self.agent_api = l3_ext_api.L3AgentExtensionAPI(None, None)
self.gw_ip_qos_ext.consume_api(self.agent_api)

View File

@ -91,7 +91,7 @@ class PortForwardingExtensionBaseTestCase(
'L3AgentExtensionAPI.get_router_info').start()
self.get_router_info.return_value = self.router_info
self.agent_api = l3_ext_api.L3AgentExtensionAPI(None)
self.agent_api = l3_ext_api.L3AgentExtensionAPI(None, None)
self.fip_pf_ext.consume_api(self.agent_api)
self.port_forwardings = [self.portforwarding1]

View File

@ -34,7 +34,9 @@ from testtools import matchers
from neutron.agent.common import resource_processing_queue
from neutron.agent.l3 import agent as l3_agent
from neutron.agent.l3 import dvr_edge_ha_router
from neutron.agent.l3 import dvr_edge_router as dvr_router
from neutron.agent.l3 import dvr_local_router
from neutron.agent.l3 import dvr_router_base
from neutron.agent.l3 import dvr_snat_ns
from neutron.agent.l3 import ha_router
@ -428,6 +430,67 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
self.assertEqual(len(stale_router_ids), destroy_proxy.call_count)
destroy_proxy.assert_has_calls(expected_calls, any_order=True)
def test__create_router_legacy_agent(self):
router = {'distributed': False, 'ha': False}
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router_info = agent._create_router(_uuid(), router)
self.assertEqual(legacy_router.LegacyRouter, type(router_info))
def test__create_router_ha_agent(self):
router = {'distributed': False, 'ha': True}
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router_info = agent._create_router(_uuid(), router)
self.assertEqual(ha_router.HaRouter, type(router_info))
def test__create_router_dvr_agent(self):
router = {'distributed': True, 'ha': False}
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router_info = agent._create_router(_uuid(), router)
self.assertEqual(dvr_local_router.DvrLocalRouter, type(router_info))
def test__create_router_dvr_agent_with_dvr_snat_mode(self):
router = {'distributed': True, 'ha': False}
self.conf.set_override('agent_mode',
lib_constants.L3_AGENT_MODE_DVR_SNAT)
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router_info = agent._create_router(_uuid(), router)
self.assertEqual(dvr_router.DvrEdgeRouter, type(router_info))
def test__create_router_dvr_ha_agent(self):
router = {'distributed': True, 'ha': True}
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router_info = agent._create_router(_uuid(), router)
self.assertEqual(dvr_local_router.DvrLocalRouter, type(router_info))
def test__create_router_dvr_ha_agent_with_dvr_snat_mode(self):
router = {'distributed': True, 'ha': True,
lib_constants.HA_INTERFACE_KEY: None}
self.conf.set_override('agent_mode',
lib_constants.L3_AGENT_MODE_DVR_SNAT)
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router_info = agent._create_router(_uuid(), router)
self.assertEqual(dvr_router.DvrEdgeRouter, type(router_info))
router = {'distributed': True, 'ha': True,
lib_constants.HA_INTERFACE_KEY: True}
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router_info = agent._create_router(_uuid(), router)
self.assertEqual(dvr_edge_ha_router.DvrEdgeHaRouter, type(router_info))
def test_router_info_create(self):
id = _uuid()
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)

View File

@ -18,8 +18,9 @@ import mock
from oslo_utils import uuidutils
from neutron.agent.l3 import agent
from neutron.agent.l3 import l3_agent_extension_api as l3_agent_api
from neutron.agent.l3 import router_info
from neutron.agent.l3 import router_info as l3router
from neutron.agent.linux import ip_lib
from neutron.conf.agent import common as config
from neutron.conf.agent.l3 import config as l3_config
@ -38,7 +39,7 @@ class TestL3AgentExtensionApi(base.BaseTestCase):
'agent_conf': self.conf,
'interface_driver': mock.ANY,
'use_ipv6': mock.ANY}
ri = router_info.RouterInfo(mock.Mock(), self.router_id, **ri_kwargs)
ri = l3router.RouterInfo(mock.Mock(), self.router_id, **ri_kwargs)
ri.internal_ports = ports
return {ri.router_id: ri}, ri
@ -51,7 +52,7 @@ class TestL3AgentExtensionApi(base.BaseTestCase):
'list_network_namespaces') as mock_list_netns:
mock_list_netns.return_value = []
api_object = l3_agent_api.L3AgentExtensionAPI(router_info)
api_object = l3_agent_api.L3AgentExtensionAPI(router_info, None)
router = api_object.get_router_hosting_port(port_ids[0])
mock_list_netns.assert_called_once_with()
@ -65,7 +66,7 @@ class TestL3AgentExtensionApi(base.BaseTestCase):
with mock.patch.object(ip_lib,
'list_network_namespaces') as mock_list_netns:
mock_list_netns.return_value = [ri.ns_name]
api_object = l3_agent_api.L3AgentExtensionAPI(router_info)
api_object = l3_agent_api.L3AgentExtensionAPI(router_info, None)
router = api_object.get_router_hosting_port(port_ids[0])
self.assertEqual(ri, router)
@ -75,7 +76,7 @@ class TestL3AgentExtensionApi(base.BaseTestCase):
with mock.patch.object(ip_lib,
'list_network_namespaces') as mock_list_netns:
mock_list_netns.return_value = [ri.ns_name]
api_object = l3_agent_api.L3AgentExtensionAPI(router_info)
api_object = l3_agent_api.L3AgentExtensionAPI(router_info, None)
routers = api_object.get_routers_in_project(self.project_id)
self.assertEqual([ri], routers)
@ -85,7 +86,7 @@ class TestL3AgentExtensionApi(base.BaseTestCase):
with mock.patch.object(ip_lib,
'list_network_namespaces') as mock_list_netns:
mock_list_netns.return_value = [ri.ns_name]
api_object = l3_agent_api.L3AgentExtensionAPI(router_info)
api_object = l3_agent_api.L3AgentExtensionAPI(router_info, None)
router_in_ns = api_object.is_router_in_namespace(ri.router_id)
self.assertTrue(router_in_ns)
@ -95,17 +96,32 @@ class TestL3AgentExtensionApi(base.BaseTestCase):
with mock.patch.object(ip_lib,
'list_network_namespaces') as mock_list_netns:
mock_list_netns.return_value = [uuidutils.generate_uuid()]
api_object = l3_agent_api.L3AgentExtensionAPI(router_info)
api_object = l3_agent_api.L3AgentExtensionAPI(router_info, None)
router_in_ns = api_object.is_router_in_namespace(ri.router_id)
self.assertFalse(router_in_ns)
def test_get_router_info(self):
router_info, ri = self._prepare_router_data()
api_object = l3_agent_api.L3AgentExtensionAPI(router_info)
api_object = l3_agent_api.L3AgentExtensionAPI(router_info, None)
self.assertEqual(ri, api_object.get_router_info(self.router_id))
def test_get_router_info_nonexistent(self):
router_info, ri = self._prepare_router_data()
api_object = l3_agent_api.L3AgentExtensionAPI(router_info)
api_object = l3_agent_api.L3AgentExtensionAPI(router_info, None)
self.assertIsNone(
api_object.get_router_info(uuidutils.generate_uuid()))
def test_register_router(self):
router_info, ri = self._prepare_router_data()
router_info_cls = l3router.BaseRouterInfo
router_factory = agent.RouterFactory()
api_object = l3_agent_api.L3AgentExtensionAPI(router_info,
router_factory)
self.assertIsNone(
api_object.register_router([], router_info_cls))
self.assertIsNone(
api_object.register_router(['ha'], router_info_cls))
self.assertIsNone(
api_object.register_router(['distributed'], router_info_cls))
self.assertIsNone(
api_object.register_router(['ha', 'distributed'], router_info_cls))

View File

@ -75,7 +75,7 @@ class L3LoggingExtBaseTestCase(test_agent.BasicRouterOperationsFramework):
'neutron.agent.l3.l3_agent_extension_api.'
'L3AgentExtensionAPI.get_router_info').start()
self.get_router_info.side_effect = _mock_get_router_info
self.agent_api = l3_ext_api.L3AgentExtensionAPI(None)
self.agent_api = l3_ext_api.L3AgentExtensionAPI(None, None)
mock.patch(
'neutron.manager.NeutronManager.load_class_for_provider').start()

View File

@ -0,0 +1,10 @@
---
features:
- |
A new parameter ``router_factory`` has been added to
``neutron.agent.l3.L3AgentExtensionAPI``. Developers can register
``neutron.agent.l3.agent.RouterInfo`` class and delegate it for
``RouterInfo`` creation.
Extensions can extend ``RouterInfo`` itself which correspond to each
features (ha, distribtued, ha + distributed).