Skip to content

Commit 318160f

Browse files
Add Listener sync logic
This patch adds the logic to sync a Listener entity from the Octavia database, correcting any discrepancies in fields or creating it if it does not exist in the OVN LB related on OVN Northbound (NB) database. Future patches will incrementally add support for syncing the remaining entities. Related-Bug: #2045415 Co-authored-by: Fernando Royo <[email protected]> Co-authored-by: Rico Lin <[email protected]> Change-Id: Ibf39a73386eed1e00c7ce39e60274e028df0cced
1 parent 07b4c4b commit 318160f

File tree

4 files changed

+254
-8
lines changed

4 files changed

+254
-8
lines changed

ovn_octavia_provider/driver.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ def _get_loadbalancer_request_info(self, loadbalancer):
105105
loadbalancer.additional_vips
106106
return request_info
107107

108+
def _get_listener_request_info(self, listener):
109+
self._check_for_supported_protocols(listener.protocol)
110+
self._check_for_allowed_cidrs(listener.allowed_cidrs)
111+
admin_state_up = listener.admin_state_up
112+
if isinstance(admin_state_up, o_datamodels.UnsetType):
113+
admin_state_up = True
114+
request_info = {'id': listener.listener_id,
115+
'protocol': listener.protocol,
116+
'loadbalancer_id': listener.loadbalancer_id,
117+
'protocol_port': listener.protocol_port,
118+
'default_pool_id': listener.default_pool_id,
119+
'admin_state_up': admin_state_up}
120+
return request_info
121+
108122
def loadbalancer_create(self, loadbalancer):
109123
request = {'type': ovn_const.REQ_TYPE_LB_CREATE,
110124
'info': self._get_loadbalancer_request_info(
@@ -598,9 +612,16 @@ def _ensure_loadbalancer(self, loadbalancer):
598612
except idlutils.RowNotFound:
599613
LOG.debug(f"OVN loadbalancer {loadbalancer.loadbalancer_id} "
600614
"not found. Start create process.")
601-
# TODO(froyo): By now just syncing LB only
615+
# TODO(froyo): By now just syncing LB and listener only
602616
status = self._ovn_helper.lb_create(
603617
self._get_loadbalancer_request_info(loadbalancer))
618+
619+
if not isinstance(loadbalancer.listeners, o_datamodels.UnsetType):
620+
status[constants.LISTENERS] = []
621+
for listener in loadbalancer.listeners:
622+
status_listener = self._ovn_helper.listener_create(
623+
self._get_listener_request_info(listener))
624+
status[constants.LISTENERS].append(status_listener)
604625
self._ovn_helper._update_status_to_octavia(status)
605626
else:
606627
# Load Balancer found, check LB and listener/pool/member/hms
@@ -611,6 +632,12 @@ def _ensure_loadbalancer(self, loadbalancer):
611632
"found checking other entities related")
612633
self._ovn_helper.lb_sync(
613634
self._get_loadbalancer_request_info(loadbalancer), ovn_lb)
635+
# Listener
636+
if not isinstance(loadbalancer.listeners,
637+
o_datamodels.UnsetType):
638+
for listener in loadbalancer.listeners:
639+
self._ovn_helper.listener_sync(
640+
self._get_listener_request_info(listener), ovn_lb)
614641
status = self._ovn_helper._get_current_operating_statuses(
615642
ovn_lb)
616643
self._ovn_helper._update_status_to_octavia(status)
@@ -625,4 +652,11 @@ def do_sync(self, **lb_filters):
625652
provider_lb = (
626653
self._ovn_helper._octavia_driver_lib.get_loadbalancer(lb.id)
627654
)
655+
656+
listeners = provider_lb.listeners or []
657+
provider_lb.listeners = [
658+
o_datamodels.Listener.from_dict(listener)
659+
for listener in listeners
660+
] if listeners else o_datamodels.Unset
661+
628662
self._ensure_loadbalancer(provider_lb)

ovn_octavia_provider/helper.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,50 @@ def _sync_lb_to_lr_association(self, ovn_lb, ovn_lr):
619619
"router %s: %s", ovn_lb.uuid, ovn_lr.uuid,
620620
str(e))
621621

