From 382ddb0329f93873e25a55be65bf43000332a21a Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Wed, 7 Jun 2023 11:29:02 +0200 Subject: [PATCH] Add support for SOURCE_IP session persistence This patch adds support to configure ovn loadbalancer affinity_timeout option based on the pool session persistence timeout. Change-Id: I07c8f3492e62576f66008e8ea1ef9846bed8c6fa --- ovn_octavia_provider/common/constants.py | 4 + ovn_octavia_provider/driver.py | 21 +++++ ovn_octavia_provider/helper.py | 94 +++++++++++++------ .../tests/unit/test_driver.py | 55 ++++++++--- .../session-persistence-b409428a8907f542.yaml | 7 ++ 5 files changed, 142 insertions(+), 39 deletions(-) create mode 100644 releasenotes/notes/session-persistence-b409428a8907f542.yaml diff --git a/ovn_octavia_provider/common/constants.py b/ovn_octavia_provider/common/constants.py index 52dc060e..bcc26488 100644 --- a/ovn_octavia_provider/common/constants.py +++ b/ovn_octavia_provider/common/constants.py @@ -112,3 +112,7 @@ HM_EVENT_MEMBER_PORT_OFFLINE = ['offline'] # max timeout for request MAX_TIMEOUT_REQUEST = 5 + +AFFINITY_TIMEOUT = "affinity_timeout" +# This driver only supports SOURCE_IP sesssion persistency option +OVN_NATIVE_SESSION_PERSISTENCE = [constants.SESSION_PERSISTENCE_SOURCE_IP] diff --git a/ovn_octavia_provider/driver.py b/ovn_octavia_provider/driver.py index 1cfcdce1..6623af43 100644 --- a/ovn_octavia_provider/driver.py +++ b/ovn_octavia_provider/driver.py @@ -68,6 +68,16 @@ class OvnProviderDriver(driver_base.ProviderDriver): user_fault_string=msg, operator_fault_string=msg) + def _check_for_supported_session_persistence(self, session): + if (session and + session.get("type") not in + ovn_const.OVN_NATIVE_SESSION_PERSISTENCE): + msg = _('OVN provider does not support %s session persistence. ' + 'Only SOURCE_IP type is supported.') % session.type + raise driver_exceptions.UnsupportedOptionError( + user_fault_string=msg, + operator_fault_string=msg) + def _check_for_allowed_cidrs(self, allowed_cidrs): # TODO(haleyb): add support for this if isinstance(allowed_cidrs, o_datamodels.UnsetType): @@ -140,6 +150,11 @@ class OvnProviderDriver(driver_base.ProviderDriver): 'admin_state_up': admin_state_up} request = {'type': ovn_const.REQ_TYPE_POOL_CREATE, 'info': request_info} + if not isinstance( + pool.session_persistence, o_datamodels.UnsetType): + self._check_for_supported_session_persistence( + pool.session_persistence) + request['info']['session_persistence'] = pool.session_persistence self._ovn_helper.add_request(request) if pool.healthmonitor is not None and not isinstance( pool.healthmonitor, o_datamodels.UnsetType): @@ -170,6 +185,12 @@ class OvnProviderDriver(driver_base.ProviderDriver): if not isinstance(new_pool.admin_state_up, o_datamodels.UnsetType): request_info['admin_state_up'] = new_pool.admin_state_up + if not isinstance( + new_pool.session_persistence, o_datamodels.UnsetType): + self._check_for_supported_session_persistence( + new_pool.session_persistence) + request_info['session_persistence'] = ( + new_pool.session_persistence) request = {'type': ovn_const.REQ_TYPE_POOL_UPDATE, 'info': request_info} self._ovn_helper.add_request(request) diff --git a/ovn_octavia_provider/helper.py b/ovn_octavia_provider/helper.py index 65f52714..9b9c0251 100644 --- a/ovn_octavia_provider/helper.py +++ b/ovn_octavia_provider/helper.py @@ -1605,10 +1605,25 @@ class OvnProviderHelper(): if listener_key in ovn_lb.external_ids: external_ids[listener_key] = str( external_ids[listener_key]) + str(pool_key) + persistence_timeout = None + if pool.get(constants.SESSION_PERSISTENCE): + persistence_timeout = pool[constants.SESSION_PERSISTENCE].get( + constants.PERSISTENCE_TIMEOUT, '360') try: - self.ovn_nbdb_api.db_set( + commands = [] + commands.append(self.ovn_nbdb_api.db_set( 'Load_Balancer', ovn_lb.uuid, - ('external_ids', external_ids)).execute(check_error=True) + ('external_ids', external_ids))) + + if persistence_timeout: + options = copy.deepcopy(ovn_lb.options) + options[ovn_const.AFFINITY_TIMEOUT] = str(persistence_timeout) + + commands.append(self.ovn_nbdb_api.db_set( + 'Load_Balancer', ovn_lb.uuid, + ('options', options))) + + self._execute_commands(commands) # Pool status will be set to Online after a member is added to it # or when it is created with listener. @@ -1693,6 +1708,11 @@ class OvnProviderHelper(): 'Load_Balancer', ovn_lb.uuid, 'external_ids', (pool_key_when_disabled))) + if ovn_const.AFFINITY_TIMEOUT in ovn_lb.options: + commands.append( + self.ovn_nbdb_api.db_remove('Load_Balancer', ovn_lb.uuid, + 'options', + (ovn_const.AFFINITY_TIMEOUT))) commands.extend( self._clean_lb_if_empty( ovn_lb, pool[constants.LOADBALANCER_ID], external_ids)[0]) @@ -1725,7 +1745,8 @@ class OvnProviderHelper(): status = { constants.POOLS: [pool_status], constants.LOADBALANCERS: [lbalancer_status]} - if constants.ADMIN_STATE_UP not in pool: + if (constants.ADMIN_STATE_UP not in pool and + constants.SESSION_PERSISTENCE not in pool): return status try: ovn_lb = self._find_ovn_lbs( @@ -1746,37 +1767,54 @@ class OvnProviderHelper(): p_key_to_add = {} pool_listeners = [] + commands = [] try: pool_listeners = self._get_pool_listeners(ovn_lb, pool_key) - if pool[constants.ADMIN_STATE_UP]: - if p_key_when_disabled in external_ids: - p_key_to_add[pool_key] = external_ids[p_key_when_disabled] - external_ids[pool_key] = external_ids[p_key_when_disabled] - del external_ids[p_key_when_disabled] - p_key_to_remove = p_key_when_disabled - else: - if pool_key in external_ids: - p_key_to_add[p_key_when_disabled] = external_ids[pool_key] - external_ids[p_key_when_disabled] = external_ids[pool_key] - del external_ids[pool_key] - p_key_to_remove = pool_key + admin_state_up = pool.get(constants.ADMIN_STATE_UP) + if admin_state_up is not None: + if admin_state_up: + if p_key_when_disabled in external_ids: + p_key_to_add[pool_key] = external_ids[ + p_key_when_disabled] + external_ids[pool_key] = external_ids[ + p_key_when_disabled] + del external_ids[p_key_when_disabled] + p_key_to_remove = p_key_when_disabled + else: + if pool_key in external_ids: + p_key_to_add[p_key_when_disabled] = external_ids[ + pool_key] + external_ids[p_key_when_disabled] = external_ids[ + pool_key] + del external_ids[pool_key] + p_key_to_remove = pool_key - if p_key_to_remove: - commands = [] - commands.append( - self.ovn_nbdb_api.db_remove( - 'Load_Balancer', ovn_lb.uuid, 'external_ids', - (p_key_to_remove))) + if p_key_to_remove: + commands.append( + self.ovn_nbdb_api.db_remove( + 'Load_Balancer', ovn_lb.uuid, 'external_ids', + (p_key_to_remove))) - commands.append( - self.ovn_nbdb_api.db_set( - 'Load_Balancer', ovn_lb.uuid, - ('external_ids', p_key_to_add))) + commands.append( + self.ovn_nbdb_api.db_set( + 'Load_Balancer', ovn_lb.uuid, + ('external_ids', p_key_to_add))) + + commands.extend( + self._refresh_lb_vips(ovn_lb, external_ids)) + + if pool.get(constants.SESSION_PERSISTENCE): + new_timeout = pool[constants.SESSION_PERSISTENCE].get( + constants.PERSISTENCE_TIMEOUT, '360') + options = copy.deepcopy(ovn_lb.options) + options[ovn_const.AFFINITY_TIMEOUT] = str(new_timeout) + commands.append(self.ovn_nbdb_api.db_set( + 'Load_Balancer', ovn_lb.uuid, + ('options', options))) + + self._execute_commands(commands) - commands.extend( - self._refresh_lb_vips(ovn_lb, external_ids)) - self._execute_commands(commands) if pool[constants.ADMIN_STATE_UP]: operating_status = constants.ONLINE else: diff --git a/ovn_octavia_provider/tests/unit/test_driver.py b/ovn_octavia_provider/tests/unit/test_driver.py index 475a5fd9..bb270c5f 100644 --- a/ovn_octavia_provider/tests/unit/test_driver.py +++ b/ovn_octavia_provider/tests/unit/test_driver.py @@ -89,7 +89,7 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): members=[self.ref_member], pool_id=self.pool_id, protocol='TCP', - session_persistence={'type': 'fix'}) + session_persistence={'type': 'SOURCE_IP'}) self.ref_pool = data_models.Pool( admin_state_up=True, description='pool', @@ -100,7 +100,7 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): members=[self.ref_member], pool_id=self.pool_id, protocol='TCP', - session_persistence={'type': 'fix'}) + session_persistence={'type': 'SOURCE_IP'}) self.ref_http_pool = data_models.Pool( admin_state_up=True, description='pool', @@ -689,7 +689,8 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): 'listener_id': self.ref_pool.listener_id, 'protocol': self.ref_pool.protocol, 'lb_algorithm': constants.LB_ALGORITHM_SOURCE_IP_PORT, - 'admin_state_up': self.ref_pool.admin_state_up} + 'admin_state_up': self.ref_pool.admin_state_up, + 'session_persistence': {'type': 'SOURCE_IP'}} info_member = { 'id': self.ref_member.member_id, 'address': self.ref_member.address, @@ -758,7 +759,8 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): 'listener_id': self.ref_pool.listener_id, 'protocol': self.ref_pool.protocol, 'lb_algorithm': constants.LB_ALGORITHM_SOURCE_IP_PORT, - 'admin_state_up': self.ref_pool.admin_state_up} + 'admin_state_up': self.ref_pool.admin_state_up, + 'session_persistence': {'type': 'SOURCE_IP'}} info_member = { 'id': self.ref_member.member_id, 'address': self.ref_member.address, @@ -866,7 +868,8 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): 'listener_id': self.ref_pool.listener_id, 'protocol': self.ref_pool.protocol, 'lb_algorithm': constants.LB_ALGORITHM_SOURCE_IP_PORT, - 'admin_state_up': self.ref_pool.admin_state_up} + 'admin_state_up': self.ref_pool.admin_state_up, + 'session_persistence': {'type': 'SOURCE_IP'}} expected_dict = {'type': ovn_const.REQ_TYPE_POOL_CREATE, 'info': info} self.driver.pool_create(self.ref_pool) @@ -879,7 +882,8 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): 'listener_id': self.ref_pool.listener_id, 'protocol': self.ref_pool.protocol, 'lb_algorithm': constants.LB_ALGORITHM_SOURCE_IP_PORT, - 'admin_state_up': self.ref_pool.admin_state_up} + 'admin_state_up': self.ref_pool.admin_state_up, + 'session_persistence': {'type': 'SOURCE_IP'}} info_hm = {'id': self.ref_health_monitor.healthmonitor_id, 'pool_id': self.ref_health_monitor.pool_id, 'type': self.ref_health_monitor.type, @@ -905,7 +909,21 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): 'protocol': self.ref_pool.protocol, 'lb_algorithm': constants.LB_ALGORITHM_SOURCE_IP_PORT, 'listener_id': self.ref_pool.listener_id, - 'admin_state_up': True} + 'admin_state_up': True, + 'session_persistence': {'type': 'SOURCE_IP'}} + expected_dict = {'type': ovn_const.REQ_TYPE_POOL_CREATE, + 'info': info} + self.driver.pool_create(self.ref_pool) + self.mock_add_request.assert_called_once_with(expected_dict) + + def test_pool_create_unset_session_persistence(self): + self.ref_pool.session_persistence = data_models.UnsetType() + info = {'id': self.ref_pool.pool_id, + 'loadbalancer_id': self.ref_pool.loadbalancer_id, + 'protocol': self.ref_pool.protocol, + 'lb_algorithm': constants.LB_ALGORITHM_SOURCE_IP_PORT, + 'listener_id': self.ref_pool.listener_id, + 'admin_state_up': self.ref_pool.admin_state_up} expected_dict = {'type': ovn_const.REQ_TYPE_POOL_CREATE, 'info': info} self.driver.pool_create(self.ref_pool) @@ -956,7 +974,8 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): info = {'id': self.ref_update_pool.pool_id, 'loadbalancer_id': self.ref_update_pool.loadbalancer_id, 'protocol': self.ref_pool.protocol, - 'admin_state_up': self.ref_update_pool.admin_state_up} + 'admin_state_up': self.ref_update_pool.admin_state_up, + 'session_persistence': {'type': 'SOURCE_IP'}} expected_dict = {'type': ovn_const.REQ_TYPE_POOL_UPDATE, 'info': info} self.driver.pool_update(self.ref_pool, self.ref_update_pool) @@ -967,7 +986,8 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): info = {'id': self.ref_update_pool.pool_id, 'loadbalancer_id': self.ref_update_pool.loadbalancer_id, 'protocol': self.ref_pool.protocol, - 'admin_state_up': self.ref_update_pool.admin_state_up} + 'admin_state_up': self.ref_update_pool.admin_state_up, + 'session_persistence': {'type': 'SOURCE_IP'}} expected_dict = {'type': ovn_const.REQ_TYPE_POOL_UPDATE, 'info': info} self.driver.pool_update(self.ref_pool, self.ref_update_pool) @@ -978,7 +998,8 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): info = {'id': self.ref_update_pool.pool_id, 'loadbalancer_id': self.ref_update_pool.loadbalancer_id, 'protocol': self.ref_pool.protocol, - 'admin_state_up': self.ref_update_pool.admin_state_up} + 'admin_state_up': self.ref_update_pool.admin_state_up, + 'session_persistence': {'type': 'SOURCE_IP'}} expected_dict = {'type': ovn_const.REQ_TYPE_POOL_UPDATE, 'info': info} self.driver.pool_update(self.ref_pool, self.ref_update_pool) @@ -988,7 +1009,19 @@ class TestOvnProviderDriver(ovn_base.TestOvnOctaviaBase): self.ref_update_pool.admin_state_up = data_models.UnsetType() info = {'id': self.ref_update_pool.pool_id, 'loadbalancer_id': self.ref_update_pool.loadbalancer_id, - 'protocol': self.ref_pool.protocol} + 'protocol': self.ref_pool.protocol, + 'session_persistence': {'type': 'SOURCE_IP'}} + expected_dict = {'type': ovn_const.REQ_TYPE_POOL_UPDATE, + 'info': info} + self.driver.pool_update(self.ref_pool, self.ref_update_pool) + self.mock_add_request.assert_called_once_with(expected_dict) + + def test_pool_update_unset_new_session_timeout(self): + self.ref_update_pool.session_persistence = data_models.UnsetType() + info = {'id': self.ref_update_pool.pool_id, + 'loadbalancer_id': self.ref_update_pool.loadbalancer_id, + 'protocol': self.ref_pool.protocol, + 'admin_state_up': self.ref_update_pool.admin_state_up} expected_dict = {'type': ovn_const.REQ_TYPE_POOL_UPDATE, 'info': info} self.driver.pool_update(self.ref_pool, self.ref_update_pool) diff --git a/releasenotes/notes/session-persistence-b409428a8907f542.yaml b/releasenotes/notes/session-persistence-b409428a8907f542.yaml new file mode 100644 index 00000000..29ec5ff7 --- /dev/null +++ b/releasenotes/notes/session-persistence-b409428a8907f542.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Now the OVN Octavia provider uses the affinity_timeout option of OVN + Load Balancers to support pools sessions persistence. It only supports + the SOURCE_IP option type. If not timeout is set, by default 360 seconds + is set if the session persistence is enabled.