diff --git a/neutron/db/agents_db.py b/neutron/db/agents_db.py index be1ff37382d..cf6d1d595a4 100644 --- a/neutron/db/agents_db.py +++ b/neutron/db/agents_db.py @@ -267,6 +267,9 @@ class AgentDbMixin(ext_agent.AgentPluginBase, AgentAvailabilityZoneMixin): payload=events.DBEventPayload( context, states=(agent,), resource_id=id)) agent.delete() + registry.publish(resources.AGENT, events.AFTER_DELETE, self, + payload=events.DBEventPayload( + context, states=(agent,), resource_id=id)) @db_api.retry_if_session_inactive() def update_agent(self, context, id, agent): diff --git a/neutron/db/l3_dvr_db.py b/neutron/db/l3_dvr_db.py index 4bc4f61d26a..dd090ba422b 100644 --- a/neutron/db/l3_dvr_db.py +++ b/neutron/db/l3_dvr_db.py @@ -52,6 +52,7 @@ from neutron.ipam import utils as ipam_utils from neutron.objects import agent as ag_obj from neutron.objects import base as base_obj from neutron.objects import l3agent as rb_obj +from neutron.objects import ports as port_obj from neutron.objects import router as l3_obj @@ -791,6 +792,61 @@ class DVRResourceOperationHandler(object): p['id'], l3_port_check=False) + def _get_ext_nets_by_host(self, context, host): + ext_nets = set() + ports_ids = port_obj.Port.get_ports_by_host(context, host) + for port_id in ports_ids: + fips = self._get_floatingips_by_port_id(context, port_id) + for fip in fips: + ext_nets.add(fip.floating_network_id) + return ext_nets + + @registry.receives(resources.AGENT, [events.AFTER_CREATE]) + def create_fip_agent_gw_ports(self, resource, event, + trigger, payload=None): + """Create floating agent gw ports for DVR L3 agent. + + Create floating IP Agent gateway ports when an L3 agent is created. + """ + if not payload: + return + agent = payload.latest_state + if agent.get('agent_type') != const.AGENT_TYPE_L3: + return + # NOTE(slaweq) agent is passed in payload as dict so to avoid getting + # again from db, lets just get configuration from this dict directly + l3_agent_mode = agent.get('configurations', {}).get('agent_mode') + if l3_agent_mode not in [const.L3_AGENT_MODE_DVR, + const.L3_AGENT_MODE_DVR_SNAT]: + return + + host = agent['host'] + context = payload.context.elevated() + for ext_net in self._get_ext_nets_by_host(context, host): + self.create_fip_agent_gw_port_if_not_exists( + context, ext_net, host) + + @registry.receives(resources.AGENT, [events.AFTER_DELETE]) + def delete_fip_agent_gw_ports(self, resource, event, + trigger, payload=None): + """Delete floating agent gw ports for DVR. + + Delete floating IP Agent gateway ports from host when an L3 agent is + deleted. + """ + if not payload: + return + agent = payload.latest_state + if agent.get('agent_type') != const.AGENT_TYPE_L3: + return + if self._get_agent_mode(agent) not in [const.L3_AGENT_MODE_DVR, + const.L3_AGENT_MODE_DVR_SNAT]: + return + + agent_gw_ports = self._get_agent_gw_ports(payload.context, agent['id']) + for gw_port in agent_gw_ports: + self._core_plugin.delete_port(payload.context, gw_port['id']) + class _DVRAgentInterfaceMixin(object): """Contains calls made by the DVR scheduler and RPC interface. @@ -1067,6 +1123,14 @@ class _DVRAgentInterfaceMixin(object): if ports: return ports[0] + def _get_agent_gw_ports(self, context, agent_id): + """Return agent gw ports.""" + filters = { + 'device_id': [agent_id], + 'device_owner': [const.DEVICE_OWNER_AGENT_GW] + } + return self._core_plugin.get_ports(context, filters) + def check_for_fip_and_create_agent_gw_port_on_host_if_not_exists( self, context, port, host): """Create fip agent_gw_port on host if not exists""" diff --git a/neutron/objects/ports.py b/neutron/objects/ports.py index 2466f96ffb4..8252a78179d 100644 --- a/neutron/objects/ports.py +++ b/neutron/objects/ports.py @@ -639,6 +639,14 @@ class Port(base.NeutronDbObject): ~models_v2.Port.device_owner.in_(excluded_device_owners)) return [port_binding['port_id'] for port_binding in query.all()] + @classmethod + def get_ports_by_host(cls, context, host): + query = context.session.query(models_v2.Port.id).join( + ml2_models.PortBinding) + query = query.filter( + ml2_models.PortBinding.host == host) + return [port_id[0] for port_id in query.all()] + @classmethod def get_ports_by_binding_type_and_host(cls, context, binding_type, host): diff --git a/neutron/tests/unit/db/test_l3_dvr_db.py b/neutron/tests/unit/db/test_l3_dvr_db.py index 74326f41aae..e160efd904a 100644 --- a/neutron/tests/unit/db/test_l3_dvr_db.py +++ b/neutron/tests/unit/db/test_l3_dvr_db.py @@ -37,6 +37,7 @@ from neutron.db.models import l3 as l3_models from neutron.db import models_v2 from neutron.objects import agent as agent_obj from neutron.objects import l3agent as rb_obj +from neutron.objects import ports as port_obj from neutron.objects import router as router_obj from neutron.tests.unit.db import test_db_base_plugin_v2 from neutron.tests.unit.extensions import test_l3 @@ -815,6 +816,115 @@ class L3DvrTestCase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase): fip, floatingip, router)) self.assertFalse(create_fip.called) + def test_get_ext_nets_by_host(self): + ports = [mock.Mock(id=_uuid()) for _ in range(3)] + fips = [mock.Mock(fixed_port_id=p.id, floating_network_id=_uuid()) + for p in ports] + expected_ext_nets = set([fip.floating_network_id for fip in fips]) + with mock.patch.object( + port_obj.Port, 'get_ports_by_host', + return_value=[p.id for p in ports] + ) as get_ports_by_host, mock.patch.object( + self.mixin, '_get_floatingips_by_port_id', return_value=fips + ) as get_floatingips_by_port_id: + self.assertEqual( + expected_ext_nets, + self.mixin._get_ext_nets_by_host(self.ctx, 'host')) + get_ports_by_host.assert_called_once_with(self.ctx, 'host') + get_floatingips_by_port_id.assert_has_calls( + [mock.call(self.ctx, p.id) for p in ports]) + + def _test_create_fip_agent_gw_ports(self, agent_type, agent_mode=None): + agent = { + 'id': _uuid(), + 'host': 'host', + 'agent_type': agent_type, + 'configurations': {'agent_mode': agent_mode}} + payload = events.DBEventPayload( + self.ctx, states=(agent,), resource_id=agent['id']) + + ext_nets = ['ext-net-1', 'ext-net-2'] + with mock.patch.object( + self.mixin, + 'create_fip_agent_gw_port_if_not_exists' + ) as create_fip_gw, mock.patch.object( + self.mixin, "_get_ext_nets_by_host", + return_value=ext_nets + ) as get_ext_nets_by_host: + + registry.publish(resources.AGENT, events.AFTER_CREATE, mock.Mock(), + payload=payload) + + if agent_type == 'L3 agent' and agent_mode in ['dvr', 'dvr_snat']: + get_ext_nets_by_host.assert_called_once_with( + mock.ANY, 'host') + create_fip_gw.assert_has_calls( + [mock.call(mock.ANY, ext_net, 'host') for + ext_net in ext_nets]) + else: + get_ext_nets_by_host.assert_not_called() + create_fip_gw.assert_not_called() + + def test_create_fip_agent_gw_ports(self): + self._test_create_fip_agent_gw_ports( + agent_type='L3 agent', agent_mode='dvr') + self._test_create_fip_agent_gw_ports( + agent_type='L3 agent', agent_mode='dvr_snat') + + def test_create_fip_agent_gw_ports_dvr_no_external_agent(self): + self._test_create_fip_agent_gw_ports( + agent_type='L3 agent', agent_mode='dvr_no_external') + + def test_create_fip_agent_gw_ports_non_dvr_agent(self): + self._test_create_fip_agent_gw_ports( + agent_type='L3 agent', agent_mode='legacy') + + def test_create_fip_agent_gw_ports_deleted_non_l3_agent(self): + self._test_create_fip_agent_gw_ports('Other agent type') + + def _test_delete_fip_agent_gw_ports(self, agent_type, agent_mode=None): + agent = agent_obj.Agent( + self.ctx, id=_uuid(), agent_type=agent_type, + configurations={"agent_mode": agent_mode}) + payload = events.DBEventPayload( + self.ctx, states=(agent,), resource_id=agent.id) + + gw_port = {'id': _uuid(), 'network_id': _uuid()} + with mock.patch.object( + self.mixin, '_get_agent_gw_ports', + return_value=[gw_port] + ) as get_agent_gw_ports, mock.patch.object( + self.core_plugin, 'delete_port' + ) as delete_port: + registry.publish(resources.AGENT, events.AFTER_DELETE, mock.Mock(), + payload=payload) + + if agent_type == 'L3 agent' and agent_mode in ['dvr', 'dvr_snat']: + get_agent_gw_ports.assert_called_once_with(payload.context, + agent['id']) + delete_port.assert_called_once_with(payload.context, + gw_port['id']) + else: + get_agent_gw_ports.assert_not_called() + delete_port.assert_not_called() + + def test_delete_fip_agent_gw_ports(self): + self._test_delete_fip_agent_gw_ports( + agent_type='L3 agent', agent_mode='dvr') + self._test_delete_fip_agent_gw_ports( + agent_type='L3 agent', agent_mode='dvr_snat') + + def test_delete_fip_agent_gw_ports_dvr_no_external_agent(self): + self._test_delete_fip_agent_gw_ports( + agent_type='L3 agent', agent_mode='dvr_no_external') + + def test_delete_fip_agent_gw_ports_non_dvr_agent(self): + self._test_delete_fip_agent_gw_ports( + agent_type='L3 agent', agent_mode='legacy') + + def test_delete_fip_agent_gw_ports_deleted_non_l3_agent(self): + self._test_delete_fip_agent_gw_ports('Other agent type') + def _test_update_router_gw_info_external_network_change(self): router_dict = {'name': 'test_router', 'admin_state_up': True, 'distributed': True}