622+
def _build_listener_info(self, listener, external_ids):
623+
"""Build listener key and listener info."""
624+
listener_key = self._get_listener_key(
625+
listener.get(constants.ID),
626+
is_enabled=listener.get(constants.ADMIN_STATE_UP)
627+
)
628+
pool_key = ''
629+
if listener.get(constants.DEFAULT_POOL_ID):
630+
pool_key = self._get_pool_key(
631+
listener.get(constants.DEFAULT_POOL_ID))
632+
external_ids[listener_key] = self._make_listener_key_value(
633+
listener[constants.PROTOCOL_PORT], pool_key
634+
)
635+
listener_info = {listener_key: external_ids[listener_key]}
636+
return listener_key, listener_info
637+
638+
def _update_listener_key_if_needed(self, listener_key, listener_info,
639+
ovn_lb, commands):
640+
"""Update listener key on OVN LoadBalancer if needed."""
641+
prev_listener_key_content = ovn_lb.external_ids.get(listener_key, '')
642+
if (listener_key not in ovn_lb.external_ids or
643+
listener_info.get(listener_key) != prev_listener_key_content):
644+
commands.append(
645+
self.ovn_nbdb_api.db_set(
646+
'Load_Balancer',
647+
ovn_lb.uuid,
648+
('external_ids', listener_info)
649+
)
650+
)
651+
652+
def _update_protocol_if_needed(self, listener, ovn_lb, commands):
653+
"""Update protocol on OVN LoadBalancer if needed."""
654+
current_protocol = ''
655+
if ovn_lb.protocol:
656+
current_protocol = ovn_lb.protocol[0].lower()
657+
listener_protocol = str(listener.get(constants.PROTOCOL)).lower()
658+
if current_protocol != listener_protocol:
659+
commands.append(
660+
self.ovn_nbdb_api.db_set(
661+
'Load_Balancer', ovn_lb.uuid,
662+
('protocol', listener_protocol)
663+
)
664+
)
665+
622666
def _lb_status(self, loadbalancer, provisioning_status, operating_status):
623667
"""Return status for the LoadBalancer."""
624668
return {
@@ -1868,6 +1912,39 @@ def listener_create(self, listener):
18681912
constants.PROVISIONING_STATUS: constants.ACTIVE}]}
18691913
return status
18701914

