diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index eda0101b9ff..57caf379b86 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 f6d2c4f71ec..1ab2e9046e7 100644 --- a/neutron/tests/unit/plugins/ml2/test_plugin.py +++ b/neutron/tests/unit/plugins/ml2/test_plugin.py @@ -2090,6 +2090,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}