diff --git a/oslo_cache/_opts.py b/oslo_cache/_opts.py index 0787959f..b1577bba 100644 --- a/oslo_cache/_opts.py +++ b/oslo_cache/_opts.py @@ -44,6 +44,7 @@ FILE_OPTIONS = { 'dogpile.cache.bmemcached', 'dogpile.cache.dbm', 'dogpile.cache.redis', + 'dogpile.cache.redis_sentinel', 'dogpile.cache.memory', 'dogpile.cache.memory_pickle', 'dogpile.cache.null'], diff --git a/oslo_cache/core.py b/oslo_cache/core.py index addd5ec1..66765240 100644 --- a/oslo_cache/core.py +++ b/oslo_cache/core.py @@ -34,6 +34,7 @@ The library has special public value for nonexistent or expired keys called from oslo_cache import core NO_VALUE = core.NO_VALUE """ +import re import ssl import dogpile.cache @@ -100,6 +101,18 @@ class _DebugProxy(proxy.ProxyBackend): self.proxied.delete_multi(keys) +def _parse_sentinel(sentinel): + # IPv6 (eg. [::1]:6379 ) + match = re.search(r'\[(\S+)\]:(\d+)', sentinel) + if match: + return (match[1], int(match[2])) + # IPv4 or hostname (eg. 127.0.0.1:6379 or localhost:6379) + match = re.search(r'(\S+):(\d+)', sentinel) + if match: + return (match[1], int(match[2])) + raise ValueError('Malformed sentinel host format') + + def _build_cache_config(conf): """Build the cache region dictionary configuration. @@ -133,6 +146,19 @@ def _build_cache_config(conf): in ('dogpile.cache.memcached', 'oslo_cache.memcache_pool') and argname == 'url'): argvalue = argvalue.split(',') + + if argname == 'sentinels': + if conf.cache.backend != 'dogpile.cache.redis_sentinel': + raise exception.ConfigurationError( + 'sentinels in backend arguments is supported only by ' + 'the %s backend' % conf.cache.backend) + try: + argvalue = [_parse_sentinel(sentinel) for sentinel + in argvalue.split(',')] + except ValueError: + raise exception.ConfigurationError( + 'Backend arguments contain malformed sentinels argument') + conf_dict[arg_key] = argvalue _LOG.debug('Oslo Cache Config: %s', conf_dict) @@ -206,7 +232,8 @@ def _build_cache_config(conf): tls_context.set_ciphers(conf.cache.tls_allowed_ciphers) conf_dict['%s.arguments.tls_context' % prefix] = tls_context - elif conf.cache.backend in ('dogpile.cache.redis',): + elif conf.cache.backend in ('dogpile.cache.redis', + 'dogpile.cache.redis_sentinel'): if conf.cache.tls_allowed_ciphers is not None: raise exception.ConfigurationError( "Limiting allowed ciphers is not supported by " @@ -216,19 +243,21 @@ def _build_cache_config(conf): "FIPS mode is not supported by the %s backend" % conf.cache.backend) - client_kwargs = {'ssl': True} + c_kwargs = {'ssl': True} if conf.cache.tls_cafile is not None: _LOG.debug('Oslo Cache TLS - CA: %s', conf.cache.tls_cafile) - client_kwargs['ssl_ca_certs'] = conf.cache.tls_cafile + c_kwargs['ssl_ca_certs'] = conf.cache.tls_cafile if conf.cache.tls_certfile is not None: _LOG.debug('Oslo Cache TLS - cert: %s', conf.cache.tls_certfile) _LOG.debug('Oslo Cache TLS - key: %s', conf.cache.tls_keyfile) - client_kwargs.update({ + c_kwargs.update({ 'ssl_certfile': conf.cache.tls_certfile, 'ssl_keyfile': conf.cache.tls_keyfile }) - conf_dict['%s.arguments.client_kwargs' % prefix] = client_kwargs + conf_dict['%s.arguments.client_kwargs' % prefix] = c_kwargs + if conf.cache.backend == 'dogpile.cache.redis_sentinel': + conf_dict['%s.arguments.sentinel_kwargs' % prefix] = c_kwargs else: msg = _( "TLS setting via [cache] tls_enabled is not supported by this " diff --git a/oslo_cache/tests/unit/test_cache_basics.py b/oslo_cache/tests/unit/test_cache_basics.py index 147a8b1a..9ae3c932 100644 --- a/oslo_cache/tests/unit/test_cache_basics.py +++ b/oslo_cache/tests/unit/test_cache_basics.py @@ -357,6 +357,39 @@ class CacheRegionTest(test_cache.BaseTestCase): 'ssl_certfile': 'path_to_cert_file' }, config_dict['test_prefix.arguments.client_kwargs']) + self.assertNotIn('test_prefix.arguments.sentinel_kwargs', config_dict) + + def test_cache_dictionary_config_builder_tls_enabled_redis_sentinel(self): + """Validate the backend is reset to default if caching is disabled.""" + self.config_fixture.config(group='cache', + enabled=True, + config_prefix='test_prefix', + backend='dogpile.cache.redis_sentinel', + tls_enabled=True, + tls_cafile='path_to_ca_file', + tls_keyfile='path_to_key_file', + tls_certfile='path_to_cert_file') + + config_dict = cache._build_cache_config(self.config_fixture.conf) + + self.assertTrue(self.config_fixture.conf.cache.tls_enabled) + self.assertIn('test_prefix.arguments.client_kwargs', config_dict) + self.assertEqual( + { + 'ssl': True, + 'ssl_ca_certs': 'path_to_ca_file', + 'ssl_keyfile': 'path_to_key_file', + 'ssl_certfile': 'path_to_cert_file' + }, + config_dict['test_prefix.arguments.client_kwargs']) + self.assertEqual( + { + 'ssl': True, + 'ssl_ca_certs': 'path_to_ca_file', + 'ssl_keyfile': 'path_to_key_file', + 'ssl_certfile': 'path_to_cert_file' + }, + config_dict['test_prefix.arguments.sentinel_kwargs']) @mock.patch('oslo_cache.core._LOG') def test_cache_dictionary_config_builder_fips_mode_supported(self, log): @@ -693,6 +726,26 @@ class CacheRegionTest(test_cache.BaseTestCase): self.assertFalse(config_dict['test_prefix.arguments' '.pool_flush_on_reconnect']) + def test_cache_dictionary_config_builder_redis_sentinel(self): + """Validate the backend is reset to default if caching is disabled.""" + self.config_fixture.config(group='cache', + enabled=True, + config_prefix='test_prefix', + backend='dogpile.cache.redis_sentinel', + backend_argument=[ + ('sentinels:127.0.0.1:26379,' + '[::1]:26379,localhost:26379') + ]) + + config_dict = cache._build_cache_config(self.config_fixture.conf) + + self.assertFalse(self.config_fixture.conf.cache.tls_enabled) + self.assertEqual([ + ('127.0.0.1', 26379), + ('::1', 26379), + ('localhost', 26379), + ], config_dict['test_prefix.arguments.sentinels']) + def test_cache_debug_proxy(self): single_value = 'Test Value' single_key = 'testkey' diff --git a/releasenotes/notes/redis-sentinel-18ba4a0da83dabc7.yaml b/releasenotes/notes/redis-sentinel-18ba4a0da83dabc7.yaml new file mode 100644 index 00000000..ac1d33b6 --- /dev/null +++ b/releasenotes/notes/redis-sentinel-18ba4a0da83dabc7.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Now Redis Sentinel is supported as a cache backend.