diff --git a/neutron/common/utils.py b/neutron/common/utils.py index 46aa899d43f..c7e8f9ebbc7 100644 --- a/neutron/common/utils.py +++ b/neutron/common/utils.py @@ -254,6 +254,21 @@ def get_other_dvr_serviced_device_owners(): n_const.DEVICE_OWNER_DHCP] +def get_dvr_allowed_address_pair_device_owners(): + """Return device_owner names for allowed_addr_pair ports serviced by DVR + + This just returns the device owners that are used by the + allowed_address_pair ports. Right now only the device_owners shown + below are used by the allowed_address_pair ports. + Later if other device owners are used for allowed_address_pairs those + device_owners should be added to the list below. + """ + # TODO(Swami): Convert these methods to constants. + # Add the constants variable to the neutron-lib + return [n_const.DEVICE_OWNER_LOADBALANCER, + n_const.DEVICE_OWNER_LOADBALANCERV2] + + def is_dvr_serviced(device_owner): """Check if the port need to be serviced by DVR diff --git a/neutron/db/l3_dvr_db.py b/neutron/db/l3_dvr_db.py index 4a278e5b5a5..114a1c98675 100644 --- a/neutron/db/l3_dvr_db.py +++ b/neutron/db/l3_dvr_db.py @@ -29,9 +29,11 @@ from neutron.callbacks import registry from neutron.callbacks import resources from neutron.common import constants as l3_const from neutron.common import utils as n_utils +from neutron.db.allowed_address_pairs import models as addr_pair_db from neutron.db import l3_agentschedulers_db as l3_sched_db from neutron.db import l3_attrs_db from neutron.db import l3_db +from neutron.db import models_v2 from neutron.extensions import l3 from neutron.extensions import portbindings from neutron.ipam import utils as ipam_utils @@ -204,6 +206,17 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin, return super(L3_NAT_with_dvr_db_mixin, self)._get_device_owner(context, router) + def _get_ports_for_allowed_address_pair_ip( + self, context, network_id, fixed_ip): + """Return all active ports associated with the allowed_addr_pair ip.""" + query = context.session.query( + models_v2.Port).filter( + models_v2.Port.id == addr_pair_db.AllowedAddressPair.port_id, + addr_pair_db.AllowedAddressPair.ip_address == fixed_ip, + models_v2.Port.network_id == network_id, + models_v2.Port.admin_state_up == True) + return query.all() + def _update_fip_assoc(self, context, fip, floatingip_db, external_port): """Override to create floating agent gw port for DVR. @@ -234,6 +247,52 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin, admin_ctx, external_port['network_id'], hostid)) LOG.debug("FIP Agent gateway port: %s", fip_agent_port) + else: + # If not hostid check if the fixed ip provided has to + # deal with allowed_address_pairs for a given service + # port. Get the port_dict, inherit the service port host + # and device owner(if it does not exist). + port = self._core_plugin.get_port( + admin_ctx, fip_port) + allowed_device_owners = ( + n_utils.get_dvr_allowed_address_pair_device_owners()) + # NOTE: We just need to deal with ports that do not + # have a device_owner and ports that are owned by the + # dvr service ports except for the compute port and + # dhcp port. + if (port['device_owner'] == "" or + port['device_owner'] in allowed_device_owners): + addr_pair_active_service_port_list = ( + self._get_ports_for_allowed_address_pair_ip( + admin_ctx, port['network_id'], + floatingip_db['fixed_ip_address'])) + if not addr_pair_active_service_port_list: + return + if len(addr_pair_active_service_port_list) > 1: + LOG.warning(_LW("Multiple active ports associated " + "with the allowed_address_pairs.")) + return + self._inherit_service_port_and_arp_update( + context, addr_pair_active_service_port_list[0], + port) + + def _inherit_service_port_and_arp_update( + self, context, service_port, allowed_address_port): + """Function inherits port host bindings for allowed_address_pair.""" + service_port_dict = self._core_plugin._make_port_dict(service_port, + None) + address_pair_list = service_port_dict.get('allowed_address_pairs') + for address_pair in address_pair_list: + updated_port = ( + self.update_unbound_allowed_address_pair_port_binding( + context, service_port_dict, + address_pair, + address_pair_port=allowed_address_port)) + if not updated_port: + LOG.warning(_LW("Allowed_address_pair port update failed: %s"), + updated_port) + self.update_arp_entry_for_dvr_service_port(context, + service_port_dict) def _get_floatingip_on_port(self, context, port_id=None): """Helper function to retrieve the fip associated with port.""" @@ -914,7 +973,8 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin, context, fip.fixed_port_id) if fip else None def update_unbound_allowed_address_pair_port_binding( - self, context, service_port_dict, port_address_pairs): + self, context, service_port_dict, + port_address_pairs, address_pair_port=None): """Update allowed address pair port with host and device_owner This function sets the host and device_owner to the port @@ -922,8 +982,9 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin, host and device_owner. """ port_addr_pair_ip = port_address_pairs['ip_address'] - address_pair_port = self._get_address_pair_active_port_with_fip( - context, service_port_dict, port_addr_pair_ip) + if not address_pair_port: + address_pair_port = self._get_address_pair_active_port_with_fip( + context, service_port_dict, port_addr_pair_ip) if address_pair_port: host = service_port_dict[portbindings.HOST_ID] dev_owner = service_port_dict['device_owner'] @@ -944,15 +1005,17 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin, return update_port def remove_unbound_allowed_address_pair_port_binding( - self, context, service_port_dict, port_address_pairs): + self, context, service_port_dict, + port_address_pairs, address_pair_port=None): """Remove allowed address pair port binding and device_owner This function clears the host and device_owner associated with the port_addr_pair_ip. """ port_addr_pair_ip = port_address_pairs['ip_address'] - address_pair_port = self._get_address_pair_active_port_with_fip( - context, service_port_dict, port_addr_pair_ip) + if not address_pair_port: + address_pair_port = self._get_address_pair_active_port_with_fip( + context, service_port_dict, port_addr_pair_ip) if address_pair_port: # Before reverting the changes, fetch the original # device owner saved in profile and update the port 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 2f24f08e696..1a6f6c8d0a3 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 @@ -548,6 +548,147 @@ class L3DvrTestCase(ml2_test_base.ML2TestFramework): self.assertEqual( 0, l3_notifier.routers_updated_on_host.call_count) + def test_allowed_addr_pairs_delayed_fip_and_update_arp_entry(self): + HOST1 = 'host1' + helpers.register_l3_agent( + host=HOST1, agent_mode=n_const.L3_AGENT_MODE_DVR) + HOST2 = 'host2' + helpers.register_l3_agent( + host=HOST2, agent_mode=n_const.L3_AGENT_MODE_DVR) + router = self._create_router() + private_net1 = self._make_network(self.fmt, 'net1', True) + test_allocation_pools = [{'start': '10.1.0.2', + 'end': '10.1.0.20'}] + fixed_vrrp_ip = [{'ip_address': '10.1.0.201'}] + 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) + # 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, + allocation_pools=test_allocation_pools, + enable_dhcp=True) + vrrp_port = self._make_port( + self.fmt, + private_net1['network']['id'], + fixed_ips=fixed_vrrp_ip) + allowed_address_pairs = [ + {'ip_address': '10.1.0.201', + 'mac_address': vrrp_port['port']['mac_address']}] + with self.port( + subnet=private_subnet1, + device_owner=DEVICE_OWNER_COMPUTE) as int_port,\ + self.port(subnet=private_subnet1, + device_owner=DEVICE_OWNER_COMPUTE) as int_port2: + self.l3_plugin.add_router_interface( + self.context, router['id'], + {'subnet_id': private_subnet1['subnet']['id']}) + with mock.patch.object(self.l3_plugin, + '_l3_rpc_notifier') as l3_notifier: + vm_port = self.core_plugin.update_port( + self.context, int_port['port']['id'], + {'port': {portbindings.HOST_ID: HOST1}}) + vm_port_mac = vm_port['mac_address'] + vm_port_fixed_ips = vm_port['fixed_ips'] + vm_port_subnet_id = vm_port_fixed_ips[0]['subnet_id'] + vm_arp_table = { + 'ip_address': vm_port_fixed_ips[0]['ip_address'], + 'mac_address': vm_port_mac, + 'subnet_id': vm_port_subnet_id} + vm_port2 = self.core_plugin.update_port( + self.context, int_port2['port']['id'], + {'port': {portbindings.HOST_ID: HOST2}}) + l3_notifier.reset_mock() + # Now update the VM port with the allowed_address_pair + self.core_plugin.update_port( + self.context, vm_port['id'], + {'port': { + 'allowed_address_pairs': allowed_address_pairs}}) + self.core_plugin.update_port( + self.context, vm_port2['id'], + {'port': { + 'allowed_address_pairs': allowed_address_pairs}}) + self.assertEqual( + 0, l3_notifier.routers_updated_on_host.call_count) + updated_vm_port1 = self.core_plugin.get_port( + self.context, vm_port['id']) + updated_vm_port2 = self.core_plugin.get_port( + self.context, vm_port2['id']) + self.assertEqual(4, l3_notifier.add_arp_entry.call_count) + expected_allowed_address_pairs = updated_vm_port1.get( + 'allowed_address_pairs') + self.assertEqual(expected_allowed_address_pairs, + allowed_address_pairs) + expected_allowed_address_pairs_2 = updated_vm_port2.get( + 'allowed_address_pairs') + self.assertEqual(expected_allowed_address_pairs_2, + allowed_address_pairs) + # Now the VRRP port is attached to the VM port. At this + # point, the VRRP port should not have inherited the + # port host bindings from the parent VM port. + cur_vrrp_port_db = self.core_plugin.get_port( + self.context, vrrp_port['port']['id']) + self.assertNotEqual( + cur_vrrp_port_db[portbindings.HOST_ID], HOST1) + self.assertNotEqual( + cur_vrrp_port_db[portbindings.HOST_ID], HOST2) + # Before we try to associate a floatingip make sure that + # only one of the Service port associated with the + # allowed_address_pair port is active and the other one + # is DOWN + mod_vm_port2 = self.core_plugin.update_port( + self.context, updated_vm_port2['id'], + {'port': { + 'admin_state_up': False}}) + self.assertFalse(mod_vm_port2['admin_state_up']) + # Next we can try to associate the floatingip to the + # VRRP port that is already attached to the VM port + l3_notifier.reset_mock() + floating_ip = {'floating_network_id': ext_net['network']['id'], + 'router_id': router['id'], + 'port_id': vrrp_port['port']['id'], + 'tenant_id': vrrp_port['port']['tenant_id']} + floating_ip = self.l3_plugin.create_floatingip( + self.context, {'floatingip': floating_ip}) + self.assertEqual( + 2, l3_notifier.routers_updated_on_host.call_count) + self.assertEqual(3, l3_notifier.add_arp_entry.call_count) + + post_update_vrrp_port_db = self.core_plugin.get_port( + self.context, vrrp_port['port']['id']) + vrrp_port_fixed_ips = post_update_vrrp_port_db['fixed_ips'] + vrrp_port_subnet_id = vrrp_port_fixed_ips[0]['subnet_id'] + vrrp_arp_table = { + 'ip_address': vrrp_port_fixed_ips[0]['ip_address'], + 'mac_address': vm_port_mac, + 'subnet_id': vrrp_port_subnet_id} + vrrp_arp_table1 = { + 'ip_address': vrrp_port_fixed_ips[0]['ip_address'], + 'mac_address': vrrp_port['port']['mac_address'], + 'subnet_id': vrrp_port_subnet_id} + + self.assertEqual( + post_update_vrrp_port_db[portbindings.HOST_ID], HOST1) + expected_calls = [ + mock.call(self.context, + router['id'], vrrp_arp_table1), + mock.call(self.context, + router['id'], vm_arp_table), + mock.call(self.context, + router['id'], vrrp_arp_table)] + l3_notifier.add_arp_entry.assert_has_calls( + expected_calls) + def test_allowed_address_pairs_update_arp_entry(self): HOST1 = 'host1' helpers.register_l3_agent(