1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

feat[redis cache plugin]: Add Redis connection parameters full control support

This commit is contained in:
pierre.debris 2024-07-03 17:52:40 +02:00
parent c517f1c483
commit 00b3468fd9
No known key found for this signature in database
GPG key ID: E9863637B6C8E8D3
2 changed files with 233 additions and 40 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- redis cache plugin - add Redis connection parameters full access in order to be able to set advanced socket options, like enabling keep alive (https://github.com/ansible-collections/community.general/pull/8578)

271
plugins/cache/redis.py vendored
View file

@ -15,6 +15,165 @@ DOCUMENTATION = '''
requirements: requirements:
- redis>=2.4.5 (python lib) - redis>=2.4.5 (python lib)
options: options:
_decode_responses:
description: If set to `true`, returned values from Redis commands get decoded automatically using the client's charset value.
type: bool
default: false
env:
- name: ANSIBLE_CACHE_REDIS_DECODE_RESPONSES
ini:
- key: fact_caching_redis_decode_responses
section: defaults
_encoding:
description: Set the charset to use for facts encoding.
type: string
default: utf-8
env:
- name: ANSIBLE_CACHE_REDIS_ENCODING
ini:
- key: fact_caching_redis_encoding
section: defaults
_encoding_errors:
description:
- The error handling scheme to use for encoding errors.
- The default is `strict` meaning that encoding errors raise a `UnicodeEncodeError`.
- See https://docs.python.org/fr/3/library/stdtypes.html#str.encode for more details.
type: string
default: strict
choices: [ backslashreplace, ignore, replace, strict, xmlcharrefreplace ]
env:
- name: ANSIBLE_CACHE_REDIS_ENCODING_ERRORS
ini:
- key: fact_caching_redis_encoding_errors
section: defaults
_keyset_name:
description: User defined name for cache keyset name.
type: string
default: ansible_cache_keys
env:
- name: ANSIBLE_CACHE_REDIS_KEYSET_NAME
ini:
- key: fact_caching_redis_keyset_name
section: defaults
version_added: 1.3.0
_prefix:
description: User defined prefix to use when creating the DB entries
type: string
default: ansible_facts
env:
- name: ANSIBLE_CACHE_PLUGIN_PREFIX
ini:
- key: fact_caching_prefix
section: defaults
_retry_on_timeout:
description:
- Controls how socket.timeout errors are handled.
- When set to `false` a TimeoutError will be raised anytime a socket.timeout is encountered.
- When set to `true`, it enable retries like other `socket.error`s.
type: bool
default: false
env:
- name: ANSIBLE_CACHE_REDIS_RETRY_ON_TIMEOUT
ini:
- key: fact_caching_redis_retry_on_timeout
section: defaults
_sentinel_service_name:
description: The redis sentinel service name (or referenced as cluster name).
type: string
env:
- name: ANSIBLE_CACHE_REDIS_SENTINEL
ini:
- key: fact_caching_redis_sentinel
section: defaults
version_added: 1.3.0
_socket_connect_timeout:
description:
- Timeout value, in seconds, for Redis socket connection.
- If not set, connection timeout is disabled.
type: integer
env:
- name: ANSIBLE_CACHE_REDIS_SOCKET_CONNECT_TIMEOUT
ini:
- key: fact_caching_redis_socket_connect_timeout
section: defaults
_socket_keepalive:
description:
- Specifies whether to enable keepalive for Redis socket connection.
type: bool
default: false
env:
- name: ANSIBLE_CACHE_REDIS_SOCKET_KEEPALIVE
ini:
- key: fact_caching_redis_socket_keepalive
section: defaults
_socket_keepalive_options:
description:
- Finer grain control keepalive options when `_socket_keepalive` is set to `true`.
- A comma separated socket options string of <key>:<value> pairs, for example V(TCP_KEEPIDLE:600,TCP_KEEPCNT=10,TCP_KEEPINTVL:300).
- Accepted keys are `TCP_KEEPIDLE`, `TCP_KEEPCNT`, and `TCP_KEEPINTVL`.
- Integers are expected for values.
type: string
env:
- name: ANSIBLE_CACHE_REDIS_SOCKET_KEEPALIVE_OPTIONS
ini:
- key: fact_caching_redis_socket_keepalive_options
section: defaults
_socket_timeout:
description:
- Timeout value, in seconds, for Redis socket connection.
- If not set, timeout is disabled.
type: integer
env:
- name: ANSIBLE_CACHE_REDIS_SOCKET_TIMEOUT
ini:
- key: fact_caching_redis_socket_timeout
section: defaults
_ssl_ca_certs_file:
description: When using SSL on Redis connection, specifies the SSL CA file path.
type: string
env:
- name: ANSIBLE_CACHE_REDIS_SSL_CA_CERTS_FILE
ini:
- key: fact_caching_redis_ssl_ca_certs_file
section: defaults
_ssl_cert_file:
description: When using SSL on Redis connection, specifies the SSL certificate file path.
type: string
env:
- name: ANSIBLE_CACHE_REDIS_SSL_CERT_FILE
ini:
- key: fact_caching_redis_ssl_cert_file
section: defaults
_ssl_cert_reqs:
default: none
type: string
description:
- When using SSL on Redis connection, specifies the security mode to use.
- See https://docs.python.org/3/library/ssl.html#ssl.SSLContext.verify_mode for more details.
env:
- name: ANSIBLE_CACHE_REDIS_SSL_CERT_REQ
ini:
- key: fact_caching_redis_ssl_cert_req
section: defaults
choices: [ none, optionnal, required ]
_ssl_key_file:
description: When using SSL on Redis connection, specifies the SSL key file path.
type: string
env:
- name: ANSIBLE_CACHE_REDIS_SSL_KEY_FILE
ini:
- key: fact_caching_redis_ssl_key_file
section: defaults
_timeout:
default: 86400
description: Expiration timeout in seconds for the cache plugin data. Set to 0 to never expire
type: integer
# TODO: determine whether it is OK to change to: type: float
env:
- name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
ini:
- key: fact_caching_timeout
section: defaults
_uri: _uri:
description: description:
- A colon separated string of connection information for Redis. - A colon separated string of connection information for Redis.
@ -28,44 +187,6 @@ DOCUMENTATION = '''
ini: ini:
- key: fact_caching_connection - key: fact_caching_connection
section: defaults section: defaults
_prefix:
description: User defined prefix to use when creating the DB entries
type: string
default: ansible_facts
env:
- name: ANSIBLE_CACHE_PLUGIN_PREFIX
ini:
- key: fact_caching_prefix
section: defaults
_keyset_name:
description: User defined name for cache keyset name.
type: string
default: ansible_cache_keys
env:
- name: ANSIBLE_CACHE_REDIS_KEYSET_NAME
ini:
- key: fact_caching_redis_keyset_name
section: defaults
version_added: 1.3.0
_sentinel_service_name:
description: The redis sentinel service name (or referenced as cluster name).
type: string
env:
- name: ANSIBLE_CACHE_REDIS_SENTINEL
ini:
- key: fact_caching_redis_sentinel
section: defaults
version_added: 1.3.0
_timeout:
default: 86400
type: integer
# TODO: determine whether it is OK to change to: type: float
description: Expiration timeout in seconds for the cache plugin data. Set to 0 to never expire
env:
- name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
ini:
- key: fact_caching_timeout
section: defaults
''' '''
import re import re
@ -97,8 +218,11 @@ class CacheModule(BaseCacheModule):
performance. performance.
""" """
_sentinel_service_name = None _sentinel_service_name = None
_encoding_errors_choices = ['backslashreplace', 'ignore', 'replace', 'strict', 'xmlcharrefreplace']
_socket_keepalive_available_opts = ['TCP_KEEPIDLE', 'TCP_KEEPCNT', 'TCP_KEEPINTVL']
re_url_conn = re.compile(r'^([^:]+|\[[^]]+\]):(\d+):(\d+)(?::(.*))?$') re_url_conn = re.compile(r'^([^:]+|\[[^]]+\]):(\d+):(\d+)(?::(.*))?$')
re_sent_conn = re.compile(r'^(.*):(\d+)$') re_sent_conn = re.compile(r'^(.*):(\d+)$')
re_socket_keepalive_opts = re.compile(r'^(\w+:\d+)(?:,(\w+:\d+))+$')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
uri = '' uri = ''
@ -110,17 +234,71 @@ class CacheModule(BaseCacheModule):
self._prefix = self.get_option('_prefix') self._prefix = self.get_option('_prefix')
self._keys_set = self.get_option('_keyset_name') self._keys_set = self.get_option('_keyset_name')
self._sentinel_service_name = self.get_option('_sentinel_service_name') self._sentinel_service_name = self.get_option('_sentinel_service_name')
self._decode_responses = bool(self.get_option('_decode_responses'))
self._encoding = self.get_option('_encoding')
self._encoding_errors = self.get_option('_encoding_errors')
self._retry_on_timeout = bool(self.get_option('_retry_on_timeout'))
self._socket_keepalive = self.get_option('_socket_keepalive')
self._socket_connect_timeout = int(self.get_option('_socket_connect_timeout')) \
if self._socket_keepalive and self.get_option('_socket_connect_timeout') else None
self._socket_keepalive_options = self._parse_socket_options(self.get_option('_socket_keepalive_options')) \
if self._socket_keepalive and self.get_option('_socket_keepalive_options') else None
self._socket_timeout = int(self.get_option('_socket_timeout')) \
if self._socket_keepalive and self.get_option('_socket_timeout') else None
self._ssl_ca_certs_file = self.get_option('_ssl_ca_certs_file')
self._ssl_cert_file = self.get_option('_ssl_cert_file')
self._ssl_cert_reqs = self.get_option('_ssl_cert_reqs')
self._ssl_key_file = self.get_option('_ssl_key_file')
if not HAS_REDIS: if not HAS_REDIS:
raise AnsibleError("The 'redis' python module (version 2.4.5 or newer) is required for the redis fact cache, 'pip install redis'") raise AnsibleError("The 'redis' python module (version 2.4.5 or newer) is required for the redis fact cache, 'pip install redis'")
if self._encoding_errors not in self._encoding_errors_choices:
raise AnsibleError("Unsupported value '%s' for Redis cache plugin parameter '_encoding_errors'" % (self._encoding_errors))
self._cache = {} self._cache = {}
kw = {} kw = {
'decode_responses': self._decode_responses,
'encoding_errors': self._encoding_errors,
'encoding': self._encoding,
'retry_on_timeout': self._retry_on_timeout,
'socket_connect_timeout': self._socket_connect_timeout,
'socket_keepalive_options': self._socket_keepalive_options,
'socket_keepalive': self._socket_keepalive,
'socket_timeout': self._socket_timeout,
}
# tls connection # tls connection
tlsprefix = 'tls://' tlsprefix = 'tls://'
if uri.startswith(tlsprefix): if uri.startswith(tlsprefix):
kw['ssl'] = True from os import access, R_OK
from os.path import isfile
import ssl
if not self._ssl_cert_reqs.upper() in list(map(lambda x: x.name.split('_')[1], ssl.VerifyMode)):
raise AnsibleError("Unsupported value '%s' for Redis cache plugin parameter '_ssl_cert_reqs'" % (self._ssl_cert_reqs))
if self._ssl_ca_certs_file:
if not isfile(self._ssl_ca_certs_file) and not access(self._ssl_ca_certs_file, R_OK):
raise AnsibleError("File %s doesn't exist or isn't readable for Redis cache plugin parameter '_ssl_ca_certs_file'" %
self._ssl_ca_certs_file)
if self._ssl_cert_file:
if not isfile(self._ssl_cert_file) and not access(self._ssl_cert_file, R_OK):
raise AnsibleError("File %s doesn't exist or isn't readable for Redis cache plugin parameter '_ssl_cert_file'" % self._ssl_cert_file)
if self._ssl_key_file:
if not isfile(self._ssl_key_file) and not access(self._ssl_key_file, R_OK):
raise AnsibleError("File %s doesn't exist or isn't readable for Redis cache plugin parameter '_ssl_key_file'" % self._ssl_key_file)
kw.update({
'ssl': True,
'ssl_keyfile': self._ssl_key_file,
'ssl_certfile': self._ssl_cert_file,
'ssl_cert_reqs': self._ssl_cert_reqs,
'ssl_ca_certs': self._ssl_ca_certs_file
})
uri = uri[len(tlsprefix):] uri = uri[len(tlsprefix):]
# redis sentinel connection # redis sentinel connection
@ -132,6 +310,7 @@ class CacheModule(BaseCacheModule):
self._db = StrictRedis(*connection, **kw) self._db = StrictRedis(*connection, **kw)
display.vv('Redis connection: %s' % self._db) display.vv('Redis connection: %s' % self._db)
display.vvv("Redis connection kwargs: %s" % ({**self._db.get_connection_kwargs(), **{'password': '****'}}))
@staticmethod @staticmethod
def _parse_connection(re_patt, uri): def _parse_connection(re_patt, uri):
@ -140,6 +319,18 @@ class CacheModule(BaseCacheModule):
raise AnsibleError("Unable to parse connection string") raise AnsibleError("Unable to parse connection string")
return match.groups() return match.groups()
def _parse_socket_options(self, options):
if not self.re_socket_keepalive_opts.match(options):
raise AnsibleError("Unable to parse Redis cache socket keepalive options string")
import socket
opts = {}
for opt in options.split(','):
key, value = opt.split(':')
if key not in self._socket_keepalive_available_opts:
raise AnsibleError("Option '%s' is not available for parameter '_socket_keepalive_options' for Redis cache plugin" % (key))
opts[getattr(socket, key)] = int(value)
return opts
def _get_sentinel_connection(self, uri, kw): def _get_sentinel_connection(self, uri, kw):
""" """
get sentinel connection details from _uri get sentinel connection details from _uri