# Copyright 2017 VMware, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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 from oslo_utils import excutils from vmware_nsx._i18n import _ from vmware_nsx.common import exceptions as nsx_exc from vmware_nsx.db import db as nsx_db 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__) class EdgePoolManagerFromDict(base_mgr.Nsxv3LoadbalancerBaseManager): @log_helpers.log_method_call def _get_pool_kwargs(self, name=None, tags=None, algorithm=None, description=None): kwargs = {} if name: kwargs['display_name'] = name if tags: kwargs['tags'] = tags if algorithm: kwargs['algorithm'] = algorithm if description: kwargs['description'] = description kwargs['snat_translation'] = {'type': "LbSnatAutoMap"} return kwargs @log_helpers.log_method_call def _build_persistence_profile_tags(self, pool_tags, listener): tags = pool_tags[:] # With octavia loadbalancer name might not be among data passed # down to the driver lb_data = listener.get('loadbalancer') if lb_data: tags.append({ 'scope': 'os-lbaas-lb-name', 'tag': lb_data['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 @log_helpers.log_method_call 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) @log_helpers.log_method_call 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("Created 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 return None, None @log_helpers.log_method_call 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) @log_helpers.log_method_call 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) @log_helpers.log_method_call 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'], context.project_name) @log_helpers.log_method_call def create(self, context, pool, completor): lb_id = pool['loadbalancer_id'] pool_client = self.core_plugin.nsxlib.load_balancer.pool 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']) if pool.get('listeners') and len(pool['listeners']) > 1: completor(success=False) msg = (_('Failed to create pool: Multiple listeners are not ' 'supported.')) raise n_exc.BadRequest(resource='lbaas-pool', msg=msg) # 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) lb_pool = pool_client.create(**kwargs) nsx_db.add_nsx_lbaas_pool_binding( context.session, lb_id, pool['id'], lb_pool['id']) except nsxlib_exc.ManagerError: completor(success=False) msg = (_('Failed to create pool on NSX backend: %(pool)s') % {'pool': pool['id']}) raise n_exc.BadRequest(resource='lbaas-pool', msg=msg) # The pool object can be created with either --listener or # --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. # 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'] 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: completor(success=False) msg = (_("Couldn't find binding on the listener: %s") % listener['id']) raise nsx_exc.NsxPluginException(err_msg=msg) completor(success=True) @log_helpers.log_method_call def update(self, context, old_pool, new_pool, completor): pool_client = self.core_plugin.nsxlib.load_balancer.pool pool_name = None tags = None lb_algorithm = None description = None if new_pool['name'] != old_pool['name']: pool_name = utils.get_name_and_uuid(new_pool['name'] or 'pool', new_pool['id']) tags = self._get_pool_tags(context, new_pool) if new_pool['lb_algorithm'] != old_pool['lb_algorithm']: lb_algorithm = lb_const.LB_POOL_ALGORITHM_MAP.get( new_pool['lb_algorithm']) if new_pool.get('description') != old_pool.get('description'): description = new_pool['description'] binding = nsx_db.get_nsx_lbaas_pool_binding( context.session, old_pool['loadbalancer_id'], old_pool['id']) if not binding: completor(success=False) msg = (_('Cannot find pool %(pool)s binding on NSX db ' 'mapping') % {'pool': old_pool['id']}) raise n_exc.BadRequest(resource='lbaas-pool', msg=msg) if new_pool.get('listeners') and len(new_pool['listeners']) > 1: completor(success=False) msg = (_('Failed to update pool %s: Multiple listeners are not ' 'supported.') % new_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(): completor(success=False) LOG.error('Failed to update pool %(pool)s with ' 'error %(error)s', {'pool': old_pool['id'], 'error': e}) @log_helpers.log_method_call def delete(self, context, pool, completor): lb_id = pool['loadbalancer_id'] pool_client = self.core_plugin.nsxlib.load_balancer.pool 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: 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: pass except nsxlib_exc.ManagerError: completor(success=False) msg = (_('Failed to delete lb pool from nsx: %(pool)s') % {'pool': lb_pool_id}) raise n_exc.BadRequest(resource='lbaas-pool', msg=msg) nsx_db.delete_nsx_lbaas_pool_binding(context.session, lb_id, pool['id']) completor(success=True) @log_helpers.log_method_call def delete_cascade(self, context, pool, completor): self.delete(context, pool, completor)