Skip to content

Commit e7068ae

Browse files
Add Pool sync logic
This patch adds the logic to sync a Pool 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: I7293f4d29683d143fdc4ad6778d090a476c36e7c
1 parent 318160f commit e7068ae

File tree

4 files changed

+199
-7
lines changed

4 files changed

+199
-7
lines changed

ovn_octavia_provider/driver.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,26 @@ def _get_listener_request_info(self, listener):
119119
'admin_state_up': admin_state_up}
120120
return request_info
121121

122+
def _get_pool_request_info(self, pool):
123+
self._check_for_supported_protocols(pool.protocol)
124+
self._check_for_supported_algorithms(pool.lb_algorithm)
125+
admin_state_up = pool.admin_state_up
126+
if isinstance(admin_state_up, o_datamodels.UnsetType):
127+
admin_state_up = True
128+
request_info = {'id': pool.pool_id,
129+
'loadbalancer_id': pool.loadbalancer_id,
130+
'protocol': pool.protocol,
131+
'lb_algorithm': pool.lb_algorithm,
132+
'listener_id': pool.listener_id,
133+
'admin_state_up': admin_state_up}
134+
if not isinstance(
135+
pool.session_persistence, o_datamodels.UnsetType):
136+
self._check_for_supported_session_persistence(
137+
pool.session_persistence)
138+
request_info['session_persistence'] = pool.session_persistence
139+
140+
return request_info
141+
122142
def loadbalancer_create(self, loadbalancer):
123143
request = {'type': ovn_const.REQ_TYPE_LB_CREATE,
124144
'info': self._get_loadbalancer_request_info(
@@ -612,7 +632,7 @@ def _ensure_loadbalancer(self, loadbalancer):
612632
except idlutils.RowNotFound:
613633
LOG.debug(f"OVN loadbalancer {loadbalancer.loadbalancer_id} "
614634
"not found. Start create process.")
615-
# TODO(froyo): By now just syncing LB and listener only
635+
# TODO(froyo): By now just syncing LB, listener and pool only
616636
status = self._ovn_helper.lb_create(
617637
self._get_loadbalancer_request_info(loadbalancer))
618638

@@ -622,6 +642,12 @@ def _ensure_loadbalancer(self, loadbalancer):
622642
status_listener = self._ovn_helper.listener_create(
623643
self._get_listener_request_info(listener))
624644
status[constants.LISTENERS].append(status_listener)
645+
if not isinstance(loadbalancer.pools, o_datamodels.UnsetType):
646+
status[constants.POOLS] = []
647+
for pool in loadbalancer.pools:
648+
status_pool = self._ovn_helper.pool_create(
649+
self._get_pool_request_info(pool))
650+
status[constants.POOLS].append(status_pool)
625651
self._ovn_helper._update_status_to_octavia(status)
626652
else:
627653
# Load Balancer found, check LB and listener/pool/member/hms
@@ -638,6 +664,11 @@ def _ensure_loadbalancer(self, loadbalancer):
638664
for listener in loadbalancer.listeners:
639665
self._ovn_helper.listener_sync(
640666
self._get_listener_request_info(listener), ovn_lb)
667+
# Pool
668+
if not isinstance(loadbalancer.pools, o_datamodels.UnsetType):
669+
for pool in loadbalancer.pools:
670+
self._ovn_helper.pool_sync(
671+
self._get_pool_request_info(pool), ovn_lb)
641672
status = self._ovn_helper._get_current_operating_statuses(
642673
ovn_lb)
643674
self._ovn_helper._update_status_to_octavia(status)
@@ -659,4 +690,10 @@ def do_sync(self, **lb_filters):
659690
for listener in listeners
660691
] if listeners else o_datamodels.Unset
661692

693+
pools = provider_lb.pools or []
694+
provider_lb.pools = [
695+
o_datamodels.Pool.from_dict(pool)
696+
for pool in pools
697+
] if pools else o_datamodels.Unset
698+
662699
self._ensure_loadbalancer(provider_lb)

ovn_octavia_provider/helper.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,65 @@ def _update_protocol_if_needed(self, listener, ovn_lb, commands):
663663
)
664664
)
665665

