diff --git a/neutron/agent/l3/agent.py b/neutron/agent/l3/agent.py index d1ac187e264..698eb5029d0 100644 --- a/neutron/agent/l3/agent.py +++ b/neutron/agent/l3/agent.py @@ -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( diff --git a/neutron/agent/l3/l3_agent_extension_api.py b/neutron/agent/l3/l3_agent_extension_api.py index 14b45032dac..3a96a37aabc 100644 --- a/neutron/agent/l3/l3_agent_extension_api.py +++ b/neutron/agent/l3/l3_agent_extension_api.py @@ -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) diff --git a/neutron/agent/l3/router_info.py b/neutron/agent/l3/router_info.py index 59a28ed9b06..79d75f1c446 100644 --- a/neutron/agent/l3/router_info.py +++ b/neutron/agent/l3/router_info.py @@ -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())) diff --git a/neutron/tests/functional/agent/l3/test_dvr_router.py b/neutron/tests/functional/agent/l3/test_dvr_router.py index 6a35e065f86..55825947966 100644 --- a/neutron/tests/functional/agent/l3/test_dvr_router.py +++ b/neutron/tests/functional/agent/l3/test_dvr_router.py @@ -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( diff --git a/neutron/tests/unit/agent/l3/extensions/qos/test_fip.py b/neutron/tests/unit/agent/l3/extensions/qos/test_fip.py index 14b7867229d..df107b44fef 100644 --- a/neutron/tests/unit/agent/l3/extensions/qos/test_fip.py +++ b/neutron/tests/unit/agent/l3/extensions/qos/test_fip.py @@ -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) diff --git a/neutron/tests/unit/agent/l3/extensions/qos/test_gateway_ip.py b/neutron/tests/unit/agent/l3/extensions/qos/test_gateway_ip.py index 33872851338..e53f222a27e 100644 --- a/neutron/tests/unit/agent/l3/extensions/qos/test_gateway_ip.py +++ b/neutron/tests/unit/agent/l3/extensions/qos/test_gateway_ip.py @@ -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) diff --git a/neutron/tests/unit/agent/l3/extensions/test_port_forwarding.py b/neutron/tests/unit/agent/l3/extensions/test_port_forwarding.py index 4840b85261e..4c7a9e201e3 100644 --- a/neutron/tests/unit/agent/l3/extensions/test_port_forwarding.py +++ b/neutron/tests/unit/agent/l3/extensions/test_port_forwarding.py @@ -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] diff --git a/neutron/tests/unit/agent/l3/test_agent.py b/neutron/tests/unit/agent/l3/test_agent.py index 4e23845e647..95483072450 100644 --- a/neutron/tests/unit/agent/l3/test_agent.py +++ b/neutron/tests/unit/agent/l3/test_agent.py @@ -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) diff --git a/neutron/tests/unit/agent/l3/test_l3_agent_extension_api.py b/neutron/tests/unit/agent/l3/test_l3_agent_extension_api.py index 4b943d936c2..58c0486e7ec 100644 --- a/neutron/tests/unit/agent/l3/test_l3_agent_extension_api.py +++ b/neutron/tests/unit/agent/l3/test_l3_agent_extension_api.py @@ -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)) diff --git a/neutron/tests/unit/services/logapi/agent/l3/test_base.py b/neutron/tests/unit/services/logapi/agent/l3/test_base.py index 494b0127927..70e62930aa2 100644 --- a/neutron/tests/unit/services/logapi/agent/l3/test_base.py +++ b/neutron/tests/unit/services/logapi/agent/l3/test_base.py @@ -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() diff --git a/releasenotes/notes/l3-agent-extensions-register-router-factory-46a86f845895f4f6.yaml b/releasenotes/notes/l3-agent-extensions-register-router-factory-46a86f845895f4f6.yaml new file mode 100644 index 00000000000..91ed977820d --- /dev/null +++ b/releasenotes/notes/l3-agent-extensions-register-router-factory-46a86f845895f4f6.yaml @@ -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).