# Copyright 2014 Rackspace # Copyright 2016 Blue Box, an IBM Company # # 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. from octavia_lib.api.drivers import data_models as driver_dm from oslo_config import cfg from oslo_db import exception as odb_exceptions from oslo_log import log as logging from oslo_utils import excutils from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan from octavia.api.drivers import driver_factory from octavia.api.drivers import utils as driver_utils from octavia.api.v2.controllers import base from octavia.api.v2.types import health_monitor as hm_types from octavia.common import constants as consts from octavia.common import data_models from octavia.common import exceptions from octavia.db import api as db_api from octavia.db import prepare as db_prepare from octavia.i18n import _ CONF = cfg.CONF LOG = logging.getLogger(__name__) class HealthMonitorController(base.BaseController): RBAC_TYPE = consts.RBAC_HEALTHMONITOR def __init__(self): super(HealthMonitorController, self).__init__() @wsme_pecan.wsexpose(hm_types.HealthMonitorRootResponse, wtypes.text, [wtypes.text], ignore_extra_args=True) def get_one(self, id, fields=None): """Gets a single healthmonitor's details.""" context = pecan_request.context.get('octavia_context') db_hm = self._get_db_hm(context.session, id, show_deleted=False) self._auth_validate_action(context, db_hm.project_id, consts.RBAC_GET_ONE) result = self._convert_db_to_type( db_hm, hm_types.HealthMonitorResponse) if fields is not None: result = self._filter_fields([result], fields)[0] return hm_types.HealthMonitorRootResponse(healthmonitor=result) @wsme_pecan.wsexpose(hm_types.HealthMonitorsRootResponse, wtypes.text, [wtypes.text], ignore_extra_args=True) def get_all(self, project_id=None, fields=None): """Gets all health monitors.""" pcontext = pecan_request.context context = pcontext.get('octavia_context') query_filter = self._auth_get_all(context, project_id) db_hm, links = self.repositories.health_monitor.get_all_API_list( context.session, show_deleted=False, pagination_helper=pcontext.get(consts.PAGINATION_HELPER), **query_filter) result = self._convert_db_to_type( db_hm, [hm_types.HealthMonitorResponse]) if fields is not None: result = self._filter_fields(result, fields) return hm_types.HealthMonitorsRootResponse( healthmonitors=result, healthmonitors_links=links) def _get_affected_listener_ids(self, session, hm): """Gets a list of all listeners this request potentially affects.""" pool = self.repositories.pool.get(session, id=hm.pool_id) listener_ids = [li.id for li in pool.listeners] return listener_ids def _test_lb_and_listener_and_pool_statuses(self, session, hm): """Verify load balancer is in a mutable state.""" # We need to verify that any listeners referencing this pool are also # mutable pool = self.repositories.pool.get(session, id=hm.pool_id) load_balancer_id = pool.load_balancer_id # Check the parent is not locked for some reason (ERROR, etc.) if pool.provisioning_status not in consts.MUTABLE_STATUSES: raise exceptions.ImmutableObject(resource='Pool', id=hm.pool_id) if not self.repositories.test_and_set_lb_and_listeners_prov_status( session, load_balancer_id, consts.PENDING_UPDATE, consts.PENDING_UPDATE, listener_ids=self._get_affected_listener_ids(session, hm), pool_id=hm.pool_id): LOG.info("Health Monitor cannot be created or modified because " "the Load Balancer is in an immutable state") raise exceptions.ImmutableObject(resource='Load Balancer', id=load_balancer_id) def _validate_create_hm(self, lock_session, hm_dict): """Validate creating health monitor on pool.""" mandatory_fields = (consts.TYPE, consts.DELAY, consts.TIMEOUT, consts.POOL_ID) for field in mandatory_fields: if hm_dict.get(field, None) is None: raise exceptions.InvalidOption(value='None', option=field) # MAX_RETRIES is renamed fall_threshold so handle is special if hm_dict.get(consts.RISE_THRESHOLD, None) is None: raise exceptions.InvalidOption(value='None', option=consts.MAX_RETRIES) if hm_dict[consts.TYPE] not in (consts.HEALTH_MONITOR_HTTP, consts.HEALTH_MONITOR_HTTPS): if hm_dict.get(consts.HTTP_METHOD, None): raise exceptions.InvalidOption( value=consts.HTTP_METHOD, option='health monitors of ' 'type {}'.format(hm_dict[consts.TYPE])) if hm_dict.get(consts.URL_PATH, None): raise exceptions.InvalidOption( value=consts.URL_PATH, option='health monitors of ' 'type {}'.format(hm_dict[consts.TYPE])) if hm_dict.get(consts.EXPECTED_CODES, None): raise exceptions.InvalidOption( value=consts.EXPECTED_CODES, option='health monitors of ' 'type {}'.format(hm_dict[consts.TYPE])) else: if not hm_dict.get(consts.HTTP_METHOD, None): hm_dict[consts.HTTP_METHOD] = ( consts.HEALTH_MONITOR_HTTP_DEFAULT_METHOD) if not hm_dict.get(consts.URL_PATH, None): hm_dict[consts.URL_PATH] = ( consts.HEALTH_MONITOR_DEFAULT_URL_PATH) if not hm_dict.get(consts.EXPECTED_CODES, None): hm_dict[consts.EXPECTED_CODES] = ( consts.HEALTH_MONITOR_DEFAULT_EXPECTED_CODES) if hm_dict.get('domain_name') and not hm_dict.get('http_version'): raise exceptions.ValidationException( detail=_("'http_version' must be specified when 'domain_name' " "is provided.")) if hm_dict.get('http_version') and hm_dict.get('domain_name'): if hm_dict['http_version'] < 1.1: raise exceptions.InvalidOption( value='http_version %s' % hm_dict['http_version'], option='health monitors HTTP 1.1 domain name health check') try: return self.repositories.health_monitor.create( lock_session, **hm_dict) except odb_exceptions.DBDuplicateEntry: raise exceptions.DuplicateHealthMonitor() except odb_exceptions.DBError: # TODO(blogan): will have to do separate validation protocol # before creation or update since the exception messages # do not give any information as to what constraint failed raise exceptions.InvalidOption(value='', option='') def _validate_healthmonitor_request_for_udp(self, request): if request.type not in ( consts.HEALTH_MONITOR_UDP_CONNECT, consts.HEALTH_MONITOR_TCP, consts.HEALTH_MONITOR_HTTP): raise exceptions.ValidationException(detail=_( "The associated pool protocol is %(pool_protocol)s, so only " "a %(types)s health monitor is supported.") % { 'pool_protocol': consts.PROTOCOL_UDP, 'types': '/'.join((consts.HEALTH_MONITOR_UDP_CONNECT, consts.HEALTH_MONITOR_TCP, consts.HEALTH_MONITOR_HTTP))}) # check the delay value if the HM type is UDP-CONNECT hm_is_type_udp = ( request.type == consts.HEALTH_MONITOR_UDP_CONNECT) conf_min_delay = ( CONF.api_settings.udp_connect_min_interval_health_monitor) if hm_is_type_udp and request.delay < conf_min_delay: raise exceptions.ValidationException(detail=_( "The request delay value %(delay)s should be larger than " "%(conf_min_delay)s for %(type)s health monitor type.") % { 'delay': request.delay, 'conf_min_delay': conf_min_delay, 'type': consts.HEALTH_MONITOR_UDP_CONNECT}) @wsme_pecan.wsexpose(hm_types.HealthMonitorRootResponse, body=hm_types.HealthMonitorRootPOST, status_code=201) def post(self, health_monitor_): """Creates a health monitor on a pool.""" context = pecan_request.context.get('octavia_context') health_monitor = health_monitor_.healthmonitor pool = self._get_db_pool(context.session, health_monitor.pool_id) health_monitor.project_id, provider = self._get_lb_project_id_provider( context.session, pool.load_balancer_id) self._auth_validate_action(context, health_monitor.project_id, consts.RBAC_POST) if (not CONF.api_settings.allow_ping_health_monitors and health_monitor.type == consts.HEALTH_MONITOR_PING): raise exceptions.DisabledOption( option='type', value=consts.HEALTH_MONITOR_PING) if pool.protocol == consts.PROTOCOL_UDP: self._validate_healthmonitor_request_for_udp(health_monitor) else: if health_monitor.type == consts.HEALTH_MONITOR_UDP_CONNECT: raise exceptions.ValidationException(detail=_( "The %(type)s type is only supported for pools of type " "%(protocol)s.") % {'type': health_monitor.type, 'protocol': consts.PROTOCOL_UDP}) # Load the driver early as it also provides validation driver = driver_factory.get_driver(provider) lock_session = db_api.get_session(autocommit=False) try: if self.repositories.check_quota_met( context.session, lock_session, data_models.HealthMonitor, health_monitor.project_id): raise exceptions.QuotaException( resource=data_models.HealthMonitor._name()) hm_dict = db_prepare.create_health_monitor( health_monitor.to_dict(render_unsets=True)) self._test_lb_and_listener_and_pool_statuses( lock_session, health_monitor) db_hm = self._validate_create_hm(lock_session, hm_dict) # Prepare the data for the driver data model provider_healthmon = (driver_utils.db_HM_to_provider_HM(db_hm)) # Dispatch to the driver LOG.info("Sending create Health Monitor %s to provider %s", db_hm.id, driver.name) driver_utils.call_provider( driver.name, driver.health_monitor_create, provider_healthmon) lock_session.commit() except odb_exceptions.DBError: lock_session.rollback() raise exceptions.InvalidOption( value=hm_dict.get('type'), option='type') except Exception: with excutils.save_and_reraise_exception(): lock_session.rollback() db_hm = self._get_db_hm(context.session, db_hm.id) result = self._convert_db_to_type( db_hm, hm_types.HealthMonitorResponse) return hm_types.HealthMonitorRootResponse(healthmonitor=result) def _graph_create(self, lock_session, hm_dict): hm_dict = db_prepare.create_health_monitor(hm_dict) db_hm = self._validate_create_hm(lock_session, hm_dict) return db_hm def _validate_update_hm(self, db_hm, health_monitor): if db_hm.type not in (consts.HEALTH_MONITOR_HTTP, consts.HEALTH_MONITOR_HTTPS): if health_monitor.http_method != wtypes.Unset: raise exceptions.InvalidOption( value=consts.HTTP_METHOD, option='health monitors of ' 'type {}'.format(db_hm.type)) if health_monitor.url_path != wtypes.Unset: raise exceptions.InvalidOption( value=consts.URL_PATH, option='health monitors of ' 'type {}'.format(db_hm.type)) if health_monitor.expected_codes != wtypes.Unset: raise exceptions.InvalidOption( value=consts.EXPECTED_CODES, option='health monitors of ' 'type {}'.format(db_hm.type)) if health_monitor.delay is None: raise exceptions.InvalidOption(value=None, option=consts.DELAY) if health_monitor.max_retries is None: raise exceptions.InvalidOption(value=None, option=consts.MAX_RETRIES) if health_monitor.timeout is None: raise exceptions.InvalidOption(value=None, option=consts.TIMEOUT) if health_monitor.domain_name and not ( db_hm.http_version or health_monitor.http_version): raise exceptions.ValidationException( detail=_("'http_version' must be specified when 'domain_name' " "is provided.")) if ((db_hm.http_version or health_monitor.http_version) and (db_hm.domain_name or health_monitor.domain_name)): http_version = health_monitor.http_version or db_hm.http_version if http_version < 1.1: raise exceptions.InvalidOption( value='http_version %s' % http_version, option='health monitors HTTP 1.1 domain name health check') def _set_default_on_none(self, health_monitor): """Reset settings to their default values if None/null was passed in A None/null value can be passed in to clear a value. PUT values that were not provided by the user have a type of wtypes.UnsetType. If the user is attempting to clear values, they should either be set to None (for example in the name field) or they should be reset to their default values. This method is intended to handle those values that need to be set back to a default value. """ if health_monitor.http_method is None: health_monitor.http_method = ( consts.HEALTH_MONITOR_HTTP_DEFAULT_METHOD) if health_monitor.url_path is None: health_monitor.url_path = ( consts.HEALTH_MONITOR_DEFAULT_URL_PATH) if health_monitor.expected_codes is None: health_monitor.expected_codes = ( consts.HEALTH_MONITOR_DEFAULT_EXPECTED_CODES) if health_monitor.max_retries_down is None: health_monitor.max_retries_down = consts.DEFAULT_MAX_RETRIES_DOWN @wsme_pecan.wsexpose(hm_types.HealthMonitorRootResponse, wtypes.text, body=hm_types.HealthMonitorRootPUT, status_code=200) def put(self, id, health_monitor_): """Updates a health monitor.""" context = pecan_request.context.get('octavia_context') health_monitor = health_monitor_.healthmonitor db_hm = self._get_db_hm(context.session, id, show_deleted=False) pool = self._get_db_pool(context.session, db_hm.pool_id) project_id, provider = self._get_lb_project_id_provider( context.session, pool.load_balancer_id) self._auth_validate_action(context, project_id, consts.RBAC_PUT) self._validate_update_hm(db_hm, health_monitor) # Validate health monitor update options for UDP-CONNECT type. if (pool.protocol == consts.PROTOCOL_UDP and db_hm.type == consts.HEALTH_MONITOR_UDP_CONNECT): health_monitor.type = db_hm.type self._validate_healthmonitor_request_for_udp(health_monitor) self._set_default_on_none(health_monitor) # Load the driver early as it also provides validation driver = driver_factory.get_driver(provider) with db_api.get_lock_session() as lock_session: self._test_lb_and_listener_and_pool_statuses(lock_session, db_hm) # Prepare the data for the driver data model healthmon_dict = health_monitor.to_dict(render_unsets=False) healthmon_dict['id'] = id provider_healthmon_dict = ( driver_utils.hm_dict_to_provider_dict(healthmon_dict)) # Also prepare the baseline object data old_provider_healthmon = driver_utils.db_HM_to_provider_HM(db_hm) # Dispatch to the driver LOG.info("Sending update Health Monitor %s to provider %s", id, driver.name) driver_utils.call_provider( driver.name, driver.health_monitor_update, old_provider_healthmon, driver_dm.HealthMonitor.from_dict(provider_healthmon_dict)) # Update the database to reflect what the driver just accepted health_monitor.provisioning_status = consts.PENDING_UPDATE db_hm_dict = health_monitor.to_dict(render_unsets=False) self.repositories.health_monitor.update(lock_session, id, **db_hm_dict) # Force SQL alchemy to query the DB, otherwise we get inconsistent # results context.session.expire_all() db_hm = self._get_db_hm(context.session, id) result = self._convert_db_to_type( db_hm, hm_types.HealthMonitorResponse) return hm_types.HealthMonitorRootResponse(healthmonitor=result) @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, id): """Deletes a health monitor.""" context = pecan_request.context.get('octavia_context') db_hm = self._get_db_hm(context.session, id, show_deleted=False) pool = self._get_db_pool(context.session, db_hm.pool_id) project_id, provider = self._get_lb_project_id_provider( context.session, pool.load_balancer_id) self._auth_validate_action(context, project_id, consts.RBAC_DELETE) if db_hm.provisioning_status == consts.DELETED: return # Load the driver early as it also provides validation driver = driver_factory.get_driver(provider) with db_api.get_lock_session() as lock_session: self._test_lb_and_listener_and_pool_statuses(lock_session, db_hm) self.repositories.health_monitor.update( lock_session, db_hm.id, provisioning_status=consts.PENDING_DELETE) LOG.info("Sending delete Health Monitor %s to provider %s", id, driver.name) provider_healthmon = driver_utils.db_HM_to_provider_HM(db_hm) driver_utils.call_provider( driver.name, driver.health_monitor_delete, provider_healthmon)