666+
def _prepare_external_ids(self, pool, ovn_lb):
667+
"""Prepare the updated external_ids for the LoadBalancer."""
668+
external_ids = copy.deepcopy(ovn_lb.external_ids)
669+
pool_key = self._get_pool_key(
670+
pool[constants.ID],
671+
is_enabled=pool[constants.ADMIN_STATE_UP])
672+
external_ids[pool_key] = ''
673+
674+
if pool[constants.LISTENER_ID]:
675+
self._update_listener_association(
676+
pool, ovn_lb, external_ids, pool_key)
677+
678+
return external_ids
679+
680+
def _update_listener_association(self, pool, ovn_lb, external_ids,
681+
pool_key):
682+
"""Update the listener association in external_ids."""
683+
listener_key = self._get_listener_key(pool[constants.LISTENER_ID])
684+
if listener_key in ovn_lb.external_ids:
685+
pool_key_enable = self._get_pool_key(pool[constants.ID],
686+
is_enabled=True)
687+
pool_key_disable = self._get_pool_key(pool[constants.ID],
688+
is_enabled=False)
689+
690+
if pool[constants.ID] in external_ids[listener_key]:
691+
# Remove existing pool keys before adding the updated key
692+
external_ids[listener_key] = (
693+
external_ids[listener_key]
694+
.replace(pool_key_disable, '')
695+
.replace(pool_key_enable, '')
696+
)
697+
698+
external_ids[listener_key] += str(pool_key)
699+
700+
def _extract_persistence_timeout(self, pool):
701+
"""Extract persistence timeout value from the pool, if available."""
702+
if pool.get(constants.SESSION_PERSISTENCE):
703+
return pool[constants.SESSION_PERSISTENCE].get(
704+
constants.PERSISTENCE_TIMEOUT, '360')
705+
return None
706+
707+
def _add_external_ids_command(self, commands, ovn_lb, external_ids):
708+
"""Add a command to update the external_ids of the LoadBalancer."""
709+
commands.append(
710+
self.ovn_nbdb_api.db_set('Load_Balancer', ovn_lb.uuid,
711+
('external_ids', external_ids))
712+
)
713+
714+
def _add_persistence_timeout_command(self, commands, ovn_lb,
715+
persistence_timeout):
716+
"""Add command to update persistence timeout in LoadBalancer."""
717+
options = copy.deepcopy(ovn_lb.options)
718+
options[ovn_const.AFFINITY_TIMEOUT] = str(persistence_timeout)
719+
if ovn_lb.options != options:
720+
commands.append(
721+
self.ovn_nbdb_api.db_set('Load_Balancer', ovn_lb.uuid,
722+
('options', options))
723+
)
724+
666725
def _lb_status(self, loadbalancer, provisioning_status, operating_status):
667726
"""Return status for the LoadBalancer."""
668727
return {
@@ -2115,7 +2174,11 @@ def pool_create(self, pool):
21152174
external_ids[pool_key] = ''
21162175
if pool[constants.LISTENER_ID]:
21172176
listener_key = self._get_listener_key(pool[constants.LISTENER_ID])
2118-
if listener_key in ovn_lb.external_ids:
2177+
# NOTE(froyo): checking is not already when ovn-db-sync-tool is
2178+
# triggered, because listener_create could be added already if
2179+
# pool is considered as default one
2180+
if listener_key in ovn_lb.external_ids and \
2181+
str(pool_key) not in external_ids[listener_key]:
21192182
external_ids[listener_key] = str(
21202183
external_ids[listener_key]) + str(pool_key)
21212184
persistence_timeout = None
@@ -2174,6 +2237,29 @@ def pool_create(self, pool):
21742237

21752238
return status
21762239

2240+
def pool_sync(self, pool, ovn_lb):
2241+
"""Sync Pool object with an OVN LoadBalancer
2242+
2243+
The method performs the following steps:
2244+
1. Update pool key on OVN Loadbalancer external_ids if needed
2245+
2. Update OVN LoadBalancer options from Pool info
2246+
2247+
:param pool: The source pool object from Octavia DB
2248+
:param ovn_lb: The OVN LoadBalancer object that needs to be sync
2249+
"""
2250+
external_ids = self._prepare_external_ids(pool, ovn_lb)
2251+
persistence_timeout = self._extract_persistence_timeout(pool)
2252+
2253+
try:
2254+
commands = []
2255+
self._add_external_ids_command(commands, ovn_lb, external_ids)
2256+
if persistence_timeout:
2257+
self._add_persistence_timeout_command(commands, ovn_lb,
2258+
persistence_timeout)
2259+
self._execute_commands(commands)
2260+
except Exception as e:
2261+
LOG.exception(f"Failed to execute commands for pool sync: {e}")
2262+
21772263
def pool_delete(self, pool):
21782264
status = {
21792265
constants.POOLS: [

ovn_octavia_provider/tests/unit/test_driver.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,10 +1237,12 @@ 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, 'pool_create')
12401241
@mock.patch.object(ovn_helper.OvnProviderHelper, 'listener_create')
12411242
@mock.patch.object(ovn_helper.OvnProviderHelper, 'lb_create')
12421243
def test_ensure_loadbalancer_lb_not_found(
1243-
self, mock_lb_create, mock_listener_create, mock_update_status):
1244+
self, mock_lb_create, mock_listener_create, mock_pool_create,
1245+
mock_update_status):
12441246
self.mock_find_ovn_lbs_with_retry.side_effect = [
12451247
idlutils.RowNotFound]
12461248
self.driver._ensure_loadbalancer(self.ref_lb_fully_populated)
@@ -1252,16 +1254,21 @@ def test_ensure_loadbalancer_lb_not_found(
12521254
self.driver._get_listener_request_info(
12531255
self.ref_lb_fully_populated.listeners[0]),
12541256
)
1257+
mock_pool_create.assert_called_once_with(
1258+
self.driver._get_pool_request_info(
1259+
self.ref_lb_fully_populated.pools[0]),
1260+
)
12551261

12561262
@mock.patch.object(ovn_helper.OvnProviderHelper,
12571263
'_update_status_to_octavia')
12581264
@mock.patch.object(ovn_helper.OvnProviderHelper, 'listener_create')
12591265
@mock.patch.object(ovn_helper.OvnProviderHelper, 'lb_create')
1260-
def test_ensure_loadbalancer_lb_no_listener_not_found(
1266+
def test_ensure_loadbalancer_lb_no_listener_no_pool_not_found(
12611267
self, mock_lb_create, mock_listener_create, mock_update_status):
12621268
self.mock_find_ovn_lbs_with_retry.side_effect = [
12631269
idlutils.RowNotFound]
12641270
self.ref_lb_fully_populated.listeners = data_models.Unset
1271+
self.ref_lb_fully_populated.pools = data_models.Unset
12651272
self.driver._ensure_loadbalancer(self.ref_lb_fully_populated)
12661273
mock_lb_create.assert_called_once_with(
12671274
self.driver._get_loadbalancer_request_info(
@@ -1273,11 +1280,12 @@ def test_ensure_loadbalancer_lb_no_listener_not_found(
12731280
'_update_status_to_octavia')
12741281
@mock.patch.object(ovn_helper.OvnProviderHelper,
12751282
'_get_current_operating_statuses')
1283+
@mock.patch.object(ovn_helper.OvnProviderHelper, 'pool_sync')
12761284
@mock.patch.object(ovn_helper.OvnProviderHelper, 'listener_sync')
12771285
@mock.patch.object(ovn_helper.OvnProviderHelper, 'lb_sync')
12781286
def test_ensure_loadbalancer_lb_found(
1279-
self, mock_lb_sync, mock_listener_sync, mock_get_status,
1280-
mock_update_status):
1287+
self, mock_lb_sync, mock_listener_sync, mock_pool_sync,
1288+
mock_get_status, mock_update_status):
12811289
self.mock_find_ovn_lbs_with_retry.return_value = [
12821290
self.ovn_lb]
12831291
self.driver._ensure_loadbalancer(self.ref_lb_fully_populated)
@@ -1327,17 +1335,20 @@ def test_do_sync_no_loadbalancers(self, mock_get_octavia_client,
13271335
mock_ensure_lb.assert_not_called()
13281336

13291337
@mock.patch.object(data_models.Listener, 'from_dict')
1338+
@mock.patch.object(data_models.Pool, 'from_dict')
13301339
@mock.patch.object(o_driver_lib.DriverLibrary, 'get_loadbalancer')
13311340
@mock.patch.object(ovn_helper.OvnProviderHelper, 'get_octavia_lbs')
13321341
@mock.patch.object(clients, 'get_octavia_client')
13331342
def test_do_sync_with_loadbalancers(self,
13341343
mock_get_octavia_client,
13351344
mock_get_octavia_lbs,
13361345
mock_get_loadbalancer,
1346+
mock_pool_from_dict,
13371347
mock_listener_from_dict):
13381348
lb = mock.MagicMock(id=self.ref_lb_fully_sync_populated.name)
13391349
mock_get_octavia_lbs.return_value = [lb]
13401350
mock_get_loadbalancer.return_value = self.ref_lb_fully_sync_populated
1351+
mock_pool_from_dict.return_value = self.ref_pool_with_hm
13411352
mock_listener_from_dict.return_value = self.ref_listener
13421353
lb_filters = {}
13431354
with mock.patch.object(self.driver, '_ensure_loadbalancer') \
@@ -1350,7 +1361,7 @@ def test_do_sync_with_loadbalancers(self,
13501361
@mock.patch.object(o_driver_lib.DriverLibrary, 'get_loadbalancer')
13511362
@mock.patch.object(ovn_helper.OvnProviderHelper, 'get_octavia_lbs')
13521363
@mock.patch.object(clients, 'get_octavia_client')
1353-
def test_do_sync_with_loadbalancers_no_listener(
1364+
def test_do_sync_with_loadbalancers_no_listener_no_pool(
13541365
self,
13551366
mock_get_octavia_client,
13561367
mock_get_octavia_lbs,
@@ -1360,6 +1371,7 @@ def test_do_sync_with_loadbalancers_no_listener(
13601371
mock_get_octavia_lbs.return_value = [lb, lb]
13611372
mock_get_loadbalancer.return_value = self.ref_lb_fully_sync_populated
13621373
self.ref_lb_fully_sync_populated.listeners = data_models.Unset
1374+
self.ref_lb_fully_sync_populated.pools = data_models.Unset
13631375
lb_filters = {}
13641376
with mock.patch.object(self.driver, '_ensure_loadbalancer') \
13651377
as mock_ensure_lb:

ovn_octavia_provider/tests/unit/test_helper.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2209,6 +2209,63 @@ def test_pool_delete_ovn_lb_empty_lb_not_empty(self, lb_empty):
22092209
self.helper.ovn_nbdb_api.lb_del.assert_called_once_with(
22102210
self.ovn_lb.uuid)
22112211

2212+
def test_pool_sync_exception(self):
2213+
self.helper.ovn_nbdb_api.db_set.side_effect = [
2214+
RuntimeError("ERROR_MSG"), RuntimeError("ERROR_MSG")]
2215+
with mock.patch.object(ovn_helper, 'LOG') as m_l:
2216+
self.assertIsNone(self.helper.pool_sync(self.pool, self.ovn_lb))
2217+
m_l.exception.assert_called_once_with(
2218+
'Failed to execute commands for pool sync: ERROR_MSG')
2219+
2220+
def test_pool_sync(self):
2221+
self.helper.pool_sync(self.pool, self.ovn_lb)
2222+
listener_key_value = (f"80:pool_{self.pool_id}:"
2223+
f"{ovn_const.DISABLED_RESOURCE_SUFFIX}")
2224+
expected_calls = [mock.call(
2225+
'Load_Balancer', self.ovn_lb.uuid,
2226+
('external_ids', {
2227+
ovn_const.LB_EXT_IDS_VIP_KEY: mock.ANY,
2228+
ovn_const.LB_EXT_IDS_VIP_FIP_KEY: '123.123.123.123',
2229+
ovn_const.LB_EXT_IDS_VIP_PORT_ID_KEY: mock.ANY,
2230+
'enabled': True,
2231+
f"pool_{self.pool_id}": mock.ANY,
2232+
f"listener_{self.listener_id}": listener_key_value,
2233+
ovn_const.OVN_MEMBER_STATUS_KEY: '{"%s": "%s"}'
2234+
% (self.member_id, constants.NO_MONITOR),
2235+
f"pool_{self.pool_id}:{ovn_const.DISABLED_RESOURCE_SUFFIX}": ''
2236+
}))
2237+
]
2238+
self.helper.ovn_nbdb_api.db_set.assert_has_calls(
2239+
expected_calls)
2240+
2241+
def test_pool_sync_press_key(self):
2242+
self.pool[constants.SESSION_PERSISTENCE] = {
2243+
constants.PERSISTENCE_TIMEOUT: '360'
2244+
}
2245+
self.ovn_lb.options = {'a': 1}
2246+
self.helper.pool_sync(self.pool, self.ovn_lb)
2247+
listener_key_value = (f"80:pool_{self.pool_id}:"
2248+
f"{ovn_const.DISABLED_RESOURCE_SUFFIX}")
2249+
expected_calls = [mock.call(
2250+
'Load_Balancer', self.ovn_lb.uuid,
2251+
('external_ids', {
2252+
ovn_const.LB_EXT_IDS_VIP_KEY: mock.ANY,
2253+
ovn_const.LB_EXT_IDS_VIP_FIP_KEY: '123.123.123.123',
2254+
ovn_const.LB_EXT_IDS_VIP_PORT_ID_KEY: mock.ANY,
2255+
'enabled': True,
2256+
f"pool_{self.pool_id}": mock.ANY,
2257+
f"listener_{self.listener_id}": listener_key_value,
2258+
ovn_const.OVN_MEMBER_STATUS_KEY: '{"%s": "%s"}'
2259+
% (self.member_id, constants.NO_MONITOR),
2260+
f"pool_{self.pool_id}:{ovn_const.DISABLED_RESOURCE_SUFFIX}": ''
2261+
})),
2262+
mock.call(
2263+
'Load_Balancer', self.ovn_lb.uuid,
2264+
('options', {'a': 1, 'affinity_timeout': '360'}))
2265+
]
2266+
self.helper.ovn_nbdb_api.db_set.assert_has_calls(
2267+
expected_calls)
2268+
22122269
@mock.patch('ovn_octavia_provider.common.clients.get_neutron_client')
22132270
def test_member_create_disabled(self, net_cli):
22142271
net_cli.return_value.show_subnet.side_effect = [idlutils.RowNotFound]

0 commit comments

Comments
 (0)