From 9515c771e742a5b6d29b17f84f49a0b39706489b Mon Sep 17 00:00:00 2001 From: Swaminathan Vasudevan Date: Wed, 19 Jul 2017 12:13:43 -0700 Subject: [PATCH] DVR: Provide options for DVR North/South routing centralized DVR supports both East/West and North/South routing. While the SNAT is centralized the DNAT is mostly distributed. There are certain circumstances where the DNAT might be centralized when the ports are unbound. In order to have a well defined behavior and when there are no external network connectivity available in the compute host, the DNAT functionality is centralized. In order to achieve this we are introducing a new agent type option 'dvr_no_external' to centralize the DNAT. This new L3 agent type ('dvr_no_external') would only allow the East/West routing to occur in the compute host and the DNAT or Floating IP will be configured in the centralized network node. Change-Id: Ia5d7336e478e0fa5ba62b7ae5ed0c56656116d94 Partial-Bug: #1667877 --- neutron/agent/l3/agent.py | 5 +- neutron/agent/l3/dvr_local_router.py | 12 +- neutron/common/constants.py | 1 + neutron/conf/agent/l3/config.py | 13 +- neutron/db/l3_agentschedulers_db.py | 14 +- neutron/db/l3_dvr_db.py | 69 +++++- neutron/db/l3_dvrscheduler_db.py | 11 +- neutron/db/l3_hamode_db.py | 13 +- .../functional/agent/l3/test_dvr_router.py | 31 ++- .../l3_router/test_l3_dvr_router_plugin.py | 214 +++++++++++++++--- ...-with-new-agent-type-05361f1f78853cf7.yaml | 21 ++ 11 files changed, 342 insertions(+), 62 deletions(-) create mode 100644 releasenotes/notes/dvr-configure-centralized-floatingip-with-new-agent-type-05361f1f78853cf7.yaml 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.