# octavia.api.v2.controllers.health_monitor

# Source code for octavia.api.v2.controllers.health_monitor

    #    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 octavia_lib.common import constants as lib_consts
    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 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().__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')
            with context.session.begin():
                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)

            with context.session.begin():
                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:
                ret = self.repositories.health_monitor.create(
                    lock_session, **hm_dict)
                lock_session.flush()
                return ret
            except odb_exceptions.DBDuplicateEntry as e:
                raise exceptions.DuplicateHealthMonitor() from e
            except odb_exceptions.DBReferenceError as e:
                raise exceptions.InvalidOption(value=hm_dict.get(e.key),
                                               option=e.key) from e
            except odb_exceptions.DBError as e:
                raise exceptions.APIException() from e

        def _validate_healthmonitor_request_for_udp_sctp(self, request,
                                                         pool_protocol):
            if request.type not in (
                    consts.HEALTH_MONITOR_UDP_CONNECT,
                    lib_consts.HEALTH_MONITOR_SCTP,
                    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': pool_protocol,
                        'types': '/'.join((consts.HEALTH_MONITOR_UDP_CONNECT,
                                           lib_consts.HEALTH_MONITOR_SCTP,
                                           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

            with context.session.begin():
                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 in (lib_consts.PROTOCOL_UDP,
                                 lib_consts.PROTOCOL_SCTP):
                self._validate_healthmonitor_request_for_udp_sctp(health_monitor,
                                                                  pool.protocol)
            else:
                if health_monitor.type in (consts.HEALTH_MONITOR_UDP_CONNECT,
                                           lib_consts.HEALTH_MONITOR_SCTP):
                    raise exceptions.ValidationException(detail=_(
                        "The %(type)s type is only supported for pools of type "
                        "%(protocols)s.") % {
                            'type': health_monitor.type,
                            'protocols': '/'.join((consts.PROTOCOL_UDP,
                                                   lib_consts.PROTOCOL_SCTP))})

            # Load the driver early as it also provides validation
            driver = driver_factory.get_driver(provider)

            context.session.begin()
            try:
                if self.repositories.check_quota_met(
                        context.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(
                    context.session, health_monitor)
                db_hm = self._validate_create_hm(context.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)

                context.session.commit()
            except odb_exceptions.DBError as e:
                context.session.rollback()
                raise exceptions.InvalidOption(
                    value=hm_dict.get('type'), option='type') from e
            except Exception:
                with excutils.save_and_reraise_exception():
                    context.session.rollback()

            with context.session.begin():
                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
            with context.session.begin():
                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/SCTP
            if pool.protocol in (lib_consts.PROTOCOL_UDP,
                                 lib_consts.PROTOCOL_SCTP):
                health_monitor.type = db_hm.type
                self._validate_healthmonitor_request_for_udp_sctp(health_monitor,
                                                                  pool.protocol)

            self._set_default_on_none(health_monitor)

            # Load the driver early as it also provides validation
            driver = driver_factory.get_driver(provider)

            with context.session.begin():

                self._test_lb_and_listener_and_pool_statuses(context.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(context.session, id,
                                                        **db_hm_dict)

            # Force SQL alchemy to query the DB, otherwise we get inconsistent
            # results
            context.session.expire_all()
            with context.session.begin():
                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')
            with context.session.begin():
                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 context.session.begin():

                self._test_lb_and_listener_and_pool_statuses(context.session,
                                                             db_hm)

                self.repositories.health_monitor.update(
                    context.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)

this page last updated: 2026-04-02 20:14:22

Except where otherwise noted, this document is licensed under Creative Commons Attribution 3.0 License. See all OpenStack Legal Documents.

found an error? report a bug