deepcopy binding and binding levels avoid expiration

Perform a deepcopy on the sqla objects passed into the PortContext
so we get detached versions of them safe to reference forever.
This is necessary because the PortContexts outlive the
transaction context managers they are creating in which means an
object can be expired and result in a query after a commit
(e.g. in bind_port_if_needed) that will fail and result in an
exception.

This required a few additional explicit session.merge calls to deal
with cases where touching the mech context was implicitly expected
to modify the DB state on the next commit.

Conflicts:
	neutron/plugins/ml2/driver_context.py
	neutron/plugins/ml2/plugin.py
	neutron/tests/unit/plugins/ml2/test_port_binding.py

Closes-Bug: #1669528
Change-Id: Ib5ba2daa80acba53c082bade1f61a3ee44ca41fc
(cherry picked from commit 20c1de9dc8)
This commit is contained in:
Kevin Benton 2017-03-03 08:51:33 -08:00 committed by Jakub Libosvar
parent 59c6530990
commit cb773098d2
4 changed files with 25 additions and 8 deletions

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from neutron_lib import constants
from oslo_log import log
from oslo_serialization import jsonutils
@ -96,8 +98,10 @@ class PortContext(MechanismDriverContext, api.PortContext):
else:
self._network_context = NetworkContext(
plugin, plugin_context, network) if network else None
self._binding = binding
self._binding_levels = binding_levels
# NOTE(kevinbenton): these copys can go away once we are working with
# OVO objects here instead of native SQLA objects.
self._binding = copy.deepcopy(binding)
self._binding_levels = copy.deepcopy(binding_levels)
self._segments_to_bind = None
self._new_bound_segment = None
self._next_segments_to_bind = None

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from eventlet import greenthread
from neutron_lib.api import validators
from neutron_lib import constants as const
@ -348,6 +350,10 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
binding.host = ''
self._update_port_dict_binding(port, binding)
# merging here brings binding changes into the session so they can be
# committed since the binding attached to the context is detached from
# the session
mech_context._plugin_context.session.merge(binding)
return changes
def _bind_port_if_needed(self, context, allow_notify=False,
@ -510,6 +516,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
cur_binding.vif_details = new_binding.vif_details
db.clear_binding_levels(session, port_id, cur_binding.host)
db.set_binding_levels(session, bind_context._binding_levels)
# refresh context with a snapshot of updated state
cur_context._binding = copy.deepcopy(cur_binding)
cur_context._binding_levels = bind_context._binding_levels
# Update PortContext's port dictionary to reflect the
@ -1414,6 +1422,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
self._update_port_dict_binding(port, binding)
binding.host = attrs and attrs.get(portbindings.HOST_ID)
binding.router_id = attrs and attrs.get('device_id')
# merge into session to reflect changes
mech_context._plugin_context.session.merge(binding)
@utils.transaction_guard
@db_api.retry_if_session_inactive()
@ -1635,8 +1645,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
session, port['id'], host)
if not binding:
return
binding['status'] = status
binding.update(binding)
binding.status = status
updated = True
if (updated and

View File

@ -1596,7 +1596,8 @@ class TestMl2PortBinding(Ml2PluginV2TestCase,
plugin = manager.NeutronManager.get_plugin()
binding = ml2_db.get_locked_port_and_binding(self.context.session,
port['port']['id'])[1]
binding['host'] = 'test'
with self.context.session.begin(subtransactions=True):
binding.host = 'test'
mech_context = driver_context.PortContext(
plugin, self.context, port['port'],
plugin.get_network(self.context, port['port']['network_id']),
@ -1604,7 +1605,9 @@ class TestMl2PortBinding(Ml2PluginV2TestCase,
with mock.patch('neutron.plugins.ml2.plugin.Ml2Plugin.'
'_update_port_dict_binding') as update_mock:
attrs = {portbindings.HOST_ID: None}
plugin._process_port_binding(mech_context, attrs)
self.assertEqual('test', binding.host)
with self.context.session.begin(subtransactions=True):
plugin._process_port_binding(mech_context, attrs)
self.assertTrue(update_mock.mock_calls)
self.assertEqual('', binding.host)
@ -2507,7 +2510,7 @@ class TestMl2PluginCreateUpdateDeletePort(base.BaseTestCase):
binding.host = 'vm_host'
binding.vnic_type = portbindings.VNIC_NORMAL
binding.profile = ''
binding.vif_type = ''
binding.vif_type = 'unbound'
binding.vif_details = ''
with mock.patch.object(ml2_plugin.Ml2Plugin, '__init__') as init,\

View File

@ -15,6 +15,7 @@
import mock
from neutron_lib import constants as const
from oslo_serialization import jsonutils
from neutron import context
from neutron.extensions import portbindings
@ -194,7 +195,7 @@ class PortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
port_id=original_port['id'],
host=original_port['binding:host_id'],
vnic_type=original_port['binding:vnic_type'],
profile=original_port['binding:profile'],
profile=jsonutils.dumps(original_port['binding:profile']),
vif_type=original_port['binding:vif_type'],
vif_details=original_port['binding:vif_details'])
levels = 1