From 7bb43bbbd58110b204c2ce9023e96d68442c21c9 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Thu, 1 Feb 2024 00:58:06 +0900 Subject: [PATCH] Add support for Redis Sentinel backend This introduces support for Redis Sentinel backend. Users can now use Redis Sentinel backend instead of Redis backend by configurations like the example below. [cache] enabled = True backend = dogpile.cache.redis_sentinel redis_password = redis_sentinels = 192.0.2.1:26379,192.0.2.2:26379,192.0.2.3:26379 If tls_enabled option is set to True then all the tls settings are applied for connections to Redis as well as connections to Redis Sentinel. Change-Id: Ic3b84fe6810e08337a884c68625ccfed11665269 --- oslo_cache/_opts.py | 11 +- oslo_cache/core.py | 45 ++++++- oslo_cache/tests/unit/test_cache_basics.py | 110 ++++++++++++++++++ .../redis-sentinel-18ba4a0da83dabc7.yaml | 4 + 4 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/redis-sentinel-18ba4a0da83dabc7.yaml diff --git a/oslo_cache/_opts.py b/oslo_cache/_opts.py index 569a2401..0d4fd416 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'], @@ -136,10 +137,18 @@ FILE_OPTIONS = { cfg.StrOpt('redis_password', secret=True, help='the password for redis'), + cfg.ListOpt('redis_sentinels', + default=['localhost:26379'], + help='Redis sentinel servers in the format of ' + '"host:port"'), cfg.FloatOpt('redis_socket_timeout', default=1.0, help='Timeout in seconds for every call to a server.' - ' (dogpile.cache.redis backend only).'), + ' (dogpile.cache.redis and dogpile.cache.redis_sentinel ' + 'backends only).'), + cfg.StrOpt('redis_sentinel_service_name', + default='mymaster', + help='Service name of the redis sentinel cluster.'), cfg.BoolOpt('tls_enabled', default=False, help='Global toggle for TLS usage when communicating with' diff --git a/oslo_cache/core.py b/oslo_cache/core.py index b5a8ce07..8a93c046 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 urllib.parse @@ -101,6 +102,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 exception.ConfigurationError('Malformed sentinel server format') + + def _build_cache_config(conf): """Build the cache region dictionary configuration. @@ -161,6 +174,22 @@ def _build_cache_config(conf): for arg in ('socket_timeout',): value = getattr(conf.cache, 'redis_' + arg) conf_dict['%s.arguments.%s' % (prefix, arg)] = value + elif conf.cache.backend == 'dogpile.cache.redis_sentinel': + for arg in ('password', 'socket_timeout'): + value = getattr(conf.cache, 'redis_' + arg) + conf_dict['%s.arguments.%s' % (prefix, arg)] = value + if conf.cache.redis_username: + # TODO(tkajinam): Update dogpile.cache to add username argument, + # similarly to password. + conf_dict['%s.arguments.connection_kwargs' % prefix] = \ + {'username': conf.cache.redis_username} + conf_dict['%s.arguments.sentinel_kwargs' % prefix] = \ + {'username': conf.cache.redis_username} + conf_dict['%s.arguments.service_name' % prefix] = \ + conf.cache.redis_sentinel_service_name + if conf.cache.redis_sentinels: + conf_dict['%s.arguments.sentinels' % prefix] = [ + _parse_sentinel(s) for s in conf.cache.redis_sentinels] else: # NOTE(yorik-sar): these arguments will be used for memcache-related # backends. Use setdefault for url to support old-style setting through @@ -233,7 +262,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 " @@ -255,7 +285,18 @@ def _build_cache_config(conf): 'ssl_certfile': conf.cache.tls_certfile, 'ssl_keyfile': conf.cache.tls_keyfile }) - conf_dict['%s.arguments.connection_kwargs' % prefix] = conn_kwargs + if conf.cache.backend == 'dogpile.cache.redis_sentinel': + conn_kwargs.update({'ssl': True}) + conf_dict.setdefault( + '%s.arguments.connection_kwargs' % prefix, + {}).update(conn_kwargs) + conf_dict.setdefault( + '%s.arguments.sentinel_kwargs' % prefix, + {}).update(conn_kwargs) + else: + conf_dict.setdefault( + '%s.arguments.connection_kwargs' % prefix, + {}).update(conn_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 8791df38..aa51a071 100644 --- a/oslo_cache/tests/unit/test_cache_basics.py +++ b/oslo_cache/tests/unit/test_cache_basics.py @@ -313,6 +313,24 @@ class CacheRegionTest(test_cache.BaseTestCase): self.assertNotIn('test_prefix.arguments.connection_kwargs', config_dict) + def test_cache_dictionary_config_builder_tls_disabled_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_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.assertFalse(self.config_fixture.conf.cache.tls_enabled) + self.assertNotIn('test_prefix.arguments.connection_kwargs', + config_dict) + self.assertNotIn('test_prefix.arguments.sentinel_kwargs', + config_dict) + def test_cache_dictionary_config_builder_tls_enabled(self): """Validate the backend is reset to default if caching is disabled.""" self.config_fixture.config(group='cache', @@ -363,6 +381,42 @@ class CacheRegionTest(test_cache.BaseTestCase): 'ssl_certfile': 'path_to_cert_file' }, config_dict['test_prefix.arguments.connection_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.connection_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.connection_kwargs']) + self.assertIn('test_prefix.arguments.sentinel_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.sentinel_kwargs']) @mock.patch('oslo_cache.core._LOG') def test_cache_dictionary_config_builder_fips_mode_supported(self, log): @@ -729,6 +783,62 @@ class CacheRegionTest(test_cache.BaseTestCase): 'redis://user:secrete@[::1]:6379', config_dict['test_prefix.arguments.url']) + 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') + + config_dict = cache._build_cache_config(self.config_fixture.conf) + + self.assertFalse(self.config_fixture.conf.cache.tls_enabled) + self.assertEqual( + 'mymaster', config_dict['test_prefix.arguments.service_name']) + self.assertEqual([ + ('localhost', 26379) + ], config_dict['test_prefix.arguments.sentinels']) + self.assertEqual( + 1.0, config_dict['test_prefix.arguments.socket_timeout']) + self.assertNotIn('test_prefix.arguments.connection_kwargs', + config_dict) + self.assertNotIn('test_prefix.arguments.sentinel_kwargs', + config_dict) + + def test_cache_dictionary_config_builder_redis_sentinel_with_auth(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', + redis_username='user', + redis_password='secrete', + redis_sentinels=[ + '127.0.0.1:26379', + '[::1]:26379', + 'localhost:26379' + ], + redis_sentinel_service_name='cluster') + + config_dict = cache._build_cache_config(self.config_fixture.conf) + + self.assertFalse(self.config_fixture.conf.cache.tls_enabled) + self.assertEqual( + 'cluster', config_dict['test_prefix.arguments.service_name']) + self.assertEqual([ + ('127.0.0.1', 26379), + ('::1', 26379), + ('localhost', 26379), + ], config_dict['test_prefix.arguments.sentinels']) + self.assertEqual( + 'secrete', config_dict['test_prefix.arguments.password']) + self.assertEqual({ + 'username': 'user' + }, config_dict['test_prefix.arguments.connection_kwargs']) + self.assertEqual({ + 'username': 'user' + }, config_dict['test_prefix.arguments.sentinel_kwargs']) + 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.