summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSlawek Kaplonski <skaplons@redhat.com>2021-04-23 10:45:14 +0200
committerSlawek Kaplonski <skaplons@redhat.com>2021-06-24 06:59:32 +0000
commit8eaa61e916da62d4246a964e7bbcb81b9d5a8506 (patch)
tree52b4c38fc039d6c15ca098412ca278561ea267c0
parentcc663a83b1b5cc654b1518a1aeeb176fb6321b2f (diff)
downloadneutron-8eaa61e916da62d4246a964e7bbcb81b9d5a8506.tar.gz
Remove FIP agent's gw port when L3 agent is deleted
Floating IP agent gateway ports are created for each external network for each node where DVR L3 agent is running and where there is some FIP from the ext_net. But even, if L3 agent is removed (e.g. when scaling down the cluster), such floating IP gateway port is never removed so it consumes IP address from the external network. With this patch when the DVR L3 agent is deleted, all such fip gateway ports owned by that agent will be deleted. When new L3 agent is created (registered in the DB), Neutron will check if there are any floating IPs on that host and will recreate such FIP gateway ports for it. Conflicts: neutron/tests/unit/db/test_l3_dvr_db.py Closes-Bug: #1891360 Change-Id: If6ef990baf039c556d7420962ac4c54608711f06 (cherry picked from commit 8cc7c0cf7a5196103b097fae67eccbd5dc3980ac)
-rw-r--r--neutron/db/agents_db.py3
-rw-r--r--neutron/db/l3_dvr_db.py64
-rw-r--r--neutron/objects/ports.py8
-rw-r--r--neutron/tests/unit/db/test_l3_dvr_db.py110
4 files changed, 185 insertions, 0 deletions
diff --git a/neutron/db/agents_db.py b/neutron/db/agents_db.py
index 77e8d033c0..ead21b8494 100644
--- a/neutron/db/agents_db.py
+++ b/neutron/db/agents_db.py
@@ -269,6 +269,9 @@ class AgentDbMixin(ext_agent.AgentPluginBase, AgentAvailabilityZoneMixin):
registry.notify(resources.AGENT, events.BEFORE_DELETE, self,
context=context, agent=agent)
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 093ed2bcb3..84d0aa37fb 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
@@ -715,6 +716,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.
@@ -993,6 +1049,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 5830ba8e09..db150c4c46 100644
--- a/neutron/objects/ports.py
+++ b/neutron/objects/ports.py
@@ -561,6 +561,14 @@ class Port(base.NeutronDbObject):
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):
query = context.session.query(models_v2.Port).join(
diff --git a/neutron/tests/unit/db/test_l3_dvr_db.py b/neutron/tests/unit/db/test_l3_dvr_db.py
index 6b5d8e7e4f..6f437337b8 100644
--- a/neutron/tests/unit/db/test_l3_dvr_db.py
+++ b/neutron/tests/unit/db/test_l3_dvr_db.py
@@ -35,6 +35,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
@@ -552,6 +553,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}