From 09b7b2e1ea6f40cd9e6104f1c5fbef31fc3e0545 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Wed, 1 May 2019 15:50:48 +0800 Subject: [PATCH] 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 0dc730c7c0d3f0a49dee28d0d6e7ff9020d94443) --- neutron/plugins/ml2/plugin.py | 16 +++++ neutron/tests/unit/plugins/ml2/test_plugin.py | 60 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 56c57136b17..c4530727ed7 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -666,6 +666,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 diff --git a/neutron/tests/unit/plugins/ml2/test_plugin.py b/neutron/tests/unit/plugins/ml2/test_plugin.py index 97687841244..82d818e65bf 100644 --- a/neutron/tests/unit/plugins/ml2/test_plugin.py +++ b/neutron/tests/unit/plugins/ml2/test_plugin.py @@ -2082,6 +2082,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}