1915+
def listener_sync(self, listener, ovn_lb):
1916+
"""Sync Listener object with an OVN LoadBalancer
1917+
1918+
The method performs the following steps:
1919+
1. Update listener key on OVN Loadbalancer external_ids if needed
1920+
2. Update OVN LoadBalancer protocol from Listener info if needed
1921+
3. Refresh OVN LoadBalancer vips
1922+
1923+
:param listener: The source listener object from Octavia DB
1924+
:param ovn_lb: The OVN LoadBalancer object that needs to be sync
1925+
"""
1926+
commands = []
1927+
external_ids = copy.deepcopy(ovn_lb.external_ids)
1928+
1929+
listener_key, listener_info = self._build_listener_info(
1930+
listener, external_ids)
1931+
self._update_listener_key_if_needed(
1932+
listener_key, listener_info, ovn_lb, commands)
1933+
self._update_protocol_if_needed(listener, ovn_lb, commands)
1934+
1935+
try:
1936+
commands.extend(self._refresh_lb_vips(
1937+
ovn_lb, external_ids, is_sync=True))
1938+
except Exception as e:
1939+
LOG.exception(f"Failed to refresh LB VIPs: {e}")
1940+
return
1941+
1942+
try:
1943+
self._execute_commands(commands)
1944+
except Exception as e:
1945+
LOG.exception(f"Failed to execute commands for listener sync: {e}")
1946+
return
1947+
18711948
def listener_delete(self, listener):
18721949
status = {
18731950
constants.LISTENERS: [

ovn_octavia_provider/tests/unit/test_driver.py

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,32 +1237,82 @@ def test_health_monitor_delete(self):
12371237

12381238
@mock.patch.object(ovn_helper.OvnProviderHelper,
12391239
'_update_status_to_octavia')
1240+
@mock.patch.object(ovn_helper.OvnProviderHelper, 'listener_create')
12401241
@mock.patch.object(ovn_helper.OvnProviderHelper, 'lb_create')
12411242
def test_ensure_loadbalancer_lb_not_found(
1242-
self, mock_lb_create, mock_update_status):
1243+
self, mock_lb_create, mock_listener_create, mock_update_status):
12431244
self.mock_find_ovn_lbs_with_retry.side_effect = [
12441245
idlutils.RowNotFound]
1245-
self.driver._ensure_loadbalancer(self.ref_lb0)
1246+
self.driver._ensure_loadbalancer(self.ref_lb_fully_populated)
12461247
mock_lb_create.assert_called_once_with(
1247-
self.driver._get_loadbalancer_request_info(self.ref_lb0),
1248+
self.driver._get_loadbalancer_request_info(
1249+
self.ref_lb_fully_populated),
12481250
)
1251+
mock_listener_create.assert_called_once_with(
1252+
self.driver._get_listener_request_info(
1253+
self.ref_lb_fully_populated.listeners[0]),
1254+
)
1255+
1256+
@mock.patch.object(ovn_helper.OvnProviderHelper,
1257+
'_update_status_to_octavia')
1258+
@mock.patch.object(ovn_helper.OvnProviderHelper, 'listener_create')
1259+
@mock.patch.object(ovn_helper.OvnProviderHelper, 'lb_create')
1260+
def test_ensure_loadbalancer_lb_no_listener_not_found(
1261+
self, mock_lb_create, mock_listener_create, mock_update_status):
1262+
self.mock_find_ovn_lbs_with_retry.side_effect = [
1263+
idlutils.RowNotFound]
1264+
self.ref_lb_fully_populated.listeners = data_models.Unset
1265+
self.driver._ensure_loadbalancer(self.ref_lb_fully_populated)
1266+
mock_lb_create.assert_called_once_with(
1267+
self.driver._get_loadbalancer_request_info(
1268+
self.ref_lb_fully_populated),
1269+
)
1270+
mock_listener_create.assert_not_called()
12491271

12501272
@mock.patch.object(ovn_helper.OvnProviderHelper,
12511273
'_update_status_to_octavia')
12521274
@mock.patch.object(ovn_helper.OvnProviderHelper,
12531275
'_get_current_operating_statuses')
1276+
@mock.patch.object(ovn_helper.OvnProviderHelper, 'listener_sync')
12541277
@mock.patch.object(ovn_helper.OvnProviderHelper, 'lb_sync')
12551278
def test_ensure_loadbalancer_lb_found(
1256-
self, mock_lb_sync, mock_get_status, mock_update_status):
1279+
self, mock_lb_sync, mock_listener_sync, mock_get_status,
1280+
mock_update_status):
12571281
self.mock_find_ovn_lbs_with_retry.return_value = [
12581282
self.ovn_lb]
1259-
self.driver._ensure_loadbalancer(self.ref_lb0)
1283+
self.driver._ensure_loadbalancer(self.ref_lb_fully_populated)
12601284
mock_lb_sync.assert_called_with(
1261-
self.driver._get_loadbalancer_request_info(self.ref_lb0),
1285+
self.driver._get_loadbalancer_request_info(
1286+
self.ref_lb_fully_populated),
1287+
self.ovn_lb
1288+
)
1289+
mock_listener_sync.assert_called_with(
1290+
self.driver._get_listener_request_info(
1291+
self.ref_lb_fully_populated.listeners[0]),
12621292
self.ovn_lb
12631293
)
12641294
mock_get_status.assert_called_with(self.ovn_lb)
12651295

