From 2e010fd9b22e380c8885f607236d8ed77aa7e5a4 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Mon, 26 Nov 2018 04:22:48 -0800 Subject: [PATCH] LBaaS: Session persistence for NSX-v3 Implement pool session persistence in the NSX-v3 driver. This patch does also a certain amount of refactoring to ensure still a single call is made for updating the NSX virtual server. Persitence profile deletion happens via a deferred function as the virtual server must be updated first to not use anymore the persistence profile. Updating a pool from cookie-based to source-ip based session persistence is currently not allowed and will result in a driver error. Change-Id: Id9fc4e664c5a212a411b4d4eda688bad5c74e869 --- vmware_nsx/services/lbaas/lb_const.py | 2 + .../nsx_v/implementation/listener_mgr.py | 2 +- .../lbaas/nsx_v3/implementation/pool_mgr.py | 225 ++++++++++-- .../unit/services/lbaas/test_nsxv3_driver.py | 337 +++++++++++++++++- 4 files changed, 536 insertions(+), 30 deletions(-) diff --git a/vmware_nsx/services/lbaas/lb_const.py b/vmware_nsx/services/lbaas/lb_const.py index 1f6dfaf48b..17cddd1734 100644 --- a/vmware_nsx/services/lbaas/lb_const.py +++ b/vmware_nsx/services/lbaas/lb_const.py @@ -57,6 +57,8 @@ SESSION_PERSISTENCE_COOKIE_MAP = { LB_SESSION_PERSISTENCE_APP_COOKIE: 'app', LB_SESSION_PERSISTENCE_HTTP_COOKIE: 'insert'} +SESSION_PERSISTENCE_DEFAULT_COOKIE_NAME = 'default_cookie_name' + L7_POLICY_ACTION_REJECT = 'REJECT' L7_POLICY_ACTION_REDIRECT_TO_POOL = 'REDIRECT_TO_POOL' L7_POLICY_ACTION_REDIRECT_TO_URL = 'REDIRECT_TO_URL' diff --git a/vmware_nsx/services/lbaas/nsx_v/implementation/listener_mgr.py b/vmware_nsx/services/lbaas/nsx_v/implementation/listener_mgr.py index e3f2d8a5da..a3bd4b94bc 100644 --- a/vmware_nsx/services/lbaas/nsx_v/implementation/listener_mgr.py +++ b/vmware_nsx/services/lbaas/nsx_v/implementation/listener_mgr.py @@ -64,7 +64,7 @@ def listener_to_edge_app_profile(listener, edge_cert_id): lb_const.SESSION_PERSISTENCE_COOKIE_MAP): cookie_name = pool_sess_persist.get('cookie_name', None) if cookie_name is None: - cookie_name = 'default_cookie_name' + cookie_name = lb_const.SESSION_PERSISTENCE_DEFAULT_COOKIE_NAME persistence.update({ 'cookieName': cookie_name, 'cookieMode': lb_const.SESSION_PERSISTENCE_COOKIE_MAP[ diff --git a/vmware_nsx/services/lbaas/nsx_v3/implementation/pool_mgr.py b/vmware_nsx/services/lbaas/nsx_v3/implementation/pool_mgr.py index 2d7b542f61..02a11a7a7c 100644 --- a/vmware_nsx/services/lbaas/nsx_v3/implementation/pool_mgr.py +++ b/vmware_nsx/services/lbaas/nsx_v3/implementation/pool_mgr.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import functools + from neutron_lib import exceptions as n_exc from oslo_log import helpers as log_helpers from oslo_log import log as logging @@ -25,6 +27,7 @@ from vmware_nsx.services.lbaas import base_mgr from vmware_nsx.services.lbaas import lb_const from vmware_nsx.services.lbaas.nsx_v3.implementation import lb_utils from vmware_nsxlib.v3 import exceptions as nsxlib_exc +from vmware_nsxlib.v3 import load_balancer as nsxlib_lb from vmware_nsxlib.v3 import utils LOG = logging.getLogger(__name__) @@ -49,6 +52,163 @@ class EdgePoolManagerFromDict(base_mgr.Nsxv3LoadbalancerBaseManager): kwargs['snat_translation'] = {'type': "LbSnatAutoMap"} return kwargs + def _build_persistence_profile_tags(self, pool_tags, listener): + tags = pool_tags[:] + tags.append({ + 'scope': 'os-lbaas-lb-name', + 'tag': listener['loadbalancer']['name'][:utils.MAX_TAG_LEN]}) + tags.append({ + 'scope': 'os-lbaas-lb-id', + 'tag': listener['loadbalancer_id']}) + tags.append({ + 'scope': 'os-lbaas-listener-id', + 'tag': listener['id']}) + return tags + + def _validate_session_persistence(self, pool, listener, completor, + old_pool=None): + sp = pool.get('session_persistence') + if not listener or not sp: + # safety first! + return + # L4 listeners only allow source IP persistence + if (listener['protocol'] == lb_const.LB_PROTOCOL_TCP and + sp['type'] != lb_const.LB_SESSION_PERSISTENCE_SOURCE_IP): + completor(success=False) + msg = (_("Invalid session persistence type %(sp_type)s for " + "pool on listener %(lst_id)s with %(proto)s protocol") % + {'sp_type': sp['type'], + 'lst_id': listener['id'], + 'proto': listener['protocol']}) + raise n_exc.BadRequest(resource='lbaas-pool', msg=msg) + # Cannot switch (yet) on update from source IP to cookie based, and + # vice versa + cookie_pers_types = (lb_const.LB_SESSION_PERSISTENCE_HTTP_COOKIE, + lb_const.LB_SESSION_PERSISTENCE_APP_COOKIE) + if old_pool: + oldsp = old_pool.get('session_persistence') + if not oldsp: + return + if ((sp['type'] == lb_const.LB_SESSION_PERSISTENCE_SOURCE_IP and + oldsp['type'] in cookie_pers_types) or + (sp['type'] in cookie_pers_types and + oldsp['type'] == lb_const.LB_SESSION_PERSISTENCE_SOURCE_IP)): + completor(success=False) + msg = (_("Cannot update session persistence type to " + "%(sp_type)s for pool on listener %(lst_id)s " + "from %(old_sp_type)s") % + {'sp_type': sp['type'], + 'lst_id': listener['id'], + 'old_sp_type': oldsp['type']}) + raise n_exc.BadRequest(resource='lbaas-pool', msg=msg) + + def _setup_session_persistence(self, pool, pool_tags, + listener, vs_data): + sp = pool.get('session_persistence') + pers_type = None + cookie_name = None + cookie_mode = None + if not sp: + LOG.debug("No session persistence info for pool %s", pool['id']) + elif sp['type'] == lb_const.LB_SESSION_PERSISTENCE_HTTP_COOKIE: + pers_type = nsxlib_lb.PersistenceProfileTypes.COOKIE + cookie_name = sp.get('cookie_name') + if not cookie_name: + cookie_name = lb_const.SESSION_PERSISTENCE_DEFAULT_COOKIE_NAME + cookie_mode = "INSERT" + elif sp['type'] == lb_const.LB_SESSION_PERSISTENCE_APP_COOKIE: + pers_type = nsxlib_lb.PersistenceProfileTypes.COOKIE + # In this case cookie name is mandatory + cookie_name = sp['cookie_name'] + cookie_mode = "REWRITE" + else: + pers_type = nsxlib_lb.PersistenceProfileTypes.SOURCE_IP + + if pers_type: + # There is a profile to create or update + pp_kwargs = { + 'resource_type': pers_type, + 'display_name': "persistence_%s" % utils.get_name_and_uuid( + pool['name'] or 'pool', pool['id'], maxlen=235), + 'tags': self._build_persistence_profile_tags( + pool_tags, listener) + } + if cookie_name: + pp_kwargs['cookie_name'] = cookie_name + pp_kwargs['cookie_mode'] = cookie_mode + + pp_client = self.core_plugin.nsxlib.load_balancer.persistence_profile + persistence_profile_id = vs_data.get('persistence_profile_id') + if persistence_profile_id: + # NOTE: removal of the persistence profile must be executed + # after the virtual server has been updated + if pers_type: + # Update existing profile + LOG.debug("Updating persistence profile %(profile_id)s for " + "listener %(listener_id)s with pool %(pool_id)s", + {'profile_id': persistence_profile_id, + 'listener_id': listener['id'], + 'pool_id': pool['id']}) + pp_client.update(persistence_profile_id, **pp_kwargs) + return persistence_profile_id, None + else: + # Prepare removal of persistence profile + return (None, functools.partial(self._remove_persistence, + vs_data)) + elif pers_type: + # Create persistence profile + pp_data = pp_client.create(**pp_kwargs) + LOG.debug("Creaed persistence profile %(profile_id)s for " + "listener %(listener_id)s with pool %(pool_id)s", + {'profile_id': pp_data['id'], + 'listener_id': listener['id'], + 'pool_id': pool['id']}) + return pp_data['id'], None + + def _remove_persistence(self, vs_data): + pp_client = self.core_plugin.nsxlib.load_balancer.persistence_profile + persistence_profile_id = vs_data.get('persistence_profile_id') + if persistence_profile_id: + pp_client.delete(persistence_profile_id) + + def _process_vs_update(self, context, pool, listener, + nsx_pool_id, nsx_vs_id, completor): + vs_client = self.core_plugin.nsxlib.load_balancer.virtual_server + try: + # Process pool persistence profile and + # create/update/delete profile for virtual server + vs_data = vs_client.get(nsx_vs_id) + if nsx_pool_id: + (persistence_profile_id, + post_process_func) = self._setup_session_persistence( + pool, self._get_pool_tags(context, pool), + listener, vs_data) + else: + post_process_func = functools.partial( + self._remove_persistence, vs_data) + persistence_profile_id = None + except nsxlib_exc.ManagerError: + with excutils.save_and_reraise_exception(): + completor(success=False) + LOG.error("Failed to configure session persistence " + "profile for pool %(pool_id)s", + {'pool_id': pool['id']}) + try: + # Update persistence profile and pool on virtual server + vs_client.update(nsx_vs_id, pool_id=nsx_pool_id, + persistence_profile_id=persistence_profile_id) + LOG.debug("Updated NSX virtual server %(vs_id)s with " + "pool %(pool_id)s and persistence profile %(prof)s", + {'vs_id': nsx_vs_id, 'pool_id': nsx_pool_id, + 'prof': persistence_profile_id}) + if post_process_func: + post_process_func() + except nsxlib_exc.ManagerError: + with excutils.save_and_reraise_exception(): + completor(success=False) + LOG.error('Failed to attach pool %s to virtual ' + 'server %s', nsx_pool_id, nsx_vs_id) + def _get_pool_tags(self, context, pool): return lb_utils.get_tags(self.core_plugin, pool['id'], lb_const.LB_POOL_TYPE, pool['tenant_id'], @@ -57,11 +217,19 @@ class EdgePoolManagerFromDict(base_mgr.Nsxv3LoadbalancerBaseManager): def create(self, context, pool, completor): lb_id = pool['loadbalancer_id'] pool_client = self.core_plugin.nsxlib.load_balancer.pool - vs_client = self.core_plugin.nsxlib.load_balancer.virtual_server pool_name = utils.get_name_and_uuid(pool['name'] or 'pool', pool['id']) tags = self._get_pool_tags(context, pool) description = pool.get('description') lb_algorithm = lb_const.LB_POOL_ALGORITHM_MAP.get(pool['lb_algorithm']) + # NOTE(salv-orlando): Guard against accidental compat breakages + try: + listener = pool['listener'] or pool['listeners'][0] + except IndexError: + # If listeners is an empty list we hit this exception + listener = None + # Perform additional validation for session persistence before + # creating resources in the backend + self._validate_session_persistence(pool, listener, completor) try: kwargs = self._get_pool_kwargs(pool_name, tags, lb_algorithm, description) @@ -78,32 +246,22 @@ class EdgePoolManagerFromDict(base_mgr.Nsxv3LoadbalancerBaseManager): # --loadbalancer option. If listener is present, the virtual server # will be updated with the pool. Otherwise, just return. The binding # will be added later when the pool is associated with layer7 rule. - # NOTE(salv-orlando): Guard against accidental compat breakages - try: - listener = pool['listener'] or pool['listeners'][0] - except IndexError: - # If listeners is an empty list we hit this exception - listener = None + # FIXME(salv-orlando): This two-step process can leave a zombie pool on + # NSX if the VS update operation fails if listener: listener_id = listener['id'] binding = nsx_db.get_nsx_lbaas_listener_binding( context.session, lb_id, listener_id) if binding: vs_id = binding['lb_vs_id'] - try: - vs_client.update(vs_id, pool_id=lb_pool['id']) - except nsxlib_exc.ManagerError: - with excutils.save_and_reraise_exception(): - completor(success=False) - LOG.error('Failed to attach pool %s to virtual ' - 'server %s', lb_pool['id'], vs_id) + self._process_vs_update(context, pool, listener, + lb_pool['id'], vs_id, completor) nsx_db.update_nsx_lbaas_pool_binding( context.session, lb_id, pool['id'], vs_id) else: msg = (_("Couldn't find binding on the listener: %s") % - listener_id) + listener['id']) raise nsx_exc.NsxPluginException(err_msg=msg) - completor(success=True) def update(self, context, old_pool, new_pool, completor): @@ -127,11 +285,27 @@ class EdgePoolManagerFromDict(base_mgr.Nsxv3LoadbalancerBaseManager): msg = (_('Cannot find pool %(pool)s binding on NSX db ' 'mapping') % {'pool': old_pool['id']}) raise n_exc.BadRequest(resource='lbaas-pool', msg=msg) + # NOTE(salv-orlando): Guard against accidental compat breakages + try: + listener = new_pool['listener'] or new_pool['listeners'][0] + except IndexError: + # If listeners is an empty list we hit this exception + listener = None + # Perform additional validation for session persistence before + # operating on resources in the backend + self._validate_session_persistence(new_pool, listener, completor, + old_pool=old_pool) + try: lb_pool_id = binding['lb_pool_id'] kwargs = self._get_pool_kwargs(pool_name, tags, lb_algorithm, description) pool_client.update(lb_pool_id, **kwargs) + if (listener and new_pool['session_persistence'] != + old_pool['session_persistence']): + self._process_vs_update(context, new_pool, listener, + lb_pool_id, binding['lb_vs_id'], + completor) completor(success=True) except Exception as e: with excutils.save_and_reraise_exception(): @@ -143,24 +317,23 @@ class EdgePoolManagerFromDict(base_mgr.Nsxv3LoadbalancerBaseManager): def delete(self, context, pool, completor): lb_id = pool['loadbalancer_id'] pool_client = self.core_plugin.nsxlib.load_balancer.pool - vs_client = self.core_plugin.nsxlib.load_balancer.virtual_server binding = nsx_db.get_nsx_lbaas_pool_binding( context.session, lb_id, pool['id']) if binding: vs_id = binding.get('lb_vs_id') lb_pool_id = binding.get('lb_pool_id') + if vs_id: + # NOTE(salv-orlando): Guard against accidental compat breakages try: - vs_client.update(vs_id, pool_id='') - except nsxlib_exc.ResourceNotFound: - pass - except nsxlib_exc.ManagerError: - completor(success=False) - msg = _('Failed to remove lb pool %(pool)s from virtual ' - 'server %(vs)s') % {'pool': lb_pool_id, - 'vs': vs_id} - raise n_exc.BadRequest(resource='lbaas-pool', msg=msg) + listener = pool['listener'] or pool['listeners'][0] + except IndexError: + # If listeners is an empty list we hit this exception + listener = None + if listener: + self._process_vs_update(context, pool, listener, + None, vs_id, completor) try: pool_client.delete(lb_pool_id) except nsxlib_exc.ResourceNotFound: diff --git a/vmware_nsx/tests/unit/services/lbaas/test_nsxv3_driver.py b/vmware_nsx/tests/unit/services/lbaas/test_nsxv3_driver.py index 787d1eb931..c40c0b930a 100644 --- a/vmware_nsx/tests/unit/services/lbaas/test_nsxv3_driver.py +++ b/vmware_nsx/tests/unit/services/lbaas/test_nsxv3_driver.py @@ -100,6 +100,7 @@ L7RULE_ID = 'l7rule-111' L7POLICY_BINDING = {'l7policy_id': L7POLICY_ID, 'lb_vs_id': LB_VS_ID, 'lb_rule_id': LB_RULE_ID} +LB_PP_ID = "ppp-ppp" FAKE_CERT = {'id': 'cert-xyz'} @@ -164,6 +165,15 @@ class BaseTestEdgeLbaasV2(base.BaseTestCase): listener=self.listener, listeners=[self.listener], loadbalancer=self.lb) + self.sess_persistence = lb_models.SessionPersistence( + POOL_ID, 'HTTP_COOKIE', 'meh_cookie') + self.pool_persistency = lb_models.Pool(POOL_ID, LB_TENANT_ID, + 'pool1', '', None, 'HTTP', + 'ROUND_ROBIN', loadbalancer_id=LB_ID, + listener=self.listener, + listeners=[self.listener], + loadbalancer=self.lb, + session_persistence=self.sess_persistence) self.member = lb_models.Member(MEMBER_ID, LB_TENANT_ID, POOL_ID, MEMBER_ADDRESS, 80, 1, pool=self.pool, name='member1') @@ -218,6 +228,8 @@ class BaseTestEdgeLbaasV2(base.BaseTestCase): 'monitor').start() self.rule_client = mock.patch.object(load_balancer, 'rule').start() + self.pp_client = mock.patch.object(load_balancer, + 'persistence_profile').start() self.tm_client = mock.patch.object(nsxlib, 'trust_management').start() @@ -477,7 +489,10 @@ class TestEdgeLbaasV2Pool(BaseTestEdgeLbaasV2): ) as mock_add_pool_binding, \ mock.patch.object(nsx_db, 'get_nsx_lbaas_listener_binding' ) as mock_get_listener_binding, \ - mock.patch.object(self.vs_client, 'update', return_value=None), \ + mock.patch.object(self.pp_client, 'create' + ) as mock_create_pp, \ + mock.patch.object(self.vs_client, 'update', return_value=None + ) as mock_vs_update, \ mock.patch.object(nsx_db, 'update_nsx_lbaas_pool_binding' ) as mock_update_pool_binding: mock_create_pool.return_value = {'id': LB_POOL_ID} @@ -487,6 +502,9 @@ class TestEdgeLbaasV2Pool(BaseTestEdgeLbaasV2): mock_add_pool_binding.assert_called_with( self.context.session, LB_ID, POOL_ID, LB_POOL_ID) + mock_create_pp.assert_not_called() + mock_vs_update.assert_called_once_with( + LB_VS_ID, pool_id=LB_POOL_ID, persistence_profile_id=None) mock_update_pool_binding.assert_called_with( self.context.session, LB_ID, POOL_ID, LB_VS_ID) mock_successful_completion = ( @@ -495,6 +513,93 @@ class TestEdgeLbaasV2Pool(BaseTestEdgeLbaasV2): self.pool, delete=False) + def _test_create_with_persistency(self, vs_data, verify_func): + with mock.patch.object(self.pool_client, 'create' + ) as mock_create_pool, \ + mock.patch.object(nsx_db, 'add_nsx_lbaas_pool_binding' + ) as mock_add_pool_binding, \ + mock.patch.object(nsx_db, 'get_nsx_lbaas_listener_binding' + ) as mock_get_listener_binding, \ + mock.patch.object(self.pp_client, 'create' + ) as mock_create_pp, \ + mock.patch.object(self.pp_client, 'update', return_value=None, + ) as mock_update_pp, \ + mock.patch.object(self.vs_client, 'get' + ) as mock_vs_get, \ + mock.patch.object(self.vs_client, 'update', return_value=None + ) as mock_vs_update, \ + mock.patch.object(nsx_db, 'update_nsx_lbaas_pool_binding' + ) as mock_update_pool_binding: + + mock_vs_get.return_value = vs_data + mock_create_pool.return_value = {'id': LB_POOL_ID} + mock_create_pp.return_value = {'id': LB_PP_ID} + mock_get_listener_binding.return_value = LISTENER_BINDING + + self.edge_driver.pool.create(self.context, self.pool_persistency) + + mock_add_pool_binding.assert_called_with( + self.context.session, LB_ID, POOL_ID, LB_POOL_ID) + verify_func(mock_create_pp, mock_update_pp, + mock_update_pool_binding, mock_vs_update) + mock_successful_completion = ( + self.lbv2_driver.pool.successful_completion) + mock_successful_completion.assert_called_with( + self.context, self.pool_persistency, delete=False) + + def test_create_with_persistency(self): + + def verify_func(mock_create_pp, mock_update_pp, + mock_update_pool_binding, mock_vs_update): + mock_create_pp.assert_called_once_with( + resource_type='LbCookiePersistenceProfile', + cookie_mode='INSERT', + cookie_name='meh_cookie', + display_name=mock.ANY, + tags=mock.ANY) + mock_update_pp.assert_not_called() + mock_update_pool_binding.assert_called_with( + self.context.session, LB_ID, POOL_ID, LB_VS_ID) + mock_vs_update.assert_called_once_with( + LB_VS_ID, pool_id=LB_POOL_ID, persistence_profile_id=LB_PP_ID) + + vs_data = {'id': LB_VS_ID} + self._test_create_with_persistency(vs_data, verify_func) + + def test_create_with_persistency_existing_profile(self): + def verify_func(mock_create_pp, mock_update_pp, + mock_update_pool_binding, mock_vs_update): + mock_create_pp.assert_not_called() + mock_update_pp.assert_called_once_with( + LB_PP_ID, + resource_type='LbCookiePersistenceProfile', + cookie_mode='INSERT', + cookie_name='meh_cookie', + display_name=mock.ANY, + tags=mock.ANY) + mock_update_pool_binding.assert_called_with( + self.context.session, LB_ID, POOL_ID, LB_VS_ID) + mock_vs_update.assert_called_once_with( + LB_VS_ID, pool_id=LB_POOL_ID, persistence_profile_id=LB_PP_ID) + + vs_data = {'id': LB_VS_ID, + 'persistence_profile_id': LB_PP_ID} + self._test_create_with_persistency(vs_data, verify_func) + + def test_create_with_persistency_no_listener(self): + def verify_func(mock_create_pp, mock_update_pp, + mock_update_pool_binding, mock_vs_update): + mock_create_pp.assert_not_called() + mock_update_pp.assert_not_called() + mock_update_pool_binding.assert_not_called() + mock_vs_update.assert_not_called() + + vs_data = {'id': LB_VS_ID, + 'persistence_profile_id': LB_PP_ID} + self.pool_persistency.listener = None + self.pool_persistency.listeners = [] + self._test_create_with_persistency(vs_data, verify_func) + def test_update(self): new_pool = lb_models.Pool(POOL_ID, LB_TENANT_ID, 'pool-name', '', None, 'HTTP', 'LEAST_CONNECTIONS', @@ -511,6 +616,67 @@ class TestEdgeLbaasV2Pool(BaseTestEdgeLbaasV2): new_pool, delete=False) + def _test_update_with_persistency(self, vs_data, old_pool, new_pool, + verify_func): + with mock.patch.object(nsx_db, 'get_nsx_lbaas_pool_binding' + ) as mock_get_pool_binding, \ + mock.patch.object(self.pp_client, 'create' + ) as mock_create_pp, \ + mock.patch.object(self.pp_client, 'update', return_value=None, + ) as mock_update_pp, \ + mock.patch.object(self.pp_client, 'delete', return_value=None, + ) as mock_delete_pp, \ + mock.patch.object(self.vs_client, 'get' + ) as mock_vs_get, \ + mock.patch.object(self.vs_client, 'update', return_value=None + ) as mock_vs_update: + + mock_vs_get.return_value = vs_data + mock_get_pool_binding.return_value = POOL_BINDING + mock_create_pp.return_value = {'id': LB_PP_ID} + + self.edge_driver.pool.update(self.context, old_pool, new_pool) + + verify_func(mock_create_pp, mock_update_pp, + mock_delete_pp, mock_vs_update) + mock_successful_completion = ( + self.lbv2_driver.pool.successful_completion) + mock_successful_completion.assert_called_with( + self.context, new_pool, delete=False) + + def test_update_with_persistency(self): + + def verify_func(mock_create_pp, mock_update_pp, + mock_delete_pp, mock_vs_update): + mock_create_pp.assert_called_once_with( + resource_type='LbCookiePersistenceProfile', + cookie_mode='INSERT', + cookie_name='meh_cookie', + display_name=mock.ANY, + tags=mock.ANY) + mock_update_pp.assert_not_called() + mock_delete_pp.assert_not_called() + mock_vs_update.assert_called_once_with( + LB_VS_ID, pool_id=LB_POOL_ID, persistence_profile_id=LB_PP_ID) + + vs_data = {'id': LB_VS_ID} + self._test_update_with_persistency(vs_data, self.pool, + self.pool_persistency, verify_func) + + def test_update_remove_persistency(self): + def verify_func(mock_create_pp, mock_update_pp, + mock_delete_pp, mock_vs_update): + mock_create_pp.assert_not_called() + mock_update_pp.assert_not_called() + mock_delete_pp.assert_called_with(LB_PP_ID) + mock_vs_update.assert_called_once_with( + LB_VS_ID, pool_id=LB_POOL_ID, persistence_profile_id=None) + + vs_data = {'id': LB_VS_ID, + 'persistence_profile_id': LB_PP_ID} + self._test_update_with_persistency(vs_data, self.pool_persistency, + self.pool, verify_func) + def test_delete(self): with mock.patch.object(nsx_db, 'get_nsx_lbaas_pool_binding' ) as mock_get_pool_binding, \ @@ -527,8 +693,8 @@ class TestEdgeLbaasV2Pool(BaseTestEdgeLbaasV2): self.edge_driver.pool.delete(self.context, self.pool) - mock_update_virtual_server.assert_called_with(LB_VS_ID, - pool_id='') + mock_update_virtual_server.assert_called_with( + LB_VS_ID, persistence_profile_id=None, pool_id=None) mock_delete_pool.assert_called_with(LB_POOL_ID) mock_delete_pool_binding.assert_called_with( self.context.session, LB_ID, POOL_ID) @@ -539,6 +705,171 @@ class TestEdgeLbaasV2Pool(BaseTestEdgeLbaasV2): self.pool, delete=True) + def test_delete_with_persistency(self): + with mock.patch.object(nsx_db, 'get_nsx_lbaas_pool_binding' + ) as mock_get_pool_binding, \ + mock.patch.object(self.vs_client, 'get' + ) as mock_vs_get, \ + mock.patch.object(self.vs_client, 'update', return_value=None + ) as mock_update_virtual_server, \ + mock.patch.object(self.pool_client, 'delete' + ) as mock_delete_pool, \ + mock.patch.object(self.pp_client, 'delete', return_value=None, + ) as mock_delete_pp, \ + mock.patch.object(nsx_db, 'delete_nsx_lbaas_pool_binding' + ) as mock_delete_pool_binding, \ + mock.patch.object(nsx_db, 'get_nsx_lbaas_loadbalancer_binding' + ) as mock_get_lb_binding: + mock_get_pool_binding.return_value = POOL_BINDING + mock_get_lb_binding.return_value = None + mock_vs_get.return_value = {'id': LB_VS_ID, + 'persistence_profile_id': LB_PP_ID} + + self.edge_driver.pool.delete(self.context, self.pool_persistency) + + mock_delete_pp.assert_called_once_with(LB_PP_ID) + mock_update_virtual_server.assert_called_once_with( + LB_VS_ID, persistence_profile_id=None, pool_id=None) + mock_delete_pool.assert_called_with(LB_POOL_ID) + mock_delete_pool_binding.assert_called_with( + self.context.session, LB_ID, POOL_ID) + + mock_successful_completion = ( + self.lbv2_driver.pool.successful_completion) + mock_successful_completion.assert_called_with( + self.context, self.pool_persistency, delete=True) + + def _verify_create(self, res_type, cookie_name, cookie_mode, + mock_create_pp, mock_update_pp): + if cookie_name: + mock_create_pp.assert_called_once_with( + resource_type=res_type, + cookie_name=cookie_name, + cookie_mode=cookie_mode, + display_name=mock.ANY, + tags=mock.ANY) + else: + mock_create_pp.assert_called_once_with( + resource_type=res_type, + display_name=mock.ANY, + tags=mock.ANY) + # Compare tags - kw args are the last item of a mock call tuple + self.assertItemsEqual(mock_create_pp.mock_calls[0][-1]['tags'], + [{'scope': 'os-lbaas-lb-id', 'tag': 'xxx-xxx'}, + {'scope': 'os-lbaas-lb-name', 'tag': 'lb1'}, + {'scope': 'os-lbaas-listener-id', 'tag': 'listener-x'}]) + mock_update_pp.assert_not_called() + + def _verify_update(self, res_type, cookie_name, cookie_mode, + mock_create_pp, mock_update_pp): + if cookie_name: + mock_update_pp.assert_called_once_with( + LB_PP_ID, + resource_type=res_type, + cookie_name=cookie_name, + cookie_mode=cookie_mode, + display_name=mock.ANY, + tags=mock.ANY) + else: + mock_update_pp.assert_called_once_with( + LB_PP_ID, + resource_type=res_type, + display_name=mock.ANY, + tags=mock.ANY) + # Compare tags - kw args are the last item of a mock call tuple + self.assertItemsEqual(mock_update_pp.mock_calls[0][-1]['tags'], + [{'scope': 'os-lbaas-lb-id', 'tag': 'xxx-xxx'}, + {'scope': 'os-lbaas-lb-name', 'tag': 'lb1'}, + {'scope': 'os-lbaas-listener-id', 'tag': 'listener-x'}]) + mock_create_pp.assert_not_called() + + def _verify_delete(self, res_type, cookie_name, cookie_mode, + mock_create_pp, mock_update_pp): + mock_create_pp.assert_not_called() + mock_update_pp.assert_not_called() + + def _test_setup_session_persistence(self, session_persistence, + res_type, vs_data, verify_func, + cookie_name=None, cookie_mode=None): + with mock.patch.object(self.pp_client, 'create' + ) as mock_create_pp, \ + mock.patch.object(self.pp_client, 'update', return_value=None, + ) as mock_update_pp: + + mock_create_pp.return_value = {'id': LB_PP_ID} + self.pool.session_persistence = session_persistence + pool_dict = self.edge_driver.pool.translator(self.pool) + list_dict = self.edge_driver.listener.translator(self.listener) + pool_impl = self.edge_driver.pool.implementor # make pep8 happy + pp_id, post_func = pool_impl._setup_session_persistence( + pool_dict, [], list_dict, vs_data) + + if session_persistence: + self.assertEqual(LB_PP_ID, pp_id) + else: + self.assertIsNone(pp_id) + self.assertEqual(({'id': LB_VS_ID, + 'persistence_profile_id': LB_PP_ID},), + post_func.args) + verify_func(res_type, cookie_name, cookie_mode, + mock_create_pp, mock_update_pp) + + def test_setup_session_persistence_sourceip_new_profile(self): + sess_persistence = lb_models.SessionPersistence(POOL_ID, 'SOURCE_IP') + res_type = 'LbSourceIpPersistenceProfile' + self._test_setup_session_persistence( + sess_persistence, res_type, {'id': LB_VS_ID}, self._verify_create) + + def test_setup_session_persistence_httpcookie_new_profile(self): + sess_persistence = lb_models.SessionPersistence( + POOL_ID, 'HTTP_COOKIE') + res_type = 'LbCookiePersistenceProfile' + self._test_setup_session_persistence( + sess_persistence, res_type, {'id': LB_VS_ID}, + self._verify_create, 'default_cookie_name', 'INSERT') + + def test_setup_session_persistence_appcookie_new_profile(self): + sess_persistence = lb_models.SessionPersistence( + POOL_ID, 'APP_COOKIE', 'whatever') + res_type = 'LbCookiePersistenceProfile' + self._test_setup_session_persistence( + sess_persistence, res_type, {'id': LB_VS_ID}, + self._verify_create, 'whatever', 'REWRITE') + + def test_setup_session_persistence_none_from_existing(self): + sess_persistence = None + self._test_setup_session_persistence( + sess_persistence, None, + {'id': LB_VS_ID, 'persistence_profile_id': LB_PP_ID}, + self._verify_delete) + + def test_setup_session_persistence_sourceip_from_existing(self): + sess_persistence = lb_models.SessionPersistence(POOL_ID, 'SOURCE_IP') + res_type = 'LbSourceIpPersistenceProfile' + self._test_setup_session_persistence( + sess_persistence, res_type, + {'id': LB_VS_ID, 'persistence_profile_id': LB_PP_ID}, + self._verify_update) + + def test_setup_session_persistence_httpcookie_from_existing(self): + sess_persistence = lb_models.SessionPersistence(POOL_ID, 'HTTP_COOKIE') + res_type = 'LbCookiePersistenceProfile' + self._test_setup_session_persistence( + sess_persistence, res_type, + {'id': LB_VS_ID, 'persistence_profile_id': LB_PP_ID}, + self._verify_update, + 'default_cookie_name', 'INSERT') + + def test_setup_session_persistence_appcookie_from_existing(self): + sess_persistence = lb_models.SessionPersistence( + POOL_ID, 'APP_COOKIE', 'whatever') + res_type = 'LbCookiePersistenceProfile' + self._test_setup_session_persistence( + sess_persistence, res_type, + {'id': LB_VS_ID, 'persistence_profile_id': LB_PP_ID}, + self._verify_update, + 'whatever', 'REWRITE') + class TestEdgeLbaasV2Member(BaseTestEdgeLbaasV2): def setUp(self):