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.

Closes-Bug: #1891360
Change-Id: If6ef990baf039c556d7420962ac4c54608711f06
(cherry picked from commit 8cc7c0cf7a)
This commit is contained in:
Slawek Kaplonski 2021-04-23 10:45:14 +02:00
parent a7fac51723
commit 4bdfc71145
4 changed files with 185 additions and 0 deletions

View File

@ -270,6 +270,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):

View File

@ -53,6 +53,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
@ -746,6 +747,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.
@ -1022,6 +1078,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"""

View File

@ -596,6 +596,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):

View File

@ -34,6 +34,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
@ -677,6 +678,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}