1296+
@mock.patch.object(ovn_helper.OvnProviderHelper,
1297+
'_update_status_to_octavia')
1298+
@mock.patch.object(ovn_helper.OvnProviderHelper,
1299+
'_get_current_operating_statuses')
1300+
@mock.patch.object(ovn_helper.OvnProviderHelper, 'listener_sync')
1301+
@mock.patch.object(ovn_helper.OvnProviderHelper, 'lb_sync')
1302+
def test_ensure_loadbalancer_lb_no_listener_found(
1303+
self, mock_lb_sync, mock_listener_sync, mock_get_status,
1304+
mock_update_status):
1305+
self.mock_find_ovn_lbs_with_retry.return_value = [
1306+
self.ovn_lb]
1307+
self.ref_lb_fully_populated.listeners = data_models.Unset
1308+
self.driver._ensure_loadbalancer(self.ref_lb_fully_populated)
1309+
mock_lb_sync.assert_called_with(
1310+
self.driver._get_loadbalancer_request_info(
1311+
self.ref_lb_fully_populated),
1312+
self.ovn_lb
1313+
)
1314+
mock_listener_sync.assert_not_called()
1315+
12661316
@mock.patch.object(ovn_helper.OvnProviderHelper, 'get_octavia_lbs')
12671317
@mock.patch.object(clients, 'get_octavia_client')
12681318
def test_do_sync_no_loadbalancers(self, mock_get_octavia_client,
@@ -1276,16 +1326,40 @@ def test_do_sync_no_loadbalancers(self, mock_get_octavia_client,
12761326
self.driver.do_sync(**lb_filters)
12771327
mock_ensure_lb.assert_not_called()
12781328

1329+
@mock.patch.object(data_models.Listener, 'from_dict')
12791330
@mock.patch.object(o_driver_lib.DriverLibrary, 'get_loadbalancer')
12801331
@mock.patch.object(ovn_helper.OvnProviderHelper, 'get_octavia_lbs')
12811332
@mock.patch.object(clients, 'get_octavia_client')
12821333
def test_do_sync_with_loadbalancers(self,
12831334
mock_get_octavia_client,
12841335
mock_get_octavia_lbs,
1285-
mock_get_loadbalancer):
1336+
mock_get_loadbalancer,
1337+
mock_listener_from_dict):
12861338
lb = mock.MagicMock(id=self.ref_lb_fully_sync_populated.name)
12871339
mock_get_octavia_lbs.return_value = [lb]
12881340
mock_get_loadbalancer.return_value = self.ref_lb_fully_sync_populated
1341+
mock_listener_from_dict.return_value = self.ref_listener
1342+
lb_filters = {}
1343+
with mock.patch.object(self.driver, '_ensure_loadbalancer') \
1344+
as mock_ensure_lb:
1345+
self.driver.do_sync(**lb_filters)
1346+
mock_ensure_lb.assert_any_call(
1347+
self.ref_lb_fully_sync_populated)
1348+
1349+
@mock.patch.object(data_models.Listener, 'from_dict')
1350+
@mock.patch.object(o_driver_lib.DriverLibrary, 'get_loadbalancer')
1351+
@mock.patch.object(ovn_helper.OvnProviderHelper, 'get_octavia_lbs')
1352+
@mock.patch.object(clients, 'get_octavia_client')
1353+
def test_do_sync_with_loadbalancers_no_listener(
1354+
self,
1355+
mock_get_octavia_client,
1356+
mock_get_octavia_lbs,
1357+
mock_get_loadbalancer,
1358+
mock_listener_from_dict):
1359+
lb = mock.MagicMock(id=self.ref_lb_fully_sync_populated.name)
1360+
mock_get_octavia_lbs.return_value = [lb, lb]
1361+
mock_get_loadbalancer.return_value = self.ref_lb_fully_sync_populated
1362+
self.ref_lb_fully_sync_populated.listeners = data_models.Unset
12891363
lb_filters = {}
12901364
with mock.patch.object(self.driver, '_ensure_loadbalancer') \
12911365
as mock_ensure_lb:

ovn_octavia_provider/tests/unit/test_helper.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,67 @@ def test_listener_delete_ovn_lb_empty_ovn_lb_not_found(self, lb_empty):
18861886
mock.call('Load_Balancer', self.ovn_lb.uuid,
18871887
('vips', {}))])
18881888

1889+
@mock.patch.object(ovn_helper.OvnProviderHelper, '_refresh_lb_vips')
1890+
def test_listener_sync_listener_same_in_externals_ids(self, refresh_vips):
1891+
self.listener['admin_state_up'] = True
1892+
listener_key = 'listener_%s' % self.listener_id
1893+
self.ovn_lb.external_ids[listener_key] = f"80:pool_{self.pool_id}"
1894+
self.helper.listener_sync(self.listener, self.ovn_lb)
1895+
refresh_vips.assert_called_once_with(
1896+
self.ovn_lb, self.ovn_lb.external_ids, is_sync=True)
1897+
self.helper.ovn_nbdb_api.db_set.assert_not_called()
1898+
1899+
@mock.patch.object(ovn_helper.OvnProviderHelper, '_refresh_lb_vips')
1900+
def test_listener_sync_listener_diff_in_externals_ids(self, refresh_vips):
1901+
self.listener['admin_state_up'] = True
1902+
listener_key = 'listener_%s' % self.listener_id
1903+
external_ids = copy.deepcopy(self.ovn_lb.external_ids)
1904+
self.ovn_lb.external_ids[listener_key] = ''
1905+
self.helper.listener_sync(self.listener, self.ovn_lb)
1906+
refresh_vips.assert_called_once_with(
1907+
self.ovn_lb, external_ids, is_sync=True)
1908+
expected_calls = [
1909+
mock.call('Load_Balancer', self.ovn_lb.uuid, ('external_ids', {
1910+
f"listener_{self.listener_id}": f"80:pool_{self.pool_id}"}))
1911+
]
1912+
self.helper.ovn_nbdb_api.db_set.assert_has_calls(
1913+
expected_calls)
1914+
1915+
@mock.patch.object(ovn_helper.OvnProviderHelper, '_execute_commands')
1916+
def test_listener_sync_exception(self, execute_commands):
1917+
execute_commands.side_effect = [RuntimeError('a fail')]
1918+
self.ovn_lb.external_ids.pop('listener_%s' % self.listener_id)
1919+
self.listener['admin_state_up'] = True
1920+
with mock.patch.object(ovn_helper, 'LOG') as m_l:
1921+
self.assertIsNone(self.helper.listener_sync(
1922+
self.listener, self.ovn_lb))
1923+
m_l.exception.assert_called_once_with(
1924+
'Failed to execute commands for listener sync: a fail')
1925+
1926+
@mock.patch.object(ovn_helper.OvnProviderHelper, '_refresh_lb_vips')
1927+
def test_listener_sync_refresh_vips_exception(self, refresh_lb_vips):
1928+
refresh_lb_vips.side_effect = [RuntimeError('a fail')]
1929+
self.ovn_lb.external_ids.pop('listener_%s' % self.listener_id)
1930+
self.listener['admin_state_up'] = True
1931+
with mock.patch.object(ovn_helper, 'LOG') as m_l:
1932+
self.assertIsNone(self.helper.listener_sync(
1933+
self.listener, self.ovn_lb))
1934+
m_l.exception.assert_called_once_with(
1935+
'Failed to refresh LB VIPs: a fail')
1936+
1937+
@mock.patch.object(ovn_helper.OvnProviderHelper, '_refresh_lb_vips')
1938+
def test_listener_sync_listener_not_in_externals_ids(
1939+
self, refresh_vips):
1940+
self.ovn_lb.external_ids.pop('listener_%s' % self.listener_id)
1941+
self.listener['admin_state_up'] = True
1942+
self.helper.listener_sync(self.listener, self.ovn_lb)
1943+
expected_calls = [
1944+
mock.call('Load_Balancer', self.ovn_lb.uuid, ('external_ids', {
1945+
f"listener_{self.listener_id}": f"80:pool_{self.pool_id}"}))
1946+
]
1947+
self.helper.ovn_nbdb_api.db_set.assert_has_calls(
1948+
expected_calls)
1949+
18891950
def test_pool_create(self):
18901951
status = self.helper.pool_create(self.pool)
18911952
self.assertEqual(status['loadbalancers'][0]['provisioning_status'],

0 commit comments

Comments
 (0)