diff --git a/neutron/agent/l3/agent.py b/neutron/agent/l3/agent.py index 0f9964e9a61..598e08fed62 100644 --- a/neutron/agent/l3/agent.py +++ b/neutron/agent/l3/agent.py @@ -572,8 +572,9 @@ class L3NATAgent(ha.AgentMixin, chunk = [] is_snat_agent = (self.conf.agent_mode == lib_const.L3_AGENT_MODE_DVR_SNAT) - is_dvr_only_agent = (self.conf.agent_mode == - lib_const.L3_AGENT_MODE_DVR) + is_dvr_only_agent = (self.conf.agent_mode in + [lib_const.L3_AGENT_MODE_DVR, + l3_constants.L3_AGENT_MODE_DVR_NO_EXTERNAL]) try: router_ids = self.plugin_rpc.get_router_ids(context) # We set HA network port status to DOWN to let l2 agent update it diff --git a/neutron/agent/l3/dvr_local_router.py b/neutron/agent/l3/dvr_local_router.py index 4137fc84280..c8fea766aa4 100644 --- a/neutron/agent/l3/dvr_local_router.py +++ b/neutron/agent/l3/dvr_local_router.py @@ -95,7 +95,7 @@ class DvrLocalRouter(dvr_router_base.DvrRouterBase): """ def floating_ip_added_dist(self, fip, fip_cidr): - """Add floating IP to FIP namespace.""" + """Add floating IP to respective namespace based on agent mode.""" if fip.get(n_const.DVR_SNAT_BOUND): floating_ip_status = self.add_centralized_floatingip(fip, fip_cidr) if floating_ip_status == lib_constants.FLOATINGIP_STATUS_ACTIVE: @@ -552,10 +552,12 @@ class DvrLocalRouter(dvr_router_base.DvrRouterBase): i.get('dest_host') == self.host)] def process_external(self): - ex_gw_port = self.get_ex_gw_port() - if ex_gw_port: - self.create_dvr_external_gateway_on_agent(ex_gw_port) - self.connect_rtr_2_fip() + if self.agent_conf.agent_mode != ( + n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL): + ex_gw_port = self.get_ex_gw_port() + if ex_gw_port: + self.create_dvr_external_gateway_on_agent(ex_gw_port) + self.connect_rtr_2_fip() super(DvrLocalRouter, self).process_external() def connect_rtr_2_fip(self): diff --git a/neutron/common/constants.py b/neutron/common/constants.py index 7dd4bcaee2c..4ad780d0261 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -29,6 +29,7 @@ METERING_LABEL_KEY = '_metering_labels' FLOATINGIP_AGENT_INTF_KEY = '_floatingip_agent_interfaces' SNAT_ROUTER_INTF_KEY = '_snat_router_interfaces' DVR_SNAT_BOUND = 'dvr_snat_bound' +L3_AGENT_MODE_DVR_NO_EXTERNAL = 'dvr_no_external' HA_NETWORK_NAME = 'HA network tenant %s' HA_SUBNET_NAME = 'HA subnet tenant %s' diff --git a/neutron/conf/agent/l3/config.py b/neutron/conf/agent/l3/config.py index 258f0f499db..a43a0b73746 100644 --- a/neutron/conf/agent/l3/config.py +++ b/neutron/conf/agent/l3/config.py @@ -18,6 +18,7 @@ from neutron_lib import constants from oslo_config import cfg from neutron._i18n import _ +from neutron.common import constants as common_consts from neutron.conf.agent import common as config @@ -25,7 +26,8 @@ OPTS = [ cfg.StrOpt('agent_mode', default=constants.L3_AGENT_MODE_LEGACY, choices=(constants.L3_AGENT_MODE_DVR, constants.L3_AGENT_MODE_DVR_SNAT, - constants.L3_AGENT_MODE_LEGACY), + constants.L3_AGENT_MODE_LEGACY, + common_consts.L3_AGENT_MODE_DVR_NO_EXTERNAL), help=_("The working mode for the agent. Allowed modes are: " "'legacy' - this preserves the existing behavior " "where the L3 agent is deployed on a centralized " @@ -37,7 +39,14 @@ OPTS = [ "enables centralized SNAT support in conjunction " "with DVR. This mode must be used for an L3 agent " "running on a centralized node (or in single-host " - "deployments, e.g. devstack)")), + "deployments, e.g. devstack). " + "'dvr_no_external' - this mode enables only East/West " + "DVR routing functionality for a L3 agent that runs on " + "a compute host, the North/South functionality such " + "as DNAT and SNAT will be provided by the centralized " + "network node that is running in 'dvr_snat' mode. " + "This mode should be used when there is no " + "external network connectivity on the compute host.")), cfg.PortOpt('metadata_port', default=9697, help=_("TCP Port used by Neutron metadata namespace proxy.")), diff --git a/neutron/db/l3_agentschedulers_db.py b/neutron/db/l3_agentschedulers_db.py index d2d87d679ec..e3b6167a5cf 100644 --- a/neutron/db/l3_agentschedulers_db.py +++ b/neutron/db/l3_agentschedulers_db.py @@ -24,6 +24,7 @@ from sqlalchemy import or_ from neutron._i18n import _, _LI from neutron.agent.common import utils as agent_utils +from neutron.common import constants as l_consts from neutron.common import utils as n_utils from neutron.db import agentschedulers_db from neutron.db.models import agent as agent_model @@ -108,7 +109,8 @@ class L3AgentSchedulerDbMixin(l3agentscheduler.L3AgentSchedulerPluginBase, agent_mode = self._get_agent_mode(agent) - if agent_mode == constants.L3_AGENT_MODE_DVR: + if agent_mode in [constants.L3_AGENT_MODE_DVR, + l_consts.L3_AGENT_MODE_DVR_NO_EXTERNAL]: raise l3agentscheduler.DVRL3CannotAssignToDvrAgent() if (agent_mode == constants.L3_AGENT_MODE_LEGACY and @@ -194,11 +196,11 @@ class L3AgentSchedulerDbMixin(l3agentscheduler.L3AgentSchedulerPluginBase, """ agent = self._get_agent(context, agent_id) agent_mode = self._get_agent_mode(agent) - if agent_mode == constants.L3_AGENT_MODE_DVR: + if agent_mode in [constants.L3_AGENT_MODE_DVR, + l_consts.L3_AGENT_MODE_DVR_NO_EXTERNAL]: raise l3agentscheduler.DVRL3CannotRemoveFromDvrAgent() self._unbind_router(context, router_id, agent_id) - router = self.get_router(context, router_id) plugin = directory.get_plugin(plugin_constants.L3) if router.get('ha'): @@ -416,8 +418,9 @@ class L3AgentSchedulerDbMixin(l3agentscheduler.L3AgentSchedulerPluginBase, ignore_admin_state=False): """Get the valid l3 agents for the router from a list of l3_agents. - It will not return agents in 'dvr' mode for a dvr router as dvr - routers are not explicitly scheduled to l3 agents on compute nodes + It will not return agents in 'dvr' mode or in 'dvr_no_external' mode + for a dvr router as dvr routers are not explicitly scheduled to l3 + agents on compute nodes """ candidates = [] is_router_distributed = sync_router.get('distributed', False) @@ -431,6 +434,7 @@ class L3AgentSchedulerDbMixin(l3agentscheduler.L3AgentSchedulerPluginBase, agent_mode = agent_conf.get(constants.L3_AGENT_MODE, constants.L3_AGENT_MODE_LEGACY) if (agent_mode == constants.L3_AGENT_MODE_DVR or + agent_mode == l_consts.L3_AGENT_MODE_DVR_NO_EXTERNAL or (agent_mode == constants.L3_AGENT_MODE_LEGACY and is_router_distributed)): continue diff --git a/neutron/db/l3_dvr_db.py b/neutron/db/l3_dvr_db.py index 304fa986a4d..26e631764cb 100644 --- a/neutron/db/l3_dvr_db.py +++ b/neutron/db/l3_dvr_db.py @@ -714,22 +714,56 @@ class _DVRAgentInterfaceMixin(object): # All unbound ports with floatingip irrespective of # the device owner should be included as valid ports # and updated. - if (port[portbindings.HOST_ID] == host or port_in_migration or + port_host = port[portbindings.HOST_ID] + if (port_host == host or port_in_migration or self._is_unbound_port(port)): port_dict.update({port['id']: port}) + if port_host and port_host != host: + # Consider the ports where the portbinding host and + # request host does not match. + l3_agent_on_host = self.get_l3_agents( + context, + filters={'host': [port_host]}) + if len(l3_agent_on_host): + l3_agent_mode = self._get_agent_mode( + l3_agent_on_host[0]) + # If the agent requesting is dvr_snat but + # the portbinding host resides in dvr_no_external + # agent then include the port. + requesting_agent_mode = self._get_agent_mode(agent) + if (l3_agent_mode == ( + l3_const.L3_AGENT_MODE_DVR_NO_EXTERNAL) and + requesting_agent_mode == ( + const.L3_AGENT_MODE_DVR_SNAT)): + port['agent'] = ( + l3_const.L3_AGENT_MODE_DVR_NO_EXTERNAL) + port_dict.update({port['id']: port}) # Add the port binding host to the floatingip dictionary for fip in floating_ips: vm_port = port_dict.get(fip['port_id'], None) if vm_port: - fip['host'] = self._get_dvr_service_port_hostid( - context, fip['port_id'], port=vm_port) - fip['dest_host'] = ( - self._get_dvr_migrating_service_port_hostid( - context, fip['port_id'], port=vm_port)) + port_host = vm_port[portbindings.HOST_ID] + if port_host: + fip['host'] = port_host + fip['dest_host'] = ( + self._get_dvr_migrating_service_port_hostid( + context, fip['port_id'], port=vm_port)) + vm_port_agent_mode = vm_port.get('agent', None) + if vm_port_agent_mode == ( + l3_const.L3_AGENT_MODE_DVR_NO_EXTERNAL): + # For floatingip configured on ports that + # reside on 'dvr_no_external' agent, get rid of + # the fip host binding since it would be created + # in the 'dvr_snat' agent. + fip['host'] = None + else: + # If no port-binding assign the fip['host'] + # value to None. + fip['host'] = None # Handle the case were there is no host binding # for the private ports that are associated with # floating ip. - if not fip['host']: + if not fip['host'] or fip['host'] is None: fip[l3_const.DVR_SNAT_BOUND] = True routers_dict = self._process_routers(context, routers, agent) self._process_floating_ips_dvr(context, routers_dict, @@ -961,6 +995,10 @@ class L3_NAT_with_dvr_db_mixin(_DVRAgentInterfaceMixin, self._notify_floating_ip_change(context, floating_ip) return floating_ip + def get_dvr_agent_on_host(self, context, fip_host): + agent_filters = {'host': [fip_host]} + return self.get_l3_agents(context, filters=agent_filters) + def _notify_floating_ip_change(self, context, floating_ip): router_id = floating_ip['router_id'] fixed_port_id = floating_ip['port_id'] @@ -981,6 +1019,17 @@ class L3_NAT_with_dvr_db_mixin(_DVRAgentInterfaceMixin, host = self._get_dvr_service_port_hostid(context, fixed_port_id) dest_host = self._get_dvr_migrating_service_port_hostid( context, fixed_port_id) + if host is not None: + l3_agent_on_host = self.get_dvr_agent_on_host( + context, host) + agent_mode = self._get_agent_mode(l3_agent_on_host[0]) + if agent_mode == l3_const.L3_AGENT_MODE_DVR_NO_EXTERNAL: + # If the agent hosting the fixed port is in + # 'dvr_no_external' mode, then set the host to None, + # since we would be centralizing the floatingip for + # those fixed_ports. + host = None + if host is not None: self.l3_rpc_notifier.routers_updated_on_host( context, [router_id], host) @@ -988,7 +1037,11 @@ class L3_NAT_with_dvr_db_mixin(_DVRAgentInterfaceMixin, self.l3_rpc_notifier.routers_updated_on_host( context, [router_id], dest_host) else: - self.notify_router_updated(context, router_id) + centralized_agent_list = self.list_l3_agents_hosting_router( + context, router_id)['agents'] + for agent in centralized_agent_list: + self.l3_rpc_notifier.routers_updated_on_host( + context, [router_id], agent['host']) else: self.notify_router_updated(context, router_id) diff --git a/neutron/db/l3_dvrscheduler_db.py b/neutron/db/l3_dvrscheduler_db.py index 03f1d17ad0e..294759044f3 100644 --- a/neutron/db/l3_dvrscheduler_db.py +++ b/neutron/db/l3_dvrscheduler_db.py @@ -23,6 +23,7 @@ from neutron_lib.plugins import directory from oslo_log import log as logging from sqlalchemy import or_ +from neutron.common import constants as l3_consts from neutron.common import utils as n_utils from neutron.db import agentschedulers_db @@ -44,7 +45,9 @@ class L3_DVRsch_db_mixin(l3agent_sch_db.L3AgentSchedulerDbMixin): distributed manner across Compute Nodes without a centralized element. This includes E/W traffic between VMs on the same Compute Node. - North/South traffic for Floating IPs (FIP N/S): this is supported on the - distributed routers on Compute Nodes without any centralized element. + distributed routers on Compute Nodes when there is external network + connectivity and on centralized nodes when the port is not bound or when + the agent is configured as 'dvr_no_external'. - North/South traffic for SNAT (SNAT N/S): this is supported via a centralized element that handles the SNAT traffic. @@ -288,8 +291,10 @@ class L3_DVRsch_db_mixin(l3agent_sch_db.L3AgentSchedulerDbMixin): # dvr routers are not explicitly scheduled to agents on hosts with # dvr serviceable ports, so need special handling - if self._get_agent_mode(agent_db) in [n_const.L3_AGENT_MODE_DVR, - n_const.L3_AGENT_MODE_DVR_SNAT]: + if (self._get_agent_mode(agent_db) in + [n_const.L3_AGENT_MODE_DVR, + l3_consts.L3_AGENT_MODE_DVR_NO_EXTERNAL, + n_const.L3_AGENT_MODE_DVR_SNAT]): if not router_ids: result_set |= set(self._get_dvr_router_ids_for_host( context, agent_db['host'])) diff --git a/neutron/db/l3_hamode_db.py b/neutron/db/l3_hamode_db.py index eebbf037890..d9b134ed15b 100644 --- a/neutron/db/l3_hamode_db.py +++ b/neutron/db/l3_hamode_db.py @@ -641,19 +641,22 @@ class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin, self._populate_mtu_and_subnets_for_ports(context, interfaces) - # If this is a DVR+HA router, but the agent is question is in 'dvr' - # mode (as opposed to 'dvr_snat'), then we want to always return it - # even though it's missing the '_ha_interface' key. + # If this is a DVR+HA router, but the agent in question is in 'dvr' + # or 'dvr_no_external' mode (as opposed to 'dvr_snat'), then we want to + # always return it even though it's missing the '_ha_interface' key. return [r for r in list(routers_dict.values()) if (agent_mode == constants.L3_AGENT_MODE_DVR or + agent_mode == n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL or not r.get('ha') or r.get(constants.HA_INTERFACE_KEY))] @log_helpers.log_method_call def get_ha_sync_data_for_host(self, context, host, agent, router_ids=None, active=None): agent_mode = self._get_agent_mode(agent) - dvr_agent_mode = (agent_mode in [constants.L3_AGENT_MODE_DVR_SNAT, - constants.L3_AGENT_MODE_DVR]) + dvr_agent_mode = ( + agent_mode in [constants.L3_AGENT_MODE_DVR_SNAT, + constants.L3_AGENT_MODE_DVR, + n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL]) if (dvr_agent_mode and n_utils.is_extension_supported( self, constants.L3_DISTRIBUTED_EXT_ALIAS)): # DVR has to be handled differently diff --git a/neutron/tests/functional/agent/l3/test_dvr_router.py b/neutron/tests/functional/agent/l3/test_dvr_router.py index ef31193141c..4bfa312f2be 100644 --- a/neutron/tests/functional/agent/l3/test_dvr_router.py +++ b/neutron/tests/functional/agent/l3/test_dvr_router.py @@ -449,6 +449,7 @@ class TestDvrRouter(framework.L3AgentTestFramework): agent=None, extra_routes=False, enable_floating_ip=True, + enable_centralized_fip=False, **kwargs): if not agent: agent = self.agent @@ -469,6 +470,11 @@ class TestDvrRouter(framework.L3AgentTestFramework): if snat_bound_fip: floating_ip[n_const.DVR_SNAT_BOUND] = True + if enable_floating_ip and enable_centralized_fip: + # For centralizing the fip, we are emulating the legacy + # router behavior were the fip dict does not contain any + # host information. + floating_ip['host'] = None if enable_gw: external_gw_port = router['gw_port'] router['gw_port'][portbindings.HOST_ID] = agent.conf.host @@ -479,7 +485,6 @@ class TestDvrRouter(framework.L3AgentTestFramework): # dependent on the agent_type. if enable_floating_ip: floating_ip = router['_floatingips'][0] - floating_ip['host'] = agent.conf.host floating_ip['floating_network_id'] = ( external_gw_port['network_id']) floating_ip['port_id'] = internal_ports[0]['id'] @@ -686,7 +691,8 @@ class TestDvrRouter(framework.L3AgentTestFramework): self.assertTrue(ip_lib.device_exists( device_name, namespace=router.ns_name)) - # In the router namespace, check the iptables rules are set correctly + # In the router namespace, check the iptables rules are set + # correctly for fip in floating_ips: expected_rules = router.floating_forward_rules(fip) self._assert_iptables_rules_exist( @@ -1001,6 +1007,27 @@ class TestDvrRouter(framework.L3AgentTestFramework): self._assert_iptables_rules_exist( router1.snat_iptables_manager, 'nat', expected_rules) + def test_floating_ip_not_deployed_on_dvr_no_external_agent(self): + """Test to check floating ips not configured for dvr_no_external.""" + self.agent.conf.agent_mode = n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL + router_info = self.generate_dvr_router_info( + enable_floating_ip=True, enable_centralized_fip=True) + router1 = self.manage_router(self.agent, router_info) + centralized_floatingips = router_info[lib_constants.FLOATINGIP_KEY] + # For private ports hosted in dvr_no_fip agent, the floatingip + # dict will contain the fip['host'] key, but the value will always + # be None to emulate the legacy router. + self.assertIsNone(centralized_floatingips[0]['host']) + self.assertTrue(self._namespace_exists(router1.ns_name)) + fip_ns = router1.fip_ns.get_name() + self.assertFalse(self._namespace_exists(fip_ns)) + # If fips are centralized then, the DNAT rules are only + # configured in the SNAT Namespace and not in the router-ns. + for fip in centralized_floatingips: + expected_rules = router1.floating_forward_rules(fip) + self.assertFalse(self._assert_iptables_rules_exist( + router1.iptables_manager, 'nat', expected_rules)) + def test_dvr_router_snat_namespace_with_interface_remove(self): """Test to validate the snat namespace with interface remove. diff --git a/neutron/tests/functional/services/l3_router/test_l3_dvr_router_plugin.py b/neutron/tests/functional/services/l3_router/test_l3_dvr_router_plugin.py index 2cd2b848599..c71a983a395 100644 --- a/neutron/tests/functional/services/l3_router/test_l3_dvr_router_plugin.py +++ b/neutron/tests/functional/services/l3_router/test_l3_dvr_router_plugin.py @@ -248,7 +248,8 @@ class L3DvrTestCase(L3DvrTestCaseBase): self.l3_plugin._get_agent_gw_ports_exist_for_network( self.context, ext_net_id, "", self.l3_agent['id'])) - def _test_create_floating_ip_agent_notification(self, dvr=True): + def _test_create_floating_ip_agent_notification( + self, dvr=True, test_agent_mode=constants.L3_AGENT_MODE_DVR): with self.subnet() as ext_subnet,\ self.subnet(cidr='20.0.0.0/24') as int_subnet,\ self.port(subnet=int_subnet, @@ -258,7 +259,7 @@ class L3DvrTestCase(L3DvrTestCaseBase): {'port': {portbindings.HOST_ID: 'host1'}}) # and create l3 agents on corresponding hosts helpers.register_l3_agent(host='host1', - agent_mode=constants.L3_AGENT_MODE_DVR) + agent_mode=test_agent_mode) # make net external ext_net_id = ext_subnet['subnet']['network_id'] @@ -273,7 +274,6 @@ class L3DvrTestCase(L3DvrTestCaseBase): self.l3_plugin.add_router_interface( self.context, router['id'], {'subnet_id': int_subnet['subnet']['id']}) - floating_ip = {'floating_network_id': ext_net_id, 'router_id': router['id'], 'port_id': int_port['port']['id'], @@ -284,10 +284,27 @@ class L3DvrTestCase(L3DvrTestCaseBase): self.l3_plugin.create_floatingip( self.context, {'floatingip': floating_ip}) if dvr: - l3_notif.routers_updated_on_host.assert_called_once_with( - self.context, [router['id']], - 'host1') - self.assertFalse(l3_notif.routers_updated.called) + if test_agent_mode == ( + n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL): + if router['ha']: + expected_calls = [ + mock.call(self.context, + [router['id']], 'host0'), + mock.call(self.context, + [router['id']], 'standby')] + l3_notif.routers_updated_on_host.assert_has_calls( + expected_calls, any_order=True) + self.assertFalse(l3_notif.routers_updated.called) + if not router['ha']: + l3_notif.routers_updated_on_host.\ + assert_called_once_with( + self.context, [router['id']], 'host0') + self.assertFalse(l3_notif.routers_updated.called) + else: + l3_notif.routers_updated_on_host.\ + assert_called_once_with( + self.context, [router['id']], 'host1') + self.assertFalse(l3_notif.routers_updated.called) else: l3_notif.routers_updated.assert_called_once_with( self.context, [router['id']], None) @@ -297,10 +314,17 @@ class L3DvrTestCase(L3DvrTestCaseBase): def test_create_floating_ip_agent_notification(self): self._test_create_floating_ip_agent_notification() + def test_create_floating_ip_agent_notification_for_dvr_no_external_agent( + self): + agent_mode = n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL + self._test_create_floating_ip_agent_notification( + test_agent_mode=agent_mode) + def test_create_floating_ip_agent_notification_non_dvr(self): self._test_create_floating_ip_agent_notification(dvr=False) - def _test_update_floating_ip_agent_notification(self, dvr=True): + def _test_update_floating_ip_agent_notification( + self, dvr=True, test_agent_mode=constants.L3_AGENT_MODE_DVR): with self.subnet() as ext_subnet,\ self.subnet(cidr='20.0.0.0/24') as int_subnet1,\ self.subnet(cidr='30.0.0.0/24') as int_subnet2,\ @@ -317,9 +341,9 @@ class L3DvrTestCase(L3DvrTestCaseBase): {'port': {portbindings.HOST_ID: 'host2'}}) # and create l3 agents on corresponding hosts helpers.register_l3_agent(host='host1', - agent_mode=constants.L3_AGENT_MODE_DVR) + agent_mode=test_agent_mode) helpers.register_l3_agent(host='host2', - agent_mode=constants.L3_AGENT_MODE_DVR) + agent_mode=test_agent_mode) # make net external ext_net_id = ext_subnet['subnet']['network_id'] @@ -356,14 +380,45 @@ class L3DvrTestCase(L3DvrTestCaseBase): self.context, floating_ip['id'], {'floatingip': updated_floating_ip}) if dvr: - self.assertEqual( - 2, l3_notif.routers_updated_on_host.call_count) - expected_calls = [ - mock.call(self.context, [router1['id']], 'host1'), - mock.call(self.context, [router2['id']], 'host2')] - l3_notif.routers_updated_on_host.assert_has_calls( - expected_calls) - self.assertFalse(l3_notif.routers_updated.called) + if test_agent_mode == ( + n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL): + if router1['ha'] and router2['ha']: + self.assertEqual( + 4, + l3_notif.routers_updated_on_host.call_count) + expected_calls = [ + mock.call(self.context, + [router1['id']], 'host0'), + mock.call(self.context, + [router1['id']], 'standby'), + mock.call(self.context, + [router2['id']], 'host0'), + mock.call(self.context, + [router2['id']], 'standby')] + l3_notif.routers_updated_on_host.assert_has_calls( + expected_calls, any_order=True) + self.assertFalse(l3_notif.routers_updated.called) + else: + self.assertEqual( + 2, + l3_notif.routers_updated_on_host.call_count) + expected_calls = [ + mock.call(self.context, + [router1['id']], 'host0'), + mock.call(self.context, + [router2['id']], 'host0')] + l3_notif.routers_updated_on_host.assert_has_calls( + expected_calls) + self.assertFalse(l3_notif.routers_updated.called) + else: + self.assertEqual( + 2, l3_notif.routers_updated_on_host.call_count) + expected_calls = [ + mock.call(self.context, [router1['id']], 'host1'), + mock.call(self.context, [router2['id']], 'host2')] + l3_notif.routers_updated_on_host.assert_has_calls( + expected_calls) + self.assertFalse(l3_notif.routers_updated.called) else: self.assertEqual( 2, l3_notif.routers_updated.call_count) @@ -377,10 +432,17 @@ class L3DvrTestCase(L3DvrTestCaseBase): def test_update_floating_ip_agent_notification(self): self._test_update_floating_ip_agent_notification() + def test_update_floating_ip_agent_notification_with_dvr_no_external_agents( + self): + agent_mode = n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL + self._test_update_floating_ip_agent_notification( + test_agent_mode=agent_mode) + def test_update_floating_ip_agent_notification_non_dvr(self): self._test_update_floating_ip_agent_notification(dvr=False) - def _test_delete_floating_ip_agent_notification(self, dvr=True): + def _test_delete_floating_ip_agent_notification( + self, dvr=True, test_agent_mode=constants.L3_AGENT_MODE_DVR): with self.subnet() as ext_subnet,\ self.subnet(cidr='20.0.0.0/24') as int_subnet,\ self.port(subnet=int_subnet, @@ -390,7 +452,7 @@ class L3DvrTestCase(L3DvrTestCaseBase): {'port': {portbindings.HOST_ID: 'host1'}}) # and create l3 agents on corresponding hosts helpers.register_l3_agent(host='host1', - agent_mode=constants.L3_AGENT_MODE_DVR) + agent_mode=test_agent_mode) # make net external ext_net_id = ext_subnet['subnet']['network_id'] self._update('networks', ext_net_id, @@ -417,10 +479,27 @@ class L3DvrTestCase(L3DvrTestCaseBase): self.l3_plugin.delete_floatingip( self.context, floating_ip['id']) if dvr: - l3_notif.routers_updated_on_host.assert_called_once_with( - self.context, [router['id']], - 'host1') - self.assertFalse(l3_notif.routers_updated.called) + if test_agent_mode == ( + n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL): + if router['ha']: + expected_calls = [ + mock.call(self.context, + [router['id']], 'host0'), + mock.call(self.context, + [router['id']], 'standby')] + l3_notif.routers_updated_on_host.assert_has_calls( + expected_calls, any_order=True) + self.assertFalse(l3_notif.routers_updated.called) + else: + l3_notif.routers_updated_on_host.\ + assert_called_once_with( + self.context, [router['id']], 'host0') + self.assertFalse(l3_notif.routers_updated.called) + else: + l3_notif.routers_updated_on_host.\ + assert_called_once_with( + self.context, [router['id']], 'host1') + self.assertFalse(l3_notif.routers_updated.called) else: l3_notif.routers_updated.assert_called_once_with( self.context, [router['id']], None) @@ -430,6 +509,12 @@ class L3DvrTestCase(L3DvrTestCaseBase): def test_delete_floating_ip_agent_notification(self): self._test_delete_floating_ip_agent_notification() + def test_delete_floating_ip_agent_notification_with_dvr_no_external_agents( + self): + agent_mode = n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL + self._test_delete_floating_ip_agent_notification( + test_agent_mode=agent_mode) + def test_delete_floating_ip_agent_notification_non_dvr(self): self._test_delete_floating_ip_agent_notification(dvr=False) @@ -595,10 +680,77 @@ class L3DvrTestCase(L3DvrTestCaseBase): self.context, {'floatingip': floating_ip}) expected_routers_updated_calls = [ mock.call(self.context, mock.ANY, HOST1), - mock.call(self.context, mock.ANY, HOST2)] + mock.call(self.context, mock.ANY, HOST2), + mock.call(self.context, mock.ANY, 'host0')] l3_notifier.routers_updated_on_host.assert_has_calls( expected_routers_updated_calls) - self.assertTrue(l3_notifier.routers_updated.called) + self.assertFalse(l3_notifier.routers_updated.called) + router_info = ( + self.l3_plugin.list_active_sync_routers_on_active_l3_agent( + self.context, self.l3_agent['host'], [router['id']])) + floatingips = router_info[0][constants.FLOATINGIP_KEY] + self.assertTrue(floatingips[0][n_const.DVR_SNAT_BOUND]) + + def test_dvr_router_centralized_floating_ip(self): + HOST1 = 'host1' + helpers.register_l3_agent( + host=HOST1, agent_mode=n_const.L3_AGENT_MODE_DVR_NO_EXTERNAL) + router = self._create_router(ha=False) + private_net1 = self._make_network(self.fmt, 'net1', True) + kwargs = {'arg_list': (external_net.EXTERNAL,), + external_net.EXTERNAL: True} + ext_net = self._make_network(self.fmt, '', True, **kwargs) + self._make_subnet( + self.fmt, ext_net, '10.20.0.1', '10.20.0.0/24', + ip_version=4, enable_dhcp=True) + self.l3_plugin.schedule_router(self.context, + router['id'], + candidates=[self.l3_agent]) + + # Set gateway to router + self.l3_plugin._update_router_gw_info( + self.context, router['id'], + {'network_id': ext_net['network']['id']}) + private_subnet1 = self._make_subnet( + self.fmt, + private_net1, + '10.1.0.1', + cidr='10.1.0.0/24', + ip_version=4, + enable_dhcp=True) + with self.port( + subnet=private_subnet1, + device_owner=DEVICE_OWNER_COMPUTE) as int_port1: + self.l3_plugin.add_router_interface( + self.context, router['id'], + {'subnet_id': private_subnet1['subnet']['id']}) + router_handle = ( + self.l3_plugin.list_active_sync_routers_on_active_l3_agent( + self.context, self.l3_agent['host'], [router['id']])) + self.assertEqual(self.l3_agent['host'], + router_handle[0]['gw_port_host']) + with mock.patch.object(self.l3_plugin, + '_l3_rpc_notifier') as l3_notifier: + vm_port = self.core_plugin.update_port( + self.context, int_port1['port']['id'], + {'port': {portbindings.HOST_ID: HOST1}}) + self.assertEqual( + 1, l3_notifier.routers_updated_on_host.call_count) + # Next we can try to associate the floatingip to the + # VM port + floating_ip = {'floating_network_id': ext_net['network']['id'], + 'router_id': router['id'], + 'port_id': vm_port['id'], + 'tenant_id': vm_port['tenant_id']} + floating_ip = self.l3_plugin.create_floatingip( + self.context, {'floatingip': floating_ip}) + + expected_routers_updated_calls = [ + mock.call(self.context, mock.ANY, HOST1), + mock.call(self.context, mock.ANY, 'host0')] + l3_notifier.routers_updated_on_host.assert_has_calls( + expected_routers_updated_calls) + self.assertFalse(l3_notifier.routers_updated.called) router_info = ( self.l3_plugin.list_active_sync_routers_on_active_l3_agent( self.context, self.l3_agent['host'], [router['id']])) @@ -733,10 +885,11 @@ class L3DvrTestCase(L3DvrTestCaseBase): expected_calls) expected_routers_updated_calls = [ mock.call(self.context, mock.ANY, HOST1), - mock.call(self.context, mock.ANY, HOST2)] + mock.call(self.context, mock.ANY, HOST2), + mock.call(self.context, mock.ANY, 'host0')] l3_notifier.routers_updated_on_host.assert_has_calls( expected_routers_updated_calls) - self.assertTrue(l3_notifier.routers_updated.called) + self.assertFalse(l3_notifier.routers_updated.called) router_info = ( self.l3_plugin.list_active_sync_routers_on_active_l3_agent( self.context, self.l3_agent['host'], [router['id']])) @@ -875,10 +1028,11 @@ class L3DvrTestCase(L3DvrTestCaseBase): l3_notifier.add_arp_entry.assert_has_calls( expected_calls) expected_routers_updated_calls = [ - mock.call(self.context, mock.ANY, HOST1)] + mock.call(self.context, mock.ANY, HOST1), + mock.call(self.context, mock.ANY, 'host0')] l3_notifier.routers_updated_on_host.assert_has_calls( expected_routers_updated_calls) - self.assertTrue(l3_notifier.routers_updated.called) + self.assertFalse(l3_notifier.routers_updated.called) def test_update_vm_port_host_router_update(self): # register l3 agents in dvr mode in addition to existing dvr_snat agent diff --git a/releasenotes/notes/dvr-configure-centralized-floatingip-with-new-agent-type-05361f1f78853cf7.yaml b/releasenotes/notes/dvr-configure-centralized-floatingip-with-new-agent-type-05361f1f78853cf7.yaml new file mode 100644 index 00000000000..6c84828154b --- /dev/null +++ b/releasenotes/notes/dvr-configure-centralized-floatingip-with-new-agent-type-05361f1f78853cf7.yaml @@ -0,0 +1,21 @@ +--- +prelude: > + A new agent_mode(``dvr_no_external``) for DVR routers has been added + to allow the server to configure Floating IPs associated with DVR at + the centralized node. +features: + - | + A new DVR agent type ``dvr_no_external`` has been introduced with this + release. This agent type allows the Floating IPs (DNAT/North-South routing) + to be centralized while the East/West routing is still distributed. +issues: + - | + There can be a mixture of ``dvr`` agents and ``dvr_no_external`` agents. + But please avoid any VM with Floating IP migration between a ``dvr`` agent + and a ``dvr_no_external`` agent. All VM ports with Floating IPs should be + migrated to same agent_mode. + This would be one of the restrictions. +upgrade: + - | + A new DVR agent mode of ``dvr_no_external`` was added. Changing between + this mode and ``dvr`` is a disruptive operation to the dataplane.