Populate binding levels when concurrent ops fail

Concurrent calls to _bind_port_if_needed may lead to a missing RPC
notification which can cause a port stuck in a DOWN state. If the only
caller that succeeds in the concurrency does not specify that an RPC
notification is allowed then no RPC would be sent to the agent. The
other caller which needs to send an RPC notification will fail since the
resulting PortContext instance will not have any binding levels set.

The failure has negative effects on consumers of the L2Population
functionality because the L2Population mechanism driver will not be
triggered to publish that a port is UP on a given compute node. Manual
intervention is required in this case.

This patch proposes to handle this by populating the PortContext with
the current binding levels so that the caller can continue on and have
an RPC notification sent out.

Closes-Bug: #1755810
Story: 2003922
Change-Id: Ie2b813b2bdf181fb3c24743dbd13487ace6ee76a
(cherry picked from commit 0dc730c7c0)
This commit is contained in:
Kailun Qin 2019-05-01 15:50:48 +08:00 committed by Slawek Kaplonski
parent 2318a60046
commit f7e100d33f
2 changed files with 77 additions and 0 deletions

View File

@ -629,6 +629,22 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
# Call the mechanism driver precommit methods, commit
# the results, and call the postcommit methods.
self.mechanism_manager.update_port_precommit(cur_context)
else:
# Try to populate the PortContext with the current binding
# levels so that the RPC notification won't get suppressed.
# This is to avoid leaving ports stuck in a DOWN state.
# For more information see bug:
# https://bugs.launchpad.net/neutron/+bug/1755810
LOG.warning("Concurrent port binding operations failed on "
"port %s", port_id)
levels = db.get_binding_levels(plugin_context, port_id,
cur_binding.host)
for level in levels:
cur_context._push_binding_level(level)
# refresh context with a snapshot of the current binding state
cur_context._binding = driver_context.InstanceSnapshot(
cur_binding)
if commit:
# Continue, using the port state as of the transaction that
# just finished, whether that transaction committed new

View File

@ -48,6 +48,7 @@ from neutron.db import provisioning_blocks
from neutron.db import segments_db
from neutron.extensions import multiprovidernet as mpnet
from neutron.objects import base as base_obj
from neutron.objects import ports as port_obj
from neutron.objects import router as l3_obj
from neutron.plugins.ml2.common import exceptions as ml2_exc
from neutron.plugins.ml2 import db as ml2_db
@ -1962,6 +1963,66 @@ class TestMl2PortBinding(Ml2PluginV2TestCase,
# Successful binding should only be attempted once.
self.assertEqual(1, at_mock.call_count)
def test__bind_port_if_needed_concurrent_calls(self):
port_vif_type = portbindings.VIF_TYPE_UNBOUND
bound_vif_type = portbindings.VIF_TYPE_OVS
plugin, port_context, bound_context = (
self._create_port_and_bound_context(port_vif_type,
bound_vif_type))
bound_context._binding_levels = [mock.Mock(
port_id="port_id",
level=0,
driver='fake_agent',
segment_id="11111111-2222-3333-4444-555555555555")]
# let _commit_port_binding replace the PortContext with a new instance
# which does not have any binding levels set to simulate the concurrent
# port binding operations fail
with mock.patch(
'neutron.plugins.ml2.plugin.Ml2Plugin._bind_port',
return_value=bound_context),\
mock.patch('neutron.plugins.ml2.plugin.Ml2Plugin.'
'_notify_port_updated') as npu_mock,\
mock.patch('neutron.plugins.ml2.plugin.Ml2Plugin.'
'_attempt_binding',
side_effect=plugin._attempt_binding) as ab_mock,\
mock.patch('neutron.plugins.ml2.plugin.Ml2Plugin.'
'_commit_port_binding', return_value=(
mock.MagicMock(), True, True)) as cpb_mock:
ret_context = plugin._bind_port_if_needed(port_context,
allow_notify=True)
# _attempt_binding will return without doing anything during
# the second iteration since _should_bind_port returns False
self.assertEqual(2, ab_mock.call_count)
self.assertEqual(1, cpb_mock.call_count)
# _notify_port_updated will still be called though it does
# nothing due to the missing binding levels
npu_mock.assert_called_once_with(ret_context)
def test__commit_port_binding_populating_with_binding_levels(self):
port_vif_type = portbindings.VIF_TYPE_OVS
bound_vif_type = portbindings.VIF_TYPE_OVS
plugin, port_context, bound_context = (
self._create_port_and_bound_context(port_vif_type,
bound_vif_type))
db_portbinding = port_obj.PortBindingLevel(
self.context,
port_id=uuidutils.generate_uuid(),
level=0,
driver='fake_agent',
segment_id="11111111-2222-3333-4444-555555555555")
bound_context.network.current = {'id': 'net_id'}
with mock.patch.object(ml2_db, 'get_binding_levels',
return_value=[db_portbinding]),\
mock.patch.object(driver_context.PortContext,
'_push_binding_level') as pbl_mock:
plugin._commit_port_binding(
port_context, bound_context, True, False)
pbl_mock.assert_called_once_with(db_portbinding)
def test_port_binding_profile_not_changed(self):
profile = {'e': 5}
profile_arg = {portbindings.PROFILE: profile}