From edabbd1619029c74c56134e1570736d1f71e1f21 Mon Sep 17 00:00:00 2001 From: Mehdi Abaakouk Date: Thu, 27 Nov 2014 18:05:24 +0100 Subject: Add healthcheck middleware Implements blueprint oslo-middleware-healthcheck Change-Id: Id19a47c07ff4fbf954ab188b7186361ed9ef213a --- oslo_middleware/healthcheck/__init__.py | 74 ++++++++++++++++++++++ oslo_middleware/healthcheck/disable_by_file.py | 38 ++++++++++++ oslo_middleware/healthcheck/pluginbase.py | 35 +++++++++++ oslo_middleware/tests/test_healthcheck.py | 85 ++++++++++++++++++++++++++ requirements.txt | 1 + setup.cfg | 3 + 6 files changed, 236 insertions(+) create mode 100644 oslo_middleware/healthcheck/__init__.py create mode 100644 oslo_middleware/healthcheck/disable_by_file.py create mode 100644 oslo_middleware/healthcheck/pluginbase.py create mode 100644 oslo_middleware/tests/test_healthcheck.py diff --git a/oslo_middleware/healthcheck/__init__.py b/oslo_middleware/healthcheck/__init__.py new file mode 100644 index 0000000..559770a --- /dev/null +++ b/oslo_middleware/healthcheck/__init__.py @@ -0,0 +1,74 @@ +# Copyright 2011 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import stevedore +import webob.dec +import webob.exc +import webob.response + +from oslo_middleware import base + + +class Healthcheck(base.Middleware): + """Helper class that returns debug information. + + Can be inserted into any WSGI application chain to get information about + the request and response. + """ + + NAMESPACE = "oslo.middleware.healthcheck" + + @classmethod + def factory(cls, global_conf, **local_conf): + """Factory method for paste.deploy.""" + conf = global_conf.copy() + conf.update(local_conf) + + def healthcheck_filter(app): + return cls(app, conf) + return cls + + def __init__(self, application, conf): + super(Healthcheck, self).__init__(application) + self._path = conf.get('path', '/healthcheck') + self._backend_names = [] + backends = conf.get('backends') + if backends: + self._backend_names = backends.split(',') + + self._backends = stevedore.NamedExtensionManager( + self.NAMESPACE, self._backend_names, + name_order=True, invoke_on_load=True, + invoke_args=(conf,)) + + @webob.dec.wsgify + def process_request(self, req): + if req.path != self._path: + return None + + healthy = True + reasons = [] + for ext in self._backends: + result = ext.obj.healthcheck() + healthy &= result.available + if result.reason: + reasons.append(result.reason) + + return webob.response.Response( + status=(webob.exc.HTTPOk.code if healthy + else webob.exc.HTTPServiceUnavailable.code), + body='\n'.join(reasons), + content_type="text/plain", + ) diff --git a/oslo_middleware/healthcheck/disable_by_file.py b/oslo_middleware/healthcheck/disable_by_file.py new file mode 100644 index 0000000..7e964a4 --- /dev/null +++ b/oslo_middleware/healthcheck/disable_by_file.py @@ -0,0 +1,38 @@ +# Copyright 2011 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os + +from oslo_middleware.healthcheck import pluginbase +from oslo_middleware.i18n import _LW + +LOG = logging.getLogger(__name__) + + +class DisableByFileHealthcheck(pluginbase.HealthcheckBaseExtension): + def healthcheck(self): + path = self.conf.get('disable_by_file_path') + if path is None: + LOG.warning(_LW('DisableByFile healthcheck middleware enabled ' + 'without disable_by_file_path set')) + return pluginbase.HealthcheckResult(available=True, + reason="") + elif not os.path.exists(path): + return pluginbase.HealthcheckResult(available=True, + reason="") + else: + return pluginbase.HealthcheckResult(available=False, + reason="DISABLED BY FILE") diff --git a/oslo_middleware/healthcheck/pluginbase.py b/oslo_middleware/healthcheck/pluginbase.py new file mode 100644 index 0000000..8c6e9d4 --- /dev/null +++ b/oslo_middleware/healthcheck/pluginbase.py @@ -0,0 +1,35 @@ +# Copyright 2011 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import collections + +import six + +HealthcheckResult = collections.namedtuple( + 'HealthcheckResult', ['available', 'reason'], verbose=True) + + +@six.add_metaclass(abc.ABCMeta) +class HealthcheckBaseExtension(object): + def __init__(self, conf): + self.conf = conf + + @abc.abstractmethod + def healthcheck(): + """method called by the healthcheck middleware + + return: HealthcheckResult object + """ diff --git a/oslo_middleware/tests/test_healthcheck.py b/oslo_middleware/tests/test_healthcheck.py new file mode 100644 index 0000000..46d964c --- /dev/null +++ b/oslo_middleware/tests/test_healthcheck.py @@ -0,0 +1,85 @@ +# Copyright (c) 2013 NEC Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslotest import base as test_base +import webob.dec +import webob.exc + +from oslo_middleware import healthcheck + + +class HealthcheckTests(test_base.BaseTestCase): + + @staticmethod + @webob.dec.wsgify + def application(req): + return 'Hello, World!!!' + + def _do_test(self, conf={}, path='/healthcheck', + expected_code=webob.exc.HTTPOk.code, + expected_body=b''): + self.app = healthcheck.Healthcheck(self.application, conf) + req = webob.Request.blank(path) + res = req.get_response(self.app) + self.assertEqual(expected_code, res.status_int) + self.assertEqual(expected_body, res.body) + + def test_default_path_match(self): + self._do_test() + + def test_default_path_not_match(self): + self._do_test(path='/toto', expected_body=b'Hello, World!!!') + + def test_configured_path_match(self): + conf = {'path': '/hidden_healthcheck'} + self._do_test(conf, path='/hidden_healthcheck') + + def test_configured_path_not_match(self): + conf = {'path': '/hidden_healthcheck'} + self._do_test(conf, path='/toto', expected_body=b'Hello, World!!!') + + @mock.patch('logging.warn') + def test_disablefile_unconfigured(self, fake_warn): + conf = {'backends': 'disable_by_file'} + self._do_test(conf, expected_body=b'') + self.assertIn('disable_by_file', self.app._backends.names()) + fake_warn.assert_called_once('DisableByFile healthcheck middleware ' + 'enabled without disable_by_file_path ' + 'set') + + def test_disablefile_enabled(self): + conf = {'backends': 'disable_by_file', + 'disable_by_file_path': '/foobar'} + self._do_test(conf, expected_body=b'') + self.assertIn('disable_by_file', self.app._backends.names()) + + def test_disablefile_disabled(self): + filename = self.create_tempfiles([('test', 'foobar')])[0] + conf = {'backends': 'disable_by_file', + 'disable_by_file_path': filename} + self._do_test(conf, + expected_code=webob.exc.HTTPServiceUnavailable.code, + expected_body=b'DISABLED BY FILE') + self.assertIn('disable_by_file', self.app._backends.names()) + + def test_two_backends(self): + filename = self.create_tempfiles([('test', 'foobar')])[0] + conf = {'backends': 'disable_by_file,disable_by_file', + 'disable_by_file_path': filename} + self._do_test(conf, + expected_code=webob.exc.HTTPServiceUnavailable.code, + expected_body=b'DISABLED BY FILE\nDISABLED BY FILE') + self.assertIn('disable_by_file', self.app._backends.names()) diff --git a/requirements.txt b/requirements.txt index 1b66bf0..90f75b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ oslo.config>=1.4.0 # Apache-2.0 oslo.context>=0.1.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 six>=1.7.0 +stevedore>=1.1.0 # Apache-2.0 WebOb>=1.2.3 diff --git a/setup.cfg b/setup.cfg index b44853b..601ed3b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,9 @@ namespace_packages = oslo.config.opts = oslo.middleware = oslo_middleware.opts:list_opts +oslo.middleware.healthcheck = + disable_by_file = oslo_middleware.healthcheck.disable_by_file:DisableByFileHealthcheck + [build_sphinx] source-dir = doc/source build-dir = doc/build -- cgit v1.2.1