diff --git a/doc/source/reference/healthcheck_plugins.rst b/doc/source/reference/healthcheck_plugins.rst index 3ea93c5..d49359f 100644 --- a/doc/source/reference/healthcheck_plugins.rst +++ b/doc/source/reference/healthcheck_plugins.rst @@ -8,6 +8,9 @@ .. automodule:: oslo_middleware.healthcheck.disable_by_file :members: +.. automodule:: oslo_middleware.healthcheck.enable_by_files + :members: + Available Plugins ------------------ diff --git a/oslo_middleware/healthcheck/__init__.py b/oslo_middleware/healthcheck/__init__.py index 0e7a7b0..a05b2b4 100644 --- a/oslo_middleware/healthcheck/__init__.py +++ b/oslo_middleware/healthcheck/__init__.py @@ -39,6 +39,7 @@ except ImportError: greenlet = None from oslo_middleware import base +from oslo_middleware.basic_auth import ConfigInvalid from oslo_middleware.healthcheck import opts @@ -397,6 +398,11 @@ Reason for r in self._conf_get('allowed_source_ranges')] self._ignore_proxied_requests = self._conf_get( 'ignore_proxied_requests') + + # (abhishekk): Verify that if `enable_by_files` and + # `disable_by_file` backends are not enabled at same time. + self._verify_configured_plugins() + self._backends = stevedore.NamedExtensionManager( self.NAMESPACE, self._conf_get('backends'), name_order=True, invoke_on_load=True, @@ -414,6 +420,16 @@ Reason self._default_accept = 'text/plain' self._ignore_path = False + def _verify_configured_plugins(self): + backends = self._conf_get('backends') + desired_plugins = ['disable_by_file', 'enable_by_files'] + + if set(desired_plugins).issubset(set(backends)): + raise ConfigInvalid('`enable_by_files` and `disable_by_file`' + ' healthcheck middleware should not be ' + 'configured at once.') + + def _conf_get(self, key, group='healthcheck'): return super(Healthcheck, self)._conf_get(key, group=group) diff --git a/oslo_middleware/healthcheck/enable_by_files.py b/oslo_middleware/healthcheck/enable_by_files.py new file mode 100644 index 0000000..727b6fd --- /dev/null +++ b/oslo_middleware/healthcheck/enable_by_files.py @@ -0,0 +1,60 @@ +# Copyright 2024 Red Hat. +# 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 opts +from oslo_middleware.healthcheck import pluginbase + +LOG = logging.getLogger(__name__) + + +class EnableByFilesHealthcheck(pluginbase.HealthcheckBaseExtension): + """EnableByFilesHealthcheck healthcheck middleware plugin + + This plugin checks presence of a file at a specified location. + + Example of middleware configuration: + + .. code-block:: ini + + [app:healthcheck] + paste.app_factory = oslo_middleware:Healthcheck.app_factory + path = /healthcheck + backends = enable_by_files + enable_by_file_paths = /var/lib/glance/images/.marker, + /var/lib/glance/os_glance_staging_store/.marker + # set to True to enable detailed output, False is the default + detailed = False + """ + + def __init__(self, *args, **kwargs): + super(EnableByFilesHealthcheck, self).__init__(*args, **kwargs) + self.oslo_conf.register_opts(opts.ENABLE_BY_FILES_OPTS, + group='healthcheck') + self.file_paths = self._conf_get('enable_by_file_paths') + + def healthcheck(self, server_port): + for file_path in self.file_paths: + if not os.path.exists(file_path): + LOG.warning('EnableByFiles healthcheck middleware: Path %s ' + 'is not present', file_path) + return pluginbase.HealthcheckResult( + available=False, reason="FILE PATH MISSING", + details='File path %s is missing' % file_path) + return pluginbase.HealthcheckResult( + available=True, reason="OK", + details='Specified file paths are available') diff --git a/oslo_middleware/healthcheck/opts.py b/oslo_middleware/healthcheck/opts.py index a3f78e9..262ba41 100644 --- a/oslo_middleware/healthcheck/opts.py +++ b/oslo_middleware/healthcheck/opts.py @@ -57,3 +57,11 @@ DISABLE_BY_FILES_OPTS = [ 'Expects a "port:path" list of strings. Used by ' 'DisableByFilesPortsHealthcheck plugin.'), ] + + +ENABLE_BY_FILES_OPTS = [ + cfg.ListOpt('enable_by_file_paths', + default=[], + help='Check the presence of files. Used by ' + 'EnableByFilesHealthcheck plugin.'), +] diff --git a/oslo_middleware/opts.py b/oslo_middleware/opts.py index 2bd2bcc..a111eea 100644 --- a/oslo_middleware/opts.py +++ b/oslo_middleware/opts.py @@ -157,7 +157,8 @@ def list_opts_healthcheck(): return [ ('healthcheck', copy.deepcopy(healthcheck_opts.HEALTHCHECK_OPTS + healthcheck_opts.DISABLE_BY_FILE_OPTS + - healthcheck_opts.DISABLE_BY_FILES_OPTS)) + healthcheck_opts.DISABLE_BY_FILES_OPTS + + healthcheck_opts.ENABLE_BY_FILES_OPTS)) ] diff --git a/oslo_middleware/tests/test_entry_points.py b/oslo_middleware/tests/test_entry_points.py index 26f31ac..89cdb6f 100644 --- a/oslo_middleware/tests/test_entry_points.py +++ b/oslo_middleware/tests/test_entry_points.py @@ -35,3 +35,17 @@ class TestPasteDeploymentEntryPoints(base.BaseTestCase): factory_names = [extension.name for extension in em] self.assertThat(factory_names, matchers.ContainsAll(factory_classes)) + + def test_healthcheck_entry_points(self): + healthcheck_plugins = { + 'disable_by_file': 'DisableByFileHealthcheck', + 'disable_by_files_ports': 'DisableByFilesPortsHealthcheck', + 'enable_by_files': 'EnableByFilesHealthcheck' + } + + em = stevedore.ExtensionManager('oslo.middleware.healthcheck') + + # Ensure all the healthcheck plugins are defined by their names + plugin_names = [extension.name for extension in em] + self.assertThat(plugin_names, + matchers.ContainsAll(healthcheck_plugins)) diff --git a/oslo_middleware/tests/test_healthcheck.py b/oslo_middleware/tests/test_healthcheck.py index 2c861dd..95d1848 100644 --- a/oslo_middleware/tests/test_healthcheck.py +++ b/oslo_middleware/tests/test_healthcheck.py @@ -24,6 +24,7 @@ import requests import webob.dec import webob.exc +from oslo_middleware.basic_auth import ConfigInvalid from oslo_middleware import healthcheck from oslo_middleware.healthcheck import __main__ @@ -201,6 +202,63 @@ class HealthcheckTests(test_base.BaseTestCase): server_port=81) self.assertIn('disable_by_files_ports', self.app._backends.names()) + def test_enablefile_disablefile_configured(self): + conf = {'backends': 'disable_by_file,enable_by_files'} + self.assertRaises(ConfigInvalid, + healthcheck.Healthcheck, self.application, conf) + + def test_enablefile_unconfigured(self): + conf = {'backends': 'enable_by_files'} + self._do_test(conf, expected_body=b'OK') + self.assertIn('enable_by_files', self.app._backends.names()) + + def test_enablefile_enabled(self): + filename = self.create_tempfiles([('.test', '.foobar')])[0] + conf = {'backends': 'enable_by_files', + 'enable_by_file_paths': filename} + self._do_test(conf, expected_body=b'OK') + self.assertIn('enable_by_files', self.app._backends.names()) + + def test_enablefile_enabled_head(self): + filename = self.create_tempfiles([('.test', '.foobar')])[0] + conf = {'backends': 'enable_by_files', + 'enable_by_file_paths': filename} + self._do_test(conf, expected_body=b'', method='HEAD', + expected_code=webob.exc.HTTPNoContent.code) + + def test_enablefile_enabled_html_detailed(self): + filename = self.create_tempfiles([('.test', '.foobar')])[0] + conf = {'backends': 'enable_by_files', + 'enable_by_file_paths': filename, 'detailed': True} + res = self._do_test_request(conf, accept="text/html") + self.assertIn(b'Result of 1 checks:', res.body) + self.assertIn(b'OK', res.body) + self.assertEqual(webob.exc.HTTPOk.code, res.status_int) + + def test_enablefile_disabled(self): + conf = {'backends': 'enable_by_files', + 'enable_by_file_paths': '.foobar'} + self._do_test(conf, + expected_code=webob.exc.HTTPServiceUnavailable.code, + expected_body=b'FILE PATH MISSING') + self.assertIn('enable_by_files', self.app._backends.names()) + + def test_enablefile_disabled_head(self): + conf = {'backends': 'enable_by_files', + 'enable_by_file_paths': '.foobar'} + self._do_test(conf, + expected_code=webob.exc.HTTPServiceUnavailable.code, + expected_body=b'', method='HEAD') + self.assertIn('enable_by_files', self.app._backends.names()) + + def test_enablefile_disabled_html_detailed(self): + conf = {'backends': 'enable_by_files', + 'enable_by_file_paths': '.foobar', 'detailed': True} + res = self._do_test_request(conf, accept="text/html") + self.assertIn(b'FILE PATH MISSING', res.body) + self.assertEqual(webob.exc.HTTPServiceUnavailable.code, + res.status_int) + def test_json_response(self): expected_body = jsonutils.dumps({'detailed': False, 'reasons': []}, indent=4, diff --git a/releasenotes/notes/enable-by-files-healthcheck-29938e3d6b8e5730.yaml b/releasenotes/notes/enable-by-files-healthcheck-29938e3d6b8e5730.yaml new file mode 100644 index 0000000..4171b02 --- /dev/null +++ b/releasenotes/notes/enable-by-files-healthcheck-29938e3d6b8e5730.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The new ``enable_by_files`` healthcheck plugin has been added. + This plugin will help to check whether specified file paths in + ``[healthcheck] enable_by_file_paths`` are present or not. diff --git a/setup.cfg b/setup.cfg index 9988cbe..61aba65 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,7 @@ oslo.config.opts = oslo.middleware.healthcheck = disable_by_file = oslo_middleware.healthcheck.disable_by_file:DisableByFileHealthcheck disable_by_files_ports = oslo_middleware.healthcheck.disable_by_file:DisableByFilesPortsHealthcheck + enable_by_files = oslo_middleware.healthcheck.enable_by_files:EnableByFilesHealthcheck paste.app_factory = healthcheck = oslo_middleware:Healthcheck.app_factory