Skip to content

Commit 6eaa2a0

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "[OVN] Retry retrieving LSP hosting information" into stable/2023.1
2 parents 0110c31 + ddbb29a commit 6eaa2a0

File tree

2 files changed

+123
-1
lines changed

2 files changed

+123
-1
lines changed

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@
4242
from ovsdbapp.backend.ovs_idl import idlutils
4343
import tenacity
4444

45+
from neutron._i18n import _
4546
from neutron.common.ovn import acl as ovn_acl
4647
from neutron.common.ovn import constants as ovn_const
4748
from neutron.common.ovn import utils
4849
from neutron.common import utils as common_utils
4950
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
5051
from neutron.db import ovn_revision_numbers_db as db_rev
5152
from neutron.db import segments_db
53+
from neutron.plugins.ml2 import db as ml2_db
5254
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
5355
import placement as placement_extension
5456
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
@@ -232,6 +234,29 @@ def _get_port_dhcp_options(self, port, ip_version):
232234
external_ids=subnet_dhcp_options['external_ids'])
233235
return {'cmd': add_dhcp_opts_cmd}
234236

237+
@tenacity.retry(retry=tenacity.retry_if_exception_type(RuntimeError),
238+
wait=tenacity.wait_random(min=2, max=3),
239+
stop=tenacity.stop_after_attempt(3),
240+
reraise=True)
241+
def _wait_for_port_bindings_host(self, context, port_id):
242+
db_port = ml2_db.get_port(context, port_id)
243+
# This is already checked previously but, just to stay on
244+
# the safe side in case the port is deleted mid-operation
245+
if not db_port:
246+
raise RuntimeError(
247+
_('No port found with ID %s') % port_id)
248+
249+
if not db_port.port_bindings:
250+
raise RuntimeError(
251+
_('No port bindings information found for '
252+
'port %s') % port_id)
253+
254+
if not db_port.port_bindings[0].host:
255+
raise RuntimeError(
256+
_('No hosting information found for port %s') % port_id)
257+
258+
return db_port
259+
235260
def update_lsp_host_info(self, context, db_port, up=True):
236261
"""Update the binding hosting information for the LSP.
237262
@@ -247,8 +272,19 @@ def update_lsp_host_info(self, context, db_port, up=True):
247272
if up:
248273
if not db_port.port_bindings:
249274
return
250-
host = db_port.port_bindings[0].host
251275

276+
if not db_port.port_bindings[0].host:
277+
# NOTE(lucasgomes): There might be a sync issue between
278+
# the moment that this port was fetched from the database
279+
# and the hosting information being set, retry a few times
280+
try:
281+
db_port = self._wait_for_port_bindings_host(
282+
context, db_port.id)
283+
except RuntimeError as e:
284+
LOG.warning(e)
285+
return
286+
287+
host = db_port.port_bindings[0].host
252288
ext_ids = ('external_ids',
253289
{ovn_const.OVN_HOST_ID_EXT_ID_KEY: host})
254290
cmd.append(

neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_client.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from neutron.common.ovn import constants
1919
from neutron.conf.plugins.ml2 import config as ml2_conf
2020
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
21+
from neutron.plugins.ml2 import db as ml2_db
2122
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
2223
from neutron.tests import base
2324
from neutron.tests.unit.services.logapi.drivers.ovn \
@@ -26,6 +27,8 @@
2627
from neutron_lib import constants as const
2728
from neutron_lib.services.logapi import constants as log_const
2829

30+
from tenacity import wait_none
31+
2932

3033
class TestOVNClientBase(base.BaseTestCase):
3134

@@ -45,6 +48,9 @@ def setUp(self):
4548
self.get_plugin = mock.patch(
4649
'neutron_lib.plugins.directory.get_plugin').start()
4750

51+
# Disable tenacity wait for UT
52+
self.ovn_client._wait_for_port_bindings_host.retry.wait = wait_none()
53+
4854
def test__add_router_ext_gw_default_route(self):
4955
plugin = mock.MagicMock()
5056
self.get_plugin.return_value = plugin
@@ -114,6 +120,45 @@ def test_update_lsp_host_info_up(self):
114120
'Logical_Switch_Port', port_id,
115121
('external_ids', {constants.OVN_HOST_ID_EXT_ID_KEY: host_id}))
116122

123+
def test_update_lsp_host_info_up_retry(self):
124+
context = mock.MagicMock()
125+
host_id = 'fake-binding-host-id'
126+
port_id = 'fake-port-id'
127+
db_port_no_host = mock.Mock(
128+
id=port_id, port_bindings=[mock.Mock(host="")])
129+
db_port = mock.Mock(
130+
id=port_id, port_bindings=[mock.Mock(host=host_id)])
131+
132+
with mock.patch.object(
133+
self.ovn_client, '_wait_for_port_bindings_host') as mock_wait:
134+
mock_wait.return_value = db_port
135+
self.ovn_client.update_lsp_host_info(context, db_port_no_host)
136+
137+
# Assert _wait_for_port_bindings_host was called
138+
mock_wait.assert_called_once_with(context, port_id)
139+
140+
# Assert host_id was set
141+
self.nb_idl.db_set.assert_called_once_with(
142+
'Logical_Switch_Port', port_id,
143+
('external_ids', {constants.OVN_HOST_ID_EXT_ID_KEY: host_id}))
144+
145+
def test_update_lsp_host_info_up_retry_fail(self):
146+
context = mock.MagicMock()
147+
port_id = 'fake-port-id'
148+
db_port_no_host = mock.Mock(
149+
id=port_id, port_bindings=[mock.Mock(host="")])
150+
151+
with mock.patch.object(
152+
self.ovn_client, '_wait_for_port_bindings_host') as mock_wait:
153+
mock_wait.side_effect = RuntimeError("boom")
154+
self.ovn_client.update_lsp_host_info(context, db_port_no_host)
155+
156+
# Assert _wait_for_port_bindings_host was called
157+
mock_wait.assert_called_once_with(context, port_id)
158+
159+
# Assert host_id was NOT set
160+
self.assertFalse(self.nb_idl.db_set.called)
161+
117162
def test_update_lsp_host_info_down(self):
118163
context = mock.MagicMock()
119164
port_id = 'fake-port-id'
@@ -125,6 +170,47 @@ def test_update_lsp_host_info_down(self):
125170
'Logical_Switch_Port', port_id, 'external_ids',
126171
constants.OVN_HOST_ID_EXT_ID_KEY, if_exists=True)
127172

173+
@mock.patch.object(ml2_db, 'get_port')
174+
def test__wait_for_port_bindings_host(self, mock_get_port):
175+
context = mock.MagicMock()
176+
host_id = 'fake-binding-host-id'
177+
port_id = 'fake-port-id'
178+
db_port_no_host = mock.Mock(
179+
id=port_id, port_bindings=[mock.Mock(host="")])
180+
db_port = mock.Mock(
181+
id=port_id, port_bindings=[mock.Mock(host=host_id)])
182+
183+
mock_get_port.side_effect = (db_port_no_host, db_port)
184+
185+
ret = self.ovn_client._wait_for_port_bindings_host(
186+
context, port_id)
187+
188+
self.assertEqual(ret, db_port)
189+
190+
expected_calls = [mock.call(context, port_id),
191+
mock.call(context, port_id)]
192+
mock_get_port.assert_has_calls(expected_calls)
193+
194+
@mock.patch.object(ml2_db, 'get_port')
195+
def test__wait_for_port_bindings_host_fail(self, mock_get_port):
196+
context = mock.MagicMock()
197+
port_id = 'fake-port-id'
198+
db_port_no_pb = mock.Mock(id=port_id, port_bindings=[])
199+
db_port_no_host = mock.Mock(
200+
id=port_id, port_bindings=[mock.Mock(host="")])
201+
202+
mock_get_port.side_effect = (
203+
db_port_no_pb, db_port_no_host, db_port_no_host)
204+
205+
self.assertRaises(
206+
RuntimeError, self.ovn_client._wait_for_port_bindings_host,
207+
context, port_id)
208+
209+
expected_calls = [mock.call(context, port_id),
210+
mock.call(context, port_id),
211+
mock.call(context, port_id)]
212+
mock_get_port.assert_has_calls(expected_calls)
213+
128214

129215
class TestOVNClientFairMeter(TestOVNClientBase,
130216
test_log_driver.TestOVNDriverBase):

0 commit comments

Comments
 (0)