From 18020e6c88bc1be11c7f19ec2a74a79a09bc2b88 Mon Sep 17 00:00:00 2001 From: Sam Morrison Date: Wed, 4 Mar 2020 10:35:56 +1100 Subject: [PATCH] Add oslo middleware healthcheck to Octavia API healthcheck middleware adds a /healthcheck url that allows unauthenticated access to provide a simple check when running octavia-api behind a load balancer https://docs.openstack.org/oslo.middleware/latest/reference/healthcheck_plugins.html Co-authored-by: Michael Johnson Change-Id: I10db6226750f7b7c703067d2ab82eea3a9875112 --- doc/source/admin/healthcheck.rst | 599 ++++++++++++++++++ doc/source/admin/index.rst | 1 + etc/octavia.conf | 21 + lower-constraints.txt | 6 +- octavia/api/app.py | 9 +- .../api/healthcheck/healthcheck_plugins.py | 47 ++ octavia/api/root_controller.py | 24 +- octavia/api/v2/controllers/amphora.py | 15 +- .../controllers/availability_zone_profiles.py | 12 +- .../api/v2/controllers/availability_zones.py | 12 +- octavia/api/v2/controllers/base.py | 9 +- octavia/api/v2/controllers/flavor_profiles.py | 12 +- octavia/api/v2/controllers/flavors.py | 12 +- octavia/api/v2/controllers/health_monitor.py | 12 +- octavia/api/v2/controllers/l7policy.py | 17 +- octavia/api/v2/controllers/l7rule.py | 12 +- octavia/api/v2/controllers/listener.py | 19 +- octavia/api/v2/controllers/load_balancer.py | 21 +- octavia/api/v2/controllers/member.py | 14 +- octavia/api/v2/controllers/pool.py | 17 +- octavia/api/v2/controllers/provider.py | 15 +- octavia/api/v2/controllers/quotas.py | 15 +- octavia/common/config.py | 3 + octavia/common/keystone.py | 3 +- octavia/db/healthcheck.py | 37 ++ .../tests/functional/api/test_healthcheck.py | 261 ++++++++ ...althcheck-middleware-6c09150bddd3113f.yaml | 6 + requirements.txt | 6 +- setup.cfg | 2 + 29 files changed, 1121 insertions(+), 118 deletions(-) create mode 100644 doc/source/admin/healthcheck.rst create mode 100644 octavia/api/healthcheck/healthcheck_plugins.py create mode 100644 octavia/db/healthcheck.py create mode 100644 octavia/tests/functional/api/test_healthcheck.py create mode 100644 releasenotes/notes/add-healthcheck-middleware-6c09150bddd3113f.yaml diff --git a/doc/source/admin/healthcheck.rst b/doc/source/admin/healthcheck.rst new file mode 100644 index 0000000000..ebe8ac8377 --- /dev/null +++ b/doc/source/admin/healthcheck.rst @@ -0,0 +1,599 @@ +.. + Copyright 2020 Red Hat, 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. + +============================= +Octavia API Health Monitoring +============================= + +The Octavia API provides a health monitoring endpoint that can be used by +external load balancers to manage the Octavia API pool. When properly +configured, the health monitoring endpoint will reflect the full operational +status of the Octavia API. + +The Octavia API health monitoring endpoint extends the `OpenStack Oslo +middleware healthcheck `_ library to test the Octavia Pecan API framework and associated services. + +Oslo Healthcheck Queries +======================== + +Oslo middleware healthcheck supports HTTP **"GET"** and **"HEAD"** methods. + +The response from Oslo middleware healthcheck can be customized by specifying +the acceptable response type for the request. + +Oslo middleware healthcheck currently supports the following types: + +* text/plain +* text/html +* application/json + +If the requested type is not one of the above, it defaults to text/plain. + +.. note:: + + The content of the response "reasons" will vary based on the backend plugins + enabled in Oslo middleware healthcheck. It is a best practice to only rely + on the HTTP status code for Octavia API health monitoring. + +Example Responses +----------------- + +Example passing output for text/plain with *detailed* False: + +.. code-block:: bash + + $ curl -i http://198.51.100.10/load-balancer/healthcheck + + HTTP/1.1 200 OK + Date: Mon, 16 Mar 2020 18:10:27 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: text/plain; charset=UTF-8 + Content-Length: 2 + x-openstack-request-id: req-9c6f4303-63a7-4f30-8afc-39340658702f + Connection: close + Vary: Accept-Encoding + + OK + +Example failing output for text/plain with *detailed* False: + +.. code-block:: bash + + $ curl -i http://198.51.100.10/load-balancer/healthcheck + + HTTP/1.1 503 Service Unavailable + Date: Mon, 16 Mar 2020 18:42:12 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: text/plain; charset=UTF-8 + Content-Length: 36 + x-openstack-request-id: req-84024269-2dfb-41ad-bfda-b3e1da138bba + Connection: close + +Example passing output for text/html with *detailed* False: + +.. code-block:: bash + + $ curl -i -H "Accept: text/html" http://198.51.100.10/load-balancer/healthcheck + + HTTP/1.1 200 OK + Date: Mon, 16 Mar 2020 18:25:11 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: text/html; charset=UTF-8 + Content-Length: 239 + x-openstack-request-id: req-b212d619-146f-4b50-91a3-5da16051badc + Connection: close + Vary: Accept-Encoding + + + Healthcheck Status + + +

Result of 1 checks:

+ + + + + + + + + + + + +
+ Reason +
OK
+
+ + + + +Example failing output for text/html with *detailed* False: + +.. code-block:: bash + + $ curl -i -H "Accept: text/html" http://198.51.100.10/load-balancer/healthcheck + + HTTP/1.1 503 Service Unavailable + Date: Mon, 16 Mar 2020 18:42:22 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: text/html; charset=UTF-8 + Content-Length: 273 + x-openstack-request-id: req-c91dd214-85ca-4d33-9fa3-2db81566d9e5 + Connection: close + + + Healthcheck Status + + +

Result of 1 checks:

+ + + + + + + + + + + + +
+ Reason +
The Octavia database is unavailable.
+
+ + + + +Example passing output for application/json with *detailed* False: + +.. code-block:: bash + + $ curl -i -H "Accept: application/json" http://192.51.100.10/load-balancer/healthcheck + + HTTP/1.1 200 OK + Date: Mon, 16 Mar 2020 18:34:42 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: application/json + Content-Length: 62 + x-openstack-request-id: req-417dc85c-e64e-496e-a461-494a3e6a5479 + Connection: close + + { + "detailed": false, + "reasons": [ + "OK" + ] + } + +Example failing output for application/json with *detailed* False: + +.. code-block:: bash + + $ curl -i -H "Accept: application/json" http://192.51.100.10/load-balancer/healthcheck + + HTTP/1.1 503 Service Unavailable + Date: Mon, 16 Mar 2020 18:46:28 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: application/json + Content-Length: 96 + x-openstack-request-id: req-de50b057-6105-4fca-a758-c872ef28bbfa + Connection: close + + { + "detailed": false, + "reasons": [ + "The Octavia database is unavailable." + ] + } + +Example Detailed Responses +-------------------------- + +Example passing output for text/plain with *detailed* True: + +.. code-block:: bash + + $ curl -i http://198.51.100.10/load-balancer/healthcheck + + HTTP/1.1 200 OK + Date: Mon, 16 Mar 2020 18:10:27 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: text/plain; charset=UTF-8 + Content-Length: 2 + x-openstack-request-id: req-9c6f4303-63a7-4f30-8afc-39340658702f + Connection: close + Vary: Accept-Encoding + + OK + +Example failing output for text/plain with *detailed* True: + +.. code-block:: bash + + $ curl -i http://198.51.100.10/load-balancer/healthcheck + + HTTP/1.1 503 Service Unavailable + Date: Mon, 16 Mar 2020 23:41:23 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: text/plain; charset=UTF-8 + Content-Length: 36 + x-openstack-request-id: req-2cd046cb-3a6c-45e3-921d-5f4a9e65c63e + Connection: close + +Example passing output for text/html with *detailed* True: + +.. code-block:: bash + + $ curl -i -H "Accept: text/html" http://198.51.100.10/load-balancer/healthcheck + + HTTP/1.1 200 OK + Date: Mon, 16 Mar 2020 22:11:54 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: text/html; charset=UTF-8 + Content-Length: 9927 + x-openstack-request-id: req-ae7404c9-b183-46dc-bb1b-e5f4e4984a57 + Connection: close + Vary: Accept-Encoding + + + Healthcheck Status + +

Server status

+ Server hostname:
devstack2
+ Current time:
2020-03-16 22:11:54.320529
+ Python version:
3.6.9 (default, Nov  7 2019, 10:44:02)
+     [GCC 8.3.0]
+ Platform:
Linux-4.15.0-88-generic-x86_64-with-Ubuntu-18.04-bionic
+
+

Garbage collector:

+ Counts:
(28, 10, 4)
+ Thresholds:
(700, 10, 10)
+
+

Result of 1 checks:

+ + + + + + + + + + + + + + +
+ Kind + + Reason + + Details +
OctaviaDBCheckResultOK
+
+

1 greenthread(s) active:

+ + + + + + +
 <...> 
+
+

1 thread(s) active:

+ + + + + + +
 <...> 
+ + + +Example failing output for text/html with *detailed* True: + +.. code-block:: bash + + $ curl -i -H "Accept: text/html" http://198.51.100.10/load-balancer/healthcheck + + HTTP/1.1 503 Service Unavailable + Date: Mon, 16 Mar 2020 23:43:52 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: text/html; charset=UTF-8 + Content-Length: 10211 + x-openstack-request-id: req-39b65058-6dc3-4069-a2d5-8a9714dba61d + Connection: close + + + Healthcheck Status + +

Server status

+ Server hostname:
devstack2
+ Current time:
2020-03-16 23:43:52.411127
+ Python version:
3.6.9 (default, Nov  7 2019, 10:44:02)
+     [GCC 8.3.0]
+ Platform:
Linux-4.15.0-88-generic-x86_64-with-Ubuntu-18.04-bionic
+
+

Garbage collector:

+ Counts:
(578, 10, 4)
+ Thresholds:
(700, 10, 10)
+
+

Result of 1 checks:

+ + + + + + + + + + + + + + +
+ Kind + + Reason + + Details +
OctaviaDBCheckResultThe Octavia database is unavailable.Database health check failed due to: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server on '127.0.0.1' ([Errno 111] Connection refused)") + [SQL: SELECT 1] + (Background on this error at: http://sqlalche.me/e/e3q8).
+
+

1 greenthread(s) active:

+ + + + + + +
 <...> 
+
+

1 thread(s) active:

+ + + + + + +
 <...> 
+ + + +Example passing output for application/json with *detailed* True: + +.. code-block:: bash + + $ curl -i -H "Accept: application/json" http://192.51.100.10/load-balancer/healthcheck + + HTTP/1.1 200 OK + Date: Mon, 16 Mar 2020 22:05:26 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: application/json + Content-Length: 9298 + x-openstack-request-id: req-d3913655-6e3f-4086-a252-8bb297ea5fd6 + Connection: close + + { + "detailed": true, + "gc": { + "counts": [ + 27, + 10, + 4 + ], + "threshold": [ + 700, + 10, + 10 + ] + }, + "greenthreads": [ + <...> + ], + "now": "2020-03-16 22:05:26.431429", + "platform": "Linux-4.15.0-88-generic-x86_64-with-Ubuntu-18.04-bionic", + "python_version": "3.6.9 (default, Nov 7 2019, 10:44:02) \n[GCC 8.3.0]", + "reasons": [ + { + "class": "OctaviaDBCheckResult", + "details": "", + "reason": "OK" + } + ], + "threads": [ + <...> + ] + } + +Example failing output for application/json with *detailed* True: + +.. code-block:: bash + + $ curl -i -H "Accept: application/json" http://192.51.100.10/load-balancer/healthcheck + + HTTP/1.1 503 Service Unavailable + Date: Mon, 16 Mar 2020 23:56:43 GMT + Server: Apache/2.4.29 (Ubuntu) + Content-Type: application/json + Content-Length: 9510 + x-openstack-request-id: req-3d62ea04-9bdb-4e19-b218-1a81ff7d7337 + Connection: close + + { + "detailed": true, + "gc": { + "counts": [ + 178, + 0, + 5 + ], + "threshold": [ + 700, + 10, + 10 + ] + }, + "greenthreads": [ + <...> + ], + "now": "2020-03-16 23:58:23.361209", + "platform": "Linux-4.15.0-88-generic-x86_64-with-Ubuntu-18.04-bionic", + "python_version": "3.6.9 (default, Nov 7 2019, 10:44:02) \n[GCC 8.3.0]", + "reasons": [ + { + "class": "OctaviaDBCheckResult", + "details": "(pymysql.err.OperationalError) (2003, \"Can't connect to MySQL server on '127.0.0.1' ([Errno 111] Connection refused)\")\n(Background on this error at: http://sqlalche.me/e/e3q8)", + "reason": "The Octavia database is unavailable." + } + ], + "threads": [ + <...> + ] + } + +Oslo Healthcheck Plugins +======================== + +The Octavia API health monitoring endpoint, implemented with Oslo middleware +healthcheck, is extensible using optional backend plugins. There are currently +plugins provided by the Oslo middleware library and plugins provided by +Octavia. + +**Oslo middleware provided plugins** + +* `disable_by_file `_ +* `disable_by_files_ports `_ + +**Octavia provided plugins** + +* `octavia_db_check`_ + +.. warning:: + + Some plugins may have long timeouts. It is a best practice to configure your + healthcheck query to have connection, read, and/or data timeouts. The + appropriate values will be unique to each deployment depending on the cloud + performance, number of plugins, etc. + +Enabling Octavia API Health Monitoring +====================================== + +To enable the Octavia API health monitoring endpoint, the proper configuration +file settings need to be updated and the Octavia API processes need to be +restarted. + +Start by enabling the endpoint: + +.. code-block:: ini + + [api_settings] + healthcheck_enabled = True + +When the healthcheck_enabled setting is *False*, queries of the /healthcheck +will receive an HTTP 404 Not Found response. + +You will then need to select the desired monitoring backend plugins: + +.. code-block:: ini + + [healthcheck] + backends = octavia_db_check + +.. note:: + + When no plugins are configured, the behavior of Oslo middleware healthcheck + changes. Not only does it not run any tests, it will return 204 results + instead of 200. + +Optionally you can enable the "detailed" mode in Oslo middleware healthcheck. +This will cause Oslo middleware healthcheck to return additional information +about the API instance. It will also provide exception details if one was +raised during the health check. This setting is False and disabled by default +in the Octavia API. + +.. code-block:: ini + + [healthcheck] + detailed = True + +.. warning:: + + Enabling the 'detailed' setting will expose sensitive details about + the API process. Do not enabled this unless you are sure it will + not pose a **security risk** to your API instances. + We highly recommend you do not enable this. + +Using Octavia API Health Monitoring +=================================== + +The Octavia API health monitoring endpoint can be accessed via the +/healthmonitor path on the `Octavia API endpoint `_. + +For example, if your Octavia (load-balancer) endpoint in keystone is: + +.. code-block:: bash + + https://10.21.21.78/load-balancer + +You would access the Octavia API health monitoring endpoint via: + +.. code-block:: bash + + https://10.21.21.78/load-balancer/healthcheck + +A keystone token is not required to access this endpoint. + +Octavia Plugins +=============== + +octavia_db_check +---------------- + +The octavia_db_check plugin validates the API instance has a working connection +to the Octavia database. It executes a SQL no-op query, 'SELECT 1;', against +the database. + +.. note:: + + Many OpenStack services and libraries, such as oslo.db and sqlalchemy, also + use the no-op query, 'SELECT 1;' for health checks. + +The possible octavia_db_check results are: + ++---------+--------+-------------+--------------------------------------+ +| Request | Result | Status Code | "reason" Message | ++=========+========+=============+======================================+ +| GET | Pass | 200 | OK | ++---------+--------+-------------+--------------------------------------+ +| HEAD | Pass | 204 | | ++---------+--------+-------------+--------------------------------------+ +| GET | Fail | 503 | The Octavia database is unavailable. | ++---------+--------+-------------+--------------------------------------+ +| HEAD | Fail | 503 | | ++---------+--------+-------------+--------------------------------------+ + +When running Oslo middleware healthcheck in "detailed" mode, the "details" +field will have additional information about the error encountered, including +the exception details if they were available. diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst index e3c47a2dc1..61882d0010 100644 --- a/doc/source/admin/index.rst +++ b/doc/source/admin/index.rst @@ -33,6 +33,7 @@ Optional Installation and Configuration Guides providers/index.rst log-offloading.rst api-audit.rst + healthcheck.rst flavors.rst apache-httpd.rst diff --git a/etc/octavia.conf b/etc/octavia.conf index f0de9acdbb..52ba372484 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -53,6 +53,9 @@ # The minimum health monitor delay interval for UDP-CONNECT Health Monitor type # udp_connect_min_interval_health_monitor = 3 +# Boolean to enable/disable oslo middleware /healthcheck in the Octavia API +# healthcheck_enabled = False + [database] # This line MUST be changed to actually run the plugin. # Example: @@ -572,3 +575,21 @@ # List of enabled provider agents. # enabled_provider_agents = + +[healthcheck] +# WARNING: Enabling the 'detailed' setting will expose sensitive details about +# the API process. Do not enabled this unless you are sure it will +# not pose a security risk to your API instances. +# We highly recommend you do not enable this. +# detailed = False + +# This is a list of oslo middleware healthcheck backend plugins to enable for +# the oslo middleware health check. +# +# Plugins provided by oslo middleware: +# disable_by_file +# disable_by_files_ports +# Plugins provided by Octavia: +# octavia_db_check +# +# backends = diff --git a/lower-constraints.txt b/lower-constraints.txt index 8d8a20348a..07e1479381 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -86,7 +86,7 @@ oslo.db==4.27.0 oslo.i18n==3.15.3 oslo.log==3.36.0 oslo.messaging==6.3.0 -oslo.middleware==3.31.0 +oslo.middleware==4.0.1 oslo.policy==1.30.0 oslo.reports==1.18.0 oslo.serialization==2.18.0 @@ -99,7 +99,7 @@ paramiko==2.4.1 Paste==2.0.3 PasteDeploy==1.5.2 pbr==2.0.0 -pecan==1.0.0 +pecan==1.3.2 pep8==1.7.1 pika==0.10.0 pika-pool==0.1.3 @@ -169,7 +169,7 @@ vine==1.1.4 voluptuous==0.11.1 waitress==1.1.0 warlock==1.3.0 -WebOb==1.7.1 +WebOb==1.8.2 WebTest==2.0.29 Werkzeug==0.14.1 wrapt==1.10.11 diff --git a/octavia/api/app.py b/octavia/api/app.py index 2ed45f9409..b0deb5c70b 100644 --- a/octavia/api/app.py +++ b/octavia/api/app.py @@ -20,7 +20,8 @@ from oslo_log import log as logging from oslo_middleware import cors from oslo_middleware import http_proxy_to_wsgi from oslo_middleware import request_id -import pecan +from pecan import configuration as pecan_configuration +from pecan import make_app as pecan_make_app from octavia.api import config as app_config from octavia.api.drivers import driver_factory @@ -36,7 +37,7 @@ CONF = cfg.CONF def get_pecan_config(): """Returns the pecan config.""" filename = app_config.__file__.replace('.pyc', '.py') - return pecan.configuration.conf_from_file(filename) + return pecan_configuration.conf_from_file(filename) def _init_drivers(): @@ -56,9 +57,9 @@ def setup_app(pecan_config=None, debug=False, argv=None): if not pecan_config: pecan_config = get_pecan_config() - pecan.configuration.set_config(dict(pecan_config), overwrite=True) + pecan_configuration.set_config(dict(pecan_config), overwrite=True) - return pecan.make_app( + return pecan_make_app( pecan_config.app.root, wrap_app=_wrap_app, debug=debug, diff --git a/octavia/api/healthcheck/healthcheck_plugins.py b/octavia/api/healthcheck/healthcheck_plugins.py new file mode 100644 index 0000000000..d17a134c82 --- /dev/null +++ b/octavia/api/healthcheck/healthcheck_plugins.py @@ -0,0 +1,47 @@ +# Copyright 2020 Red Hat, 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. +from oslo_middleware.healthcheck import pluginbase + +from octavia.db import api as db_apis +from octavia.db import healthcheck + + +class OctaviaDBHealthcheck(pluginbase.HealthcheckBaseExtension): + + UNAVAILABLE_REASON = 'The Octavia database is unavailable.' + + def __init__(self, *args, **kwargs): + super(OctaviaDBHealthcheck, self).__init__(*args, **kwargs) + + def healthcheck(self, server_port): + try: + result, message = healthcheck.check_database_connection( + db_apis.get_session()) + if result: + return OctaviaDBCheckResult(available=True, reason="OK") + else: + return OctaviaDBCheckResult(available=False, + reason=self.UNAVAILABLE_REASON, + details=message) + except Exception as e: + return OctaviaDBCheckResult(available=False, + reason=self.UNAVAILABLE_REASON, + details=str(e)) + + +class OctaviaDBCheckResult(pluginbase.HealthcheckResult): + """Result sub-class to provide a unique name in detail reports.""" + + def __init__(self, *args, **kwargs): + super(OctaviaDBCheckResult, self).__init__(*args, **kwargs) diff --git a/octavia/api/root_controller.py b/octavia/api/root_controller.py index 37e344d9d7..a3d20d42b1 100644 --- a/octavia/api/root_controller.py +++ b/octavia/api/root_controller.py @@ -12,25 +12,41 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_config import cfg from oslo_log import log as logging +from oslo_middleware import healthcheck +from pecan import abort as pecan_abort +from pecan import expose as pecan_expose from pecan import request as pecan_request -from pecan import rest from wsme import types as wtypes from wsmeext import pecan as wsme_pecan from octavia.api.v2 import controllers as v2_controller - +CONF = cfg.CONF LOG = logging.getLogger(__name__) -class RootController(rest.RestController): +class RootController(object): """The controller with which the pecan wsgi app should be created.""" def __init__(self): super(RootController, self).__init__() setattr(self, 'v2.0', v2_controller.V2Controller()) setattr(self, 'v2', v2_controller.V2Controller()) + if CONF.api_settings.healthcheck_enabled: + self.healthcheck_obj = healthcheck.Healthcheck.app_factory(None) + + # Run the oslo middleware healthcheck for /healthcheck + @pecan_expose('json') + @pecan_expose(content_type='plain/text') + @pecan_expose(content_type='text/html') + def healthcheck(self): # pylint: disable=inconsistent-return-statements + if CONF.api_settings.healthcheck_enabled: + if pecan_request.method not in ['GET', 'HEAD']: + pecan_abort(405) + return self.healthcheck_obj.process_request(pecan_request) + pecan_abort(404) def _add_a_version(self, versions, version, url_version, status, timestamp, base_url): @@ -45,7 +61,7 @@ class RootController(rest.RestController): }) @wsme_pecan.wsexpose(wtypes.text) - def get(self): + def index(self): host_url = pecan_request.path_url if not host_url.endswith('/'): diff --git a/octavia/api/v2/controllers/amphora.py b/octavia/api/v2/controllers/amphora.py index eeecbd9fca..f7d9923802 100644 --- a/octavia/api/v2/controllers/amphora.py +++ b/octavia/api/v2/controllers/amphora.py @@ -17,7 +17,8 @@ from oslo_config import cfg from oslo_log import log as logging import oslo_messaging as messaging from oslo_utils import excutils -import pecan +from pecan import expose as pecan_expose +from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -41,7 +42,7 @@ class AmphoraController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_one(self, id, fields=None): """Gets a single amphora's details.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_amp = self._get_db_amp(context.session, id, show_deleted=False) self._auth_validate_action(context, context.project_id, @@ -57,7 +58,7 @@ class AmphoraController(base.BaseController): ignore_extra_args=True) def get_all(self, fields=None): """Gets all health monitors.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') self._auth_validate_action(context, context.project_id, @@ -73,7 +74,7 @@ class AmphoraController(base.BaseController): return amp_types.AmphoraeRootResponse( amphorae=result, amphorae_links=links) - @pecan.expose() + @pecan_expose() def _lookup(self, amphora_id, *remainder): """Overridden pecan _lookup method for custom routing. @@ -107,7 +108,7 @@ class FailoverController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=202) def put(self): """Fails over an amphora""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') db_amp = self._get_db_amp(context.session, self.amp_id, show_deleted=False) @@ -153,7 +154,7 @@ class AmphoraUpdateController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=202) def put(self): """Update amphora agent configuration""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') db_amp = self._get_db_amp(context.session, self.amp_id, show_deleted=False) @@ -188,7 +189,7 @@ class AmphoraStatsController(base.BaseController): @wsme_pecan.wsexpose(amp_types.StatisticsRootResponse, wtypes.text, status_code=200) def get(self): - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_STATS) diff --git a/octavia/api/v2/controllers/availability_zone_profiles.py b/octavia/api/v2/controllers/availability_zone_profiles.py index 24f87b166f..5176ea9d49 100644 --- a/octavia/api/v2/controllers/availability_zone_profiles.py +++ b/octavia/api/v2/controllers/availability_zone_profiles.py @@ -17,7 +17,7 @@ from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import excutils from oslo_utils import uuidutils -import pecan +from pecan import request as pecan_request from sqlalchemy.orm import exc as sa_exception from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -43,7 +43,7 @@ class AvailabilityZoneProfileController(base.BaseController): wtypes.text, [wtypes.text], ignore_extra_args=True) def get_one(self, id, fields=None): """Gets an Availability Zone Profile's detail.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ONE) if id == constants.NIL_UUID: @@ -63,7 +63,7 @@ class AvailabilityZoneProfileController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_all(self, fields=None): """Lists all Availability Zone Profiles.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ALL) @@ -87,7 +87,7 @@ class AvailabilityZoneProfileController(base.BaseController): """Creates an Availability Zone Profile.""" availability_zone_profile = ( availability_zone_profile_.availability_zone_profile) - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_POST) # Do a basic JSON validation on the metadata @@ -155,7 +155,7 @@ class AvailabilityZoneProfileController(base.BaseController): """Updates an Availability Zone Profile.""" availability_zone_profile = ( availability_zone_profile_.availability_zone_profile) - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_PUT) @@ -216,7 +216,7 @@ class AvailabilityZoneProfileController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, availability_zone_profile_id): """Deletes an Availability Zone Profile""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_DELETE) diff --git a/octavia/api/v2/controllers/availability_zones.py b/octavia/api/v2/controllers/availability_zones.py index 0709f7b483..5f75036560 100644 --- a/octavia/api/v2/controllers/availability_zones.py +++ b/octavia/api/v2/controllers/availability_zones.py @@ -16,7 +16,7 @@ from oslo_db import api as oslo_db_api from oslo_db import exception as odb_exceptions from oslo_log import log as logging from oslo_utils import excutils -import pecan +from pecan import request as pecan_request from sqlalchemy.orm import exc as sa_exception from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -40,7 +40,7 @@ class AvailabilityZonesController(base.BaseController): wtypes.text, [wtypes.text], ignore_extra_args=True) def get_one(self, name, fields=None): """Gets an Availability Zone's detail.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ONE) if name == constants.NIL_UUID: @@ -60,7 +60,7 @@ class AvailabilityZonesController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_all(self, fields=None): """Lists all Availability Zones.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ALL) @@ -82,7 +82,7 @@ class AvailabilityZonesController(base.BaseController): def post(self, availability_zone_): """Creates an Availability Zone.""" availability_zone = availability_zone_.availability_zone - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_POST) @@ -111,7 +111,7 @@ class AvailabilityZonesController(base.BaseController): body=availability_zone_types.AvailabilityZoneRootPUT) def put(self, name, availability_zone_): availability_zone = availability_zone_.availability_zone - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_PUT) if name == constants.NIL_UUID: @@ -144,7 +144,7 @@ class AvailabilityZonesController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, availability_zone_name): """Deletes an Availability Zone""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_DELETE) diff --git a/octavia/api/v2/controllers/base.py b/octavia/api/v2/controllers/base.py index ab490ccb38..633183bc05 100644 --- a/octavia/api/v2/controllers/base.py +++ b/octavia/api/v2/controllers/base.py @@ -16,7 +16,8 @@ from cryptography.hazmat.backends import default_backend from cryptography import x509 from oslo_config import cfg from oslo_log import log as logging -import pecan +from pecan import request as pecan_request +from pecan import rest as pecan_rest from stevedore import driver as stevedore_driver from wsme import types as wtypes @@ -31,7 +32,7 @@ CONF = cfg.CONF LOG = logging.getLogger(__name__) -class BaseController(pecan.rest.RestController): +class BaseController(pecan_rest.RestController): RBAC_TYPE = None def __init__(self): @@ -257,7 +258,7 @@ class BaseController(pecan.rest.RestController): return attrs def _validate_tls_refs(self, tls_refs): - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') bad_refs = [] for ref in tls_refs: try: @@ -272,7 +273,7 @@ class BaseController(pecan.rest.RestController): raise exceptions.CertificateRetrievalException(ref=bad_refs) def _validate_client_ca_and_crl_refs(self, client_ca_ref, crl_ref): - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') bad_refs = [] try: self.cert_manager.set_acls(context, client_ca_ref) diff --git a/octavia/api/v2/controllers/flavor_profiles.py b/octavia/api/v2/controllers/flavor_profiles.py index fb37959a5f..91f915f6ab 100644 --- a/octavia/api/v2/controllers/flavor_profiles.py +++ b/octavia/api/v2/controllers/flavor_profiles.py @@ -18,7 +18,7 @@ from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import excutils from oslo_utils import uuidutils -import pecan +from pecan import request as pecan_request from sqlalchemy.orm import exc as sa_exception from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -44,7 +44,7 @@ class FlavorProfileController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_one(self, id, fields=None): """Gets a flavor profile's detail.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ONE) if id == constants.NIL_UUID: @@ -61,7 +61,7 @@ class FlavorProfileController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_all(self, fields=None): """Lists all flavor profiles.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ALL) @@ -81,7 +81,7 @@ class FlavorProfileController(base.BaseController): def post(self, flavor_profile_): """Creates a flavor Profile.""" flavorprofile = flavor_profile_.flavorprofile - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_POST) # Do a basic JSON validation on the metadata @@ -138,7 +138,7 @@ class FlavorProfileController(base.BaseController): def put(self, id, flavor_profile_): """Updates a flavor Profile.""" flavorprofile = flavor_profile_.flavorprofile - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_PUT) @@ -190,7 +190,7 @@ class FlavorProfileController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, flavor_profile_id): """Deletes a Flavor Profile""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_DELETE) diff --git a/octavia/api/v2/controllers/flavors.py b/octavia/api/v2/controllers/flavors.py index 367657a0df..edde0590e3 100644 --- a/octavia/api/v2/controllers/flavors.py +++ b/octavia/api/v2/controllers/flavors.py @@ -18,7 +18,7 @@ from oslo_db import exception as odb_exceptions from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import uuidutils -import pecan +from pecan import request as pecan_request from sqlalchemy.orm import exc as sa_exception from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -42,7 +42,7 @@ class FlavorsController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_one(self, id, fields=None): """Gets a flavor's detail.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ONE) if id == constants.NIL_UUID: @@ -58,7 +58,7 @@ class FlavorsController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_all(self, fields=None): """Lists all flavors.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ALL) @@ -77,7 +77,7 @@ class FlavorsController(base.BaseController): def post(self, flavor_): """Creates a flavor.""" flavor = flavor_.flavor - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_POST) @@ -106,7 +106,7 @@ class FlavorsController(base.BaseController): body=flavor_types.FlavorRootPUT) def put(self, id, flavor_): flavor = flavor_.flavor - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_PUT) if id == constants.NIL_UUID: @@ -134,7 +134,7 @@ class FlavorsController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, flavor_id): """Deletes a Flavor""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_DELETE) diff --git a/octavia/api/v2/controllers/health_monitor.py b/octavia/api/v2/controllers/health_monitor.py index 90278f03b0..8285a9422a 100644 --- a/octavia/api/v2/controllers/health_monitor.py +++ b/octavia/api/v2/controllers/health_monitor.py @@ -17,7 +17,7 @@ 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 -import pecan +from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -48,7 +48,7 @@ class HealthMonitorController(base.BaseController): [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') + 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, @@ -64,7 +64,7 @@ class HealthMonitorController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_all(self, project_id=None, fields=None): """Gets all health monitors.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') query_filter = self._auth_get_all(context, project_id) @@ -196,7 +196,7 @@ class HealthMonitorController(base.BaseController): 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') + context = pecan_request.context.get('octavia_context') health_monitor = health_monitor_.healthmonitor if (not CONF.api_settings.allow_ping_health_monitors and @@ -334,7 +334,7 @@ class HealthMonitorController(base.BaseController): body=hm_types.HealthMonitorRootPUT, status_code=200) def put(self, id, health_monitor_): """Updates a health monitor.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') health_monitor = health_monitor_.healthmonitor db_hm = self._get_db_hm(context.session, id, show_deleted=False) @@ -393,7 +393,7 @@ class HealthMonitorController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, id): """Deletes a health monitor.""" - context = pecan.request.context.get('octavia_context') + 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) diff --git a/octavia/api/v2/controllers/l7policy.py b/octavia/api/v2/controllers/l7policy.py index 2c82e0741d..4d31011c1d 100644 --- a/octavia/api/v2/controllers/l7policy.py +++ b/octavia/api/v2/controllers/l7policy.py @@ -16,7 +16,8 @@ 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 -import pecan +from pecan import expose as pecan_expose +from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -48,7 +49,7 @@ class L7PolicyController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get(self, id, fields=None): """Gets a single l7policy's details.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_l7policy = self._get_db_l7policy(context.session, id, show_deleted=False) @@ -65,7 +66,7 @@ class L7PolicyController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_all(self, project_id=None, fields=None): """Lists all l7policies of a listener.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') query_filter = self._auth_get_all(context, project_id) @@ -115,7 +116,7 @@ class L7PolicyController(base.BaseController): def post(self, l7policy_): """Creates a l7policy on a listener.""" l7policy = l7policy_.l7policy - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') # Verify the parent listener exists listener_id = l7policy.listener_id listener = self._get_db_listener( @@ -206,7 +207,7 @@ class L7PolicyController(base.BaseController): if val in l7policy_dict: l7policy_dict[attr] = l7policy_dict.pop(val) sanitized_l7policy = l7policy_types.L7PolicyPUT(**l7policy_dict) - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_l7policy = self._get_db_l7policy(context.session, id, show_deleted=False) @@ -268,7 +269,7 @@ class L7PolicyController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, id): """Deletes a l7policy.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_l7policy = self._get_db_l7policy(context.session, id, show_deleted=False) load_balancer_id, listener_id = self._get_listener_and_loadbalancer_id( @@ -300,14 +301,14 @@ class L7PolicyController(base.BaseController): driver_utils.call_provider(driver.name, driver.l7policy_delete, provider_l7policy) - @pecan.expose() + @pecan_expose() def _lookup(self, l7policy_id, *remainder): """Overridden pecan _lookup method for custom routing. Verifies that the l7policy passed in the url exists, and if so decides which controller, if any, should control be passed. """ - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') if l7policy_id and remainder and remainder[0] == 'rules': remainder = remainder[1:] db_l7policy = self.repositories.l7policy.get( diff --git a/octavia/api/v2/controllers/l7rule.py b/octavia/api/v2/controllers/l7rule.py index 8d1c8a56e3..fda37097e4 100644 --- a/octavia/api/v2/controllers/l7rule.py +++ b/octavia/api/v2/controllers/l7rule.py @@ -15,7 +15,7 @@ from oslo_db import exception as odb_exceptions from oslo_log import log as logging from oslo_utils import excutils -import pecan +from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -46,7 +46,7 @@ class L7RuleController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get(self, id, fields=None): """Gets a single l7rule's details.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_l7rule = self._get_db_l7rule(context.session, id, show_deleted=False) @@ -63,7 +63,7 @@ class L7RuleController(base.BaseController): ignore_extra_args=True) def get_all(self, fields=None): """Lists all l7rules of a l7policy.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') l7policy = self._get_db_l7policy(context.session, self.l7policy_id, @@ -127,7 +127,7 @@ class L7RuleController(base.BaseController): validate.l7rule_data(l7rule) except Exception as e: raise exceptions.L7RuleValidation(error=e) - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_l7policy = self._get_db_l7policy(context.session, self.l7policy_id, show_deleted=False) @@ -189,7 +189,7 @@ class L7RuleController(base.BaseController): def put(self, id, l7rule_): """Updates a l7rule.""" l7rule = l7rule_.rule - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_l7rule = self._get_db_l7rule(context.session, id, show_deleted=False) @@ -256,7 +256,7 @@ class L7RuleController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, id): """Deletes a l7rule.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_l7rule = self._get_db_l7rule(context.session, id, show_deleted=False) diff --git a/octavia/api/v2/controllers/listener.py b/octavia/api/v2/controllers/listener.py index fec4948e65..7bac00f747 100644 --- a/octavia/api/v2/controllers/listener.py +++ b/octavia/api/v2/controllers/listener.py @@ -17,7 +17,8 @@ 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 -import pecan +from pecan import expose as pecan_expose +from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -51,7 +52,7 @@ class ListenersController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_one(self, id, fields=None): """Gets a single listener's details.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_listener = self._get_db_listener(context.session, id, show_deleted=False) @@ -72,7 +73,7 @@ class ListenersController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_all(self, project_id=None, fields=None): """Lists all listeners.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') query_filter = self._auth_get_all(context, project_id) @@ -237,7 +238,7 @@ class ListenersController(base.BaseController): # Validate that the L4 protocol (UDP or TCP) is not already used for # the specified protocol_port in this load balancer - pcontext = pecan.request.context + pcontext = pecan_request.context query_filter = { 'project_id': listener_dict['project_id'], 'load_balancer_id': listener_dict['load_balancer_id'], @@ -310,7 +311,7 @@ class ListenersController(base.BaseController): def post(self, listener_): """Creates a listener on a load balancer.""" listener = listener_.listener - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') load_balancer_id = listener.loadbalancer_id listener.project_id, provider = self._get_lb_project_id_provider( @@ -507,7 +508,7 @@ class ListenersController(base.BaseController): def put(self, id, listener_): """Updates a listener on a load balancer.""" listener = listener_.listener - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_listener = self._get_db_listener(context.session, id, show_deleted=False) load_balancer_id = db_listener.load_balancer_id @@ -567,7 +568,7 @@ class ListenersController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, id): """Deletes a listener from a load balancer.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_listener = self._get_db_listener(context.session, id, show_deleted=False) load_balancer_id = db_listener.load_balancer_id @@ -594,7 +595,7 @@ class ListenersController(base.BaseController): driver_utils.call_provider(driver.name, driver.listener_delete, provider_listener) - @pecan.expose() + @pecan_expose() def _lookup(self, id, *remainder): """Overridden pecan _lookup method for custom routing. @@ -616,7 +617,7 @@ class StatisticsController(base.BaseController, stats.StatsMixin): @wsme_pecan.wsexpose(listener_types.StatisticsRootResponse, wtypes.text, status_code=200) def get(self): - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_listener = self._get_db_listener(context.session, self.id, show_deleted=False) if not db_listener: diff --git a/octavia/api/v2/controllers/load_balancer.py b/octavia/api/v2/controllers/load_balancer.py index 72f6b6b42d..046760cb63 100644 --- a/octavia/api/v2/controllers/load_balancer.py +++ b/octavia/api/v2/controllers/load_balancer.py @@ -18,7 +18,8 @@ from oslo_db import exception as odb_exceptions from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import strutils -import pecan +from pecan import expose as pecan_expose +from pecan import request as pecan_request from sqlalchemy.orm import exc as sa_exception from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -56,7 +57,7 @@ class LoadBalancersController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_one(self, id, fields=None): """Gets a single load balancer's details.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') load_balancer = self._get_db_lb(context.session, id, show_deleted=False) @@ -78,7 +79,7 @@ class LoadBalancersController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_all(self, project_id=None, fields=None): """Lists all load balancers.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') query_filter = self._auth_get_all(context, project_id) @@ -373,7 +374,7 @@ class LoadBalancersController(base.BaseController): def post(self, load_balancer): """Creates a load balancer.""" load_balancer = load_balancer.loadbalancer - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') if not load_balancer.project_id and context.project_id: load_balancer.project_id = context.project_id @@ -607,7 +608,7 @@ class LoadBalancersController(base.BaseController): def put(self, id, load_balancer): """Updates a load balancer.""" load_balancer = load_balancer.loadbalancer - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_lb = self._get_db_lb(context.session, id, show_deleted=False) self._auth_validate_action(context, db_lb.project_id, @@ -665,7 +666,7 @@ class LoadBalancersController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204) def delete(self, id, cascade=False): """Deletes a load balancer.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') cascade = strutils.bool_from_string(cascade) db_lb = self._get_db_lb(context.session, id, show_deleted=False) @@ -692,7 +693,7 @@ class LoadBalancersController(base.BaseController): driver_utils.call_provider(driver.name, driver.loadbalancer_delete, provider_loadbalancer, cascade) - @pecan.expose() + @pecan_expose() def _lookup(self, id, *remainder): """Overridden pecan _lookup method for custom routing. @@ -731,7 +732,7 @@ class StatusController(base.BaseController): @wsme_pecan.wsexpose(lb_types.StatusRootResponse, wtypes.text, status_code=200) def get(self): - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') load_balancer = self._get_db_lb(context.session, self.id, show_deleted=False) if not load_balancer: @@ -759,7 +760,7 @@ class StatisticsController(base.BaseController, stats.StatsMixin): @wsme_pecan.wsexpose(lb_types.StatisticsRootResponse, wtypes.text, status_code=200) def get(self): - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') load_balancer = self._get_db_lb(context.session, self.id, show_deleted=False) if not load_balancer: @@ -787,7 +788,7 @@ class FailoverController(LoadBalancersController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=202) def put(self, **kwargs): """Fails over a loadbalancer""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_lb = self._get_db_lb(context.session, self.lb_id, show_deleted=False) diff --git a/octavia/api/v2/controllers/member.py b/octavia/api/v2/controllers/member.py index 210532318d..9f5749e524 100644 --- a/octavia/api/v2/controllers/member.py +++ b/octavia/api/v2/controllers/member.py @@ -17,7 +17,7 @@ from oslo_db import exception as odb_exceptions from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import strutils -import pecan +from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -48,7 +48,7 @@ class MemberController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get(self, id, fields=None): """Gets a single pool member's details.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_member = self._get_db_member(context.session, id, show_deleted=False) @@ -67,7 +67,7 @@ class MemberController(base.BaseController): ignore_extra_args=True) def get_all(self, fields=None): """Lists all pool members of a pool.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') pool = self._get_db_pool(context.session, self.pool_id, @@ -144,7 +144,7 @@ class MemberController(base.BaseController): def post(self, member_): """Creates a pool member on a pool.""" member = member_.member - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') validate.ip_not_reserved(member.address) @@ -229,7 +229,7 @@ class MemberController(base.BaseController): def put(self, id, member_): """Updates a pool member.""" member = member_.member - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_member = self._get_db_member(context.session, id, show_deleted=False) @@ -285,7 +285,7 @@ class MemberController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, id): """Deletes a pool member.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_member = self._get_db_member(context.session, id, show_deleted=False) @@ -327,7 +327,7 @@ class MembersController(MemberController): """Updates all members.""" members = members_.members additive_only = strutils.bool_from_string(additive_only) - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_pool = self._get_db_pool(context.session, self.pool_id) old_members = db_pool.members diff --git a/octavia/api/v2/controllers/pool.py b/octavia/api/v2/controllers/pool.py index eb9cc2a94f..9528a2b8df 100644 --- a/octavia/api/v2/controllers/pool.py +++ b/octavia/api/v2/controllers/pool.py @@ -17,7 +17,8 @@ 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 -import pecan +from pecan import expose as pecan_expose +from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -51,7 +52,7 @@ class PoolsController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get(self, id, fields=None): """Gets a pool's details.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_pool = self._get_db_pool(context.session, id, show_deleted=False) self._auth_validate_action(context, db_pool.project_id, @@ -66,7 +67,7 @@ class PoolsController(base.BaseController): [wtypes.text], ignore_extra_args=True) def get_all(self, project_id=None, fields=None): """Lists all pools.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') query_filter = self._auth_get_all(context, project_id) @@ -188,7 +189,7 @@ class PoolsController(base.BaseController): # For some API requests the listener_id will be passed in the # pool_dict: pool = pool_.pool - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') if pool.protocol == constants.PROTOCOL_UDP: self._validate_pool_request_for_udp(pool) else: @@ -372,7 +373,7 @@ class PoolsController(base.BaseController): def put(self, id, pool_): """Updates a pool on a load balancer.""" pool = pool_.pool - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_pool = self._get_db_pool(context.session, id, show_deleted=False) project_id, provider = self._get_lb_project_id_provider( @@ -429,7 +430,7 @@ class PoolsController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) def delete(self, id): """Deletes a pool from a load balancer.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') db_pool = self._get_db_pool(context.session, id, show_deleted=False) if db_pool.l7policies: raise exceptions.PoolInUseByL7Policy( @@ -458,14 +459,14 @@ class PoolsController(base.BaseController): driver_utils.call_provider(driver.name, driver.pool_delete, provider_pool) - @pecan.expose() + @pecan_expose() def _lookup(self, pool_id, *remainder): """Overridden pecan _lookup method for custom routing. Verifies that the pool passed in the url exists, and if so decides which controller, if any, should control be passed. """ - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') if pool_id and remainder and remainder[0] == 'members': remainder = remainder[1:] db_pool = self.repositories.pool.get(context.session, id=pool_id) diff --git a/octavia/api/v2/controllers/provider.py b/octavia/api/v2/controllers/provider.py index 16e502a139..355fa07fbf 100644 --- a/octavia/api/v2/controllers/provider.py +++ b/octavia/api/v2/controllers/provider.py @@ -14,7 +14,8 @@ from oslo_config import cfg from oslo_log import log as logging -import pecan +from pecan import expose as pecan_expose +from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -39,7 +40,7 @@ class ProviderController(base.BaseController): ignore_extra_args=True) def get_all(self, fields=None): """List enabled provider drivers and their descriptions.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') self._auth_validate_action(context, context.project_id, @@ -53,7 +54,7 @@ class ProviderController(base.BaseController): response_list = self._filter_fields(response_list, fields) return provider_types.ProvidersRootResponse(providers=response_list) - @pecan.expose() + @pecan_expose() def _lookup(self, provider, *remainder): """Overridden pecan _lookup method for custom routing. @@ -82,7 +83,7 @@ class FlavorCapabilitiesController(base.BaseController): [wtypes.text], ignore_extra_args=True, status_code=200) def get_all(self, fields=None): - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ALL) self.driver = driver_factory.get_driver(self.provider) @@ -97,7 +98,7 @@ class FlavorCapabilitiesController(base.BaseController): # Apply any valid filters provided as URL parameters name_filter = None description_filter = None - pagination_helper = pecan.request.context.get( + pagination_helper = pecan_request.context.get( constants.PAGINATION_HELPER) if pagination_helper: name_filter = pagination_helper.params.get(constants.NAME) @@ -132,7 +133,7 @@ class AvailabilityZoneCapabilitiesController(base.BaseController): [wtypes.text], ignore_extra_args=True, status_code=200) def get_all(self, fields=None): - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, context.project_id, constants.RBAC_GET_ALL) self.driver = driver_factory.get_driver(self.provider) @@ -149,7 +150,7 @@ class AvailabilityZoneCapabilitiesController(base.BaseController): # Apply any valid filters provided as URL parameters name_filter = None description_filter = None - pagination_helper = pecan.request.context.get( + pagination_helper = pecan_request.context.get( constants.PAGINATION_HELPER) if pagination_helper: name_filter = pagination_helper.params.get(constants.NAME) diff --git a/octavia/api/v2/controllers/quotas.py b/octavia/api/v2/controllers/quotas.py index 15d436dd56..5c386ea477 100644 --- a/octavia/api/v2/controllers/quotas.py +++ b/octavia/api/v2/controllers/quotas.py @@ -13,7 +13,8 @@ # under the License. from oslo_config import cfg -import pecan +from pecan import expose as pecan_expose +from pecan import request as pecan_request from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -35,7 +36,7 @@ class QuotasController(base.BaseController): @wsme_pecan.wsexpose(quota_types.QuotaResponse, wtypes.text) def get(self, project_id): """Get a single project's quota details.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') self._auth_validate_action(context, project_id, constants.RBAC_GET_ONE) @@ -46,7 +47,7 @@ class QuotasController(base.BaseController): ignore_extra_args=True) def get_all(self, project_id=None): """List all non-default quotas.""" - pcontext = pecan.request.context + pcontext = pecan_request.context context = pcontext.get('octavia_context') query_filter = self._auth_get_all(context, project_id) @@ -63,7 +64,7 @@ class QuotasController(base.BaseController): body=quota_types.QuotaPUT, status_code=202) def put(self, project_id, quotas): """Update any or all quotas for a project.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') if not project_id: raise exceptions.MissingAPIProjectID() @@ -79,7 +80,7 @@ class QuotasController(base.BaseController): @wsme_pecan.wsexpose(None, wtypes.text, status_code=202) def delete(self, project_id): """Reset a project's quotas to the default values.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') if not project_id: raise exceptions.MissingAPIProjectID() @@ -90,7 +91,7 @@ class QuotasController(base.BaseController): db_quotas = self._get_db_quotas(context.session, project_id) return self._convert_db_to_type(db_quotas, quota_types.QuotaResponse) - @pecan.expose() + @pecan_expose() def _lookup(self, project_id, *remainder): """Overridden pecan _lookup method for routing default endpoint.""" if project_id and remainder and remainder[0] == 'default': @@ -108,7 +109,7 @@ class QuotasDefaultController(base.BaseController): @wsme_pecan.wsexpose(quota_types.QuotaResponse, wtypes.text) def get(self): """Get a project's default quota details.""" - context = pecan.request.context.get('octavia_context') + context = pecan_request.context.get('octavia_context') if not self.project_id: raise exceptions.MissingAPIProjectID() diff --git a/octavia/common/config.py b/octavia/common/config.py index ee5011dbf1..d394975b08 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -100,6 +100,9 @@ api_opts = [ help=_("The minimum health monitor delay interval for the " "UDP-CONNECT Health Monitor type. A negative integer " "value means 'no limit'.")), + cfg.BoolOpt('healthcheck_enabled', default=False, + help=_("When True, the oslo middleware healthcheck endpoint " + "is enabled in the Octavia API.")), ] # Options only used by the amphora agent diff --git a/octavia/common/keystone.py b/octavia/common/keystone.py index c86dda23df..1fd6334ac2 100644 --- a/octavia/common/keystone.py +++ b/octavia/common/keystone.py @@ -21,7 +21,8 @@ from octavia.common import constants LOG = logging.getLogger(__name__) -_NOAUTH_PATHS = ['/', '/load-balancer/'] +_NOAUTH_PATHS = ['/', '/load-balancer/', '/healthcheck', + '/load-balancer/healthcheck'] class KeystoneSession(object): diff --git a/octavia/db/healthcheck.py b/octavia/db/healthcheck.py new file mode 100644 index 0000000000..589dd1105c --- /dev/null +++ b/octavia/db/healthcheck.py @@ -0,0 +1,37 @@ +# Copyright 2020 Red Hat, 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. +from oslo_log import log as logging + +from octavia.i18n import _ + +LOG = logging.getLogger(__name__) + + +def check_database_connection(session): + """This is a simple database connection check function. + + It will do a simple no-op query (low overhead) against the sqlalchemy + session passed in. + + :param session: A Sql Alchemy database session. + :returns: True if the connection check is successful, False if not. + """ + try: + session.execute('SELECT 1;') + return True, None + except Exception as e: + message = _('Database health check failed due to: {err}.').format( + err=str(e)) + LOG.error(message) + return False, message diff --git a/octavia/tests/functional/api/test_healthcheck.py b/octavia/tests/functional/api/test_healthcheck.py new file mode 100644 index 0000000000..65ffe1cce4 --- /dev/null +++ b/octavia/tests/functional/api/test_healthcheck.py @@ -0,0 +1,261 @@ +# Copyright 2020 Red Hat, 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. +from unittest import mock + +from oslo_config import cfg +from oslo_config import fixture as oslo_fixture +import pecan + +from octavia.api import config as pconfig +from octavia.api.healthcheck import healthcheck_plugins +from octavia.tests.functional.db import base as base_db_test + + +class TestHealthCheck(base_db_test.OctaviaDBTestBase): + + def setUp(self): + super(TestHealthCheck, self).setUp() + + # We need to define these early as they are late loaded in oslo + # middleware and our configuration overrides would not apply. + # Note: These must match exactly the option definitions in + # oslo.middleware healthcheck! If not you will get duplicate option + # errors. + healthcheck_opts = [ + cfg.BoolOpt( + 'detailed', default=False, + help='Show more detailed information as part of the response. ' + 'Security note: Enabling this option may expose ' + 'sensitive details about the service being monitored. ' + 'Be sure to verify that it will not violate your ' + 'security policies.'), + cfg.ListOpt( + 'backends', default=[], + help='Additional backends that can perform health checks and ' + 'report that information back as part of a request.'), + ] + cfg.CONF.register_opts(healthcheck_opts, group='healthcheck') + + self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + self.conf.config(group='healthcheck', backends=['octavia_db_check']) + self.UNAVAILABLE = (healthcheck_plugins.OctaviaDBHealthcheck. + UNAVAILABLE_REASON) + + def reset_pecan(): + pecan.set_config({}, overwrite=True) + + self.addCleanup(reset_pecan) + + def _make_app(self): + # Note: we need to set argv=() to stop the wsgi setup_app from + # pulling in the testing tool sys.argv + return pecan.testing.load_test_app({'app': pconfig.app, + 'wsme': pconfig.wsme}, argv=()) + + def _get_enabled_app(self): + self.conf.config(group='api_settings', healthcheck_enabled=True) + return self._make_app() + + def _get_disabled_app(self): + self.conf.config(group='api_settings', healthcheck_enabled=False) + return self._make_app() + + def _get(self, app, path, params=None, headers=None, status=200, + expect_errors=False): + response = app.get(path, params=params, headers=headers, status=status, + expect_errors=expect_errors) + return response + + def _head(self, app, path, headers=None, status=204, expect_errors=False): + response = app.head(path, headers=headers, status=status, + expect_errors=expect_errors) + return response + + def _post(self, app, path, body, headers=None, status=201, + expect_errors=False): + response = app.post_json(path, params=body, headers=headers, + status=status, expect_errors=expect_errors) + return response + + def _put(self, app, path, body, headers=None, status=200, + expect_errors=False): + response = app.put_json(path, params=body, headers=headers, + status=status, expect_errors=expect_errors) + return response + + def _delete(self, app, path, params=None, headers=None, status=204, + expect_errors=False): + response = app.delete(path, headers=headers, status=status, + expect_errors=expect_errors) + return response + + def test_healthcheck_get_text(self): + self.conf.config(group='healthcheck', detailed=False) + response = self._get(self._get_enabled_app(), '/healthcheck') + self.assertEqual(200, response.status_code) + self.assertEqual('OK', response.text) + + # Note: For whatever reason, detailed=True text has no additonal info + def test_healthcheck_get_text_detailed(self): + self.conf.config(group='healthcheck', detailed=True) + response = self._get(self._get_enabled_app(), '/healthcheck') + self.assertEqual(200, response.status_code) + self.assertEqual('OK', response.text) + + def test_healthcheck_get_json(self): + self.conf.config(group='healthcheck', detailed=False) + response = self._get(self._get_enabled_app(), '/healthcheck', + headers={'Accept': 'application/json'}) + self.assertEqual(200, response.status_code) + self.assertFalse(response.json['detailed']) + self.assertEqual(['OK'], response.json['reasons']) + + def test_healthcheck_get_json_detailed(self): + self.conf.config(group='healthcheck', detailed=True) + response = self._get(self._get_enabled_app(), '/healthcheck', + headers={'Accept': 'application/json'}) + self.assertEqual(200, response.status_code) + self.assertTrue(response.json['detailed']) + self.assertEqual('OK', response.json['reasons'][0]['reason']) + self.assertTrue(response.json['gc']) + + def test_healthcheck_get_html(self): + self.conf.config(group='healthcheck', detailed=False) + response = self._get(self._get_enabled_app(), '/healthcheck', + headers={'Accept': 'text/html'}) + self.assertEqual(200, response.status_code) + self.assertIn('OK', response.text) + + def test_healthcheck_get_html_detailed(self): + self.conf.config(group='healthcheck', detailed=True) + response = self._get(self._get_enabled_app(), '/healthcheck', + headers={'Accept': 'text/html'}) + self.assertEqual(200, response.status_code) + self.assertIn('OK', response.text) + self.assertIn('Garbage collector', response.text) + + def test_healthcheck_disabled_get(self): + self._get(self._get_disabled_app(), '/healthcheck', status=404) + + def test_healthcheck_head(self): + response = self._head(self._get_enabled_app(), '/healthcheck') + self.assertEqual(204, response.status_code) + + def test_healthcheck_disabled_head(self): + self._head(self._get_disabled_app(), '/healthcheck', status=404) + + # These should be denied by the API + def test_healthcheck_post(self): + self._post(self._get_enabled_app(), '/healthcheck', + {'foo': 'bar'}, status=405) + + def test_healthcheck_put(self): + self._put(self._get_enabled_app(), '/healthcheck', + {'foo': 'bar'}, status=405) + + def test_healthcheck_delete(self): + self._delete(self._get_enabled_app(), '/healthcheck', + status=405) + + @mock.patch('octavia.db.api.get_session') + def test_healthcheck_get_failed(self, mock_get_session): + mock_session = mock.MagicMock() + mock_session.execute.side_effect = [Exception('boom')] + mock_get_session.return_value = mock_session + response = self._get(self._get_enabled_app(), '/healthcheck', + status=503) + self.assertEqual(503, response.status_code) + self.assertEqual(self.UNAVAILABLE, response.text) + + @mock.patch('octavia.db.api.get_session') + def test_healthcheck_head_failed(self, mock_get_session): + mock_session = mock.MagicMock() + mock_session.execute.side_effect = [Exception('boom')] + mock_get_session.return_value = mock_session + response = self._head(self._get_enabled_app(), '/healthcheck', + status=503) + self.assertEqual(503, response.status_code) + + @mock.patch('octavia.db.healthcheck.check_database_connection', + side_effect=Exception('boom')) + def test_healthcheck_get_failed_check(self, mock_db_check): + response = self._get(self._get_enabled_app(), '/healthcheck', + status=503) + self.assertEqual(503, response.status_code) + self.assertEqual(self.UNAVAILABLE, response.text) + + @mock.patch('octavia.db.api.get_session') + def test_healthcheck_get_json_failed(self, mock_get_session): + self.conf.config(group='healthcheck', detailed=False) + mock_session = mock.MagicMock() + mock_session.execute.side_effect = [Exception('boom')] + mock_get_session.return_value = mock_session + response = self._get(self._get_enabled_app(), '/healthcheck', + headers={'Accept': 'application/json'}, + status=503) + self.assertEqual(503, response.status_code) + self.assertFalse(response.json['detailed']) + self.assertEqual([self.UNAVAILABLE], + response.json['reasons']) + + @mock.patch('octavia.db.api.get_session') + def test_healthcheck_get_json_detailed_failed(self, mock_get_session): + self.conf.config(group='healthcheck', detailed=True) + mock_session = mock.MagicMock() + mock_session.execute.side_effect = [Exception('boom')] + mock_get_session.return_value = mock_session + response = self._get(self._get_enabled_app(), '/healthcheck', + headers={'Accept': 'application/json'}, + status=503) + self.assertEqual(503, response.status_code) + self.assertTrue(response.json['detailed']) + self.assertEqual(self.UNAVAILABLE, + response.json['reasons'][0]['reason']) + self.assertIn('boom', response.json['reasons'][0]['details']) + + @mock.patch('octavia.db.api.get_session') + def test_healthcheck_get_html_failed(self, mock_get_session): + self.conf.config(group='healthcheck', detailed=False) + mock_session = mock.MagicMock() + mock_session.execute.side_effect = [Exception('boom')] + mock_get_session.return_value = mock_session + response = self._get(self._get_enabled_app(), '/healthcheck', + headers={'Accept': 'text/html'}, status=503) + self.assertEqual(503, response.status_code) + self.assertIn(self.UNAVAILABLE, response.text) + + @mock.patch('octavia.db.api.get_session') + def test_healthcheck_get_html_detailed_failed(self, mock_get_session): + self.conf.config(group='healthcheck', detailed=True) + mock_session = mock.MagicMock() + mock_session.execute.side_effect = [Exception('boom')] + mock_get_session.return_value = mock_session + response = self._get(self._get_enabled_app(), '/healthcheck', + headers={'Accept': 'text/html'}, status=503) + self.assertEqual(503, response.status_code) + self.assertIn(self.UNAVAILABLE, response.text) + self.assertIn('boom', response.text) + self.assertIn('Garbage collector', response.text) + + # Note: For whatever reason, detailed=True text has no additonal info + @mock.patch('octavia.db.api.get_session') + def test_healthcheck_get_text_detailed_failed(self, mock_get_session): + self.conf.config(group='healthcheck', detailed=True) + mock_session = mock.MagicMock() + mock_session.execute.side_effect = [Exception('boom')] + mock_get_session.return_value = mock_session + response = self._get(self._get_enabled_app(), '/healthcheck', + status=503) + self.assertEqual(503, response.status_code) + self.assertEqual(self.UNAVAILABLE, response.text) diff --git a/releasenotes/notes/add-healthcheck-middleware-6c09150bddd3113f.yaml b/releasenotes/notes/add-healthcheck-middleware-6c09150bddd3113f.yaml new file mode 100644 index 0000000000..8c8c7c17d0 --- /dev/null +++ b/releasenotes/notes/add-healthcheck-middleware-6c09150bddd3113f.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added the oslo-middleware healthcheck app to the Octavia API. + Hitting /healthcheck will return a 200. This is enabled via the + [api_settings]healthcheck_enabled setting and is disabled by default. diff --git a/requirements.txt b/requirements.txt index 6b89e20934..f365dd2468 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. alembic>=0.8.10 # MIT cotyledon>=1.3.0 # Apache-2.0 -pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD +pecan>=1.3.2 # BSD pbr!=2.1.0,>=2.0.0 # Apache-2.0 SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT Babel!=2.4.0,>=2.3.4 # BSD @@ -13,7 +13,7 @@ rfc3986>=0.3.1 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0 keystonemiddleware>=4.17.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 -WebOb>=1.7.1 # MIT +WebOb>=1.8.2 # MIT stevedore>=1.20.0 # Apache-2.0 oslo.config>=5.2.0 # Apache-2.0 oslo.context>=2.19.2 # Apache-2.0 @@ -21,7 +21,7 @@ oslo.db>=4.27.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.log>=3.36.0 # Apache-2.0 oslo.messaging>=6.3.0 # Apache-2.0 -oslo.middleware>=3.31.0 # Apache-2.0 +oslo.middleware>=4.0.1 # Apache-2.0 oslo.policy>=1.30.0 # Apache-2.0 oslo.reports>=1.18.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index da4e744897..3d667e11c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -100,6 +100,8 @@ oslo.policy.policies = octavia = octavia.policies:list_rules oslo.policy.enforcer = octavia = octavia.common.policy:get_no_context_enforcer +oslo.middleware.healthcheck = + octavia_db_check = octavia.api.healthcheck.healthcheck_plugins:OctaviaDBHealthcheck [compile_catalog] directory = octavia/locale