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
This commit is contained in:
parent
dfd04115b0
commit
8cc7c0cf7a
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
@ -790,6 +791,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.
|
||||
@ -1066,6 +1122,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"""
|
||||
|
@ -638,6 +638,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):
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user