Add new roles, debops.redis_{server,sentinel}

This patch adds two new Ansible roles, 'debops.redis_server' and
'debops.redis_sentinel'. They replace the existing 'debops.redis' role
which will be removed in the future.
parent d4d134bb
......@@ -1111,6 +1111,22 @@ stages:
JANE_DIFF_PATTERN: '.*/debops.redis/.*'
JANE_LOG_PATTERN: '\[debops\.redis\]'
'redis_server role':
<<: *test_role_2nd_deps
variables:
JANE_TEST_PLAY: '${DEBOPS_PLAYBOOKS}/service/redis_server.yml'
JANE_INVENTORY_GROUPS: 'debops_service_redis_server'
JANE_DIFF_PATTERN: '.*/debops.redis_server/.*'
JANE_LOG_PATTERN: '\[debops\.redis_server\]'
'redis_sentinel role':
<<: *test_role_2nd_deps
variables:
JANE_TEST_PLAY: '${DEBOPS_PLAYBOOKS}/service/redis_sentinel.yml'
JANE_INVENTORY_GROUPS: 'debops_service_redis_sentinel'
JANE_DIFF_PATTERN: '.*/debops.redis_sentinel/.*'
JANE_LOG_PATTERN: '\[debops\.redis_sentinel\]'
'reprepro role':
<<: *test_role_2nd_deps
variables:
......
......@@ -21,6 +21,12 @@ You can read information about required changes between releases in the
Added
~~~~~
- New DebOps roles:
- :ref:`debops.redis_server` and :ref:`debops.redis_sentinel` roles, that
replace the existing ``debops.redis`` Ansible role. The new roles support
multiple Redis and Sentinel instances on a single host.
- [debops.users] The role can now configure ACL entries of the user home
directories using the ``item.home_acl`` parameter. This can be used for more
elaborate access restrictions.
......
---
- name: Manage Redis Sentinel service
hosts: [ 'debops_service_redis_sentinel' ]
become: True
environment: '{{ inventory__environment | d({})
| combine(inventory__group_environment | d({}))
| combine(inventory__host_environment | d({})) }}'
roles:
- role: debops.redis_sentinel/env
tags: [ 'role::redis_sentinel', 'role::ferm' ]
- role: debops.apt_preferences
tags: [ 'role::apt_preferences' ]
apt_preferences__dependent_list:
- '{{ redis_sentinel__apt_preferences__dependent_list }}'
- role: debops.etc_services
tags: [ 'role::etc_services' ]
etc_services__dependent_list:
- '{{ redis_sentinel__etc_services__dependent_list }}'
- role: debops.ferm
tags: [ 'role::ferm', 'skip::ferm' ]
ferm__dependent_rules:
- '{{ redis_sentinel__ferm__dependent_rules }}'
- role: debops.python
tags: [ 'role::python', 'role::redis_sentinel' ]
python__dependent_packages3:
- '{{ redis_sentinel__python__dependent_packages3 }}'
python__dependent_packages2:
- '{{ redis_sentinel__python__dependent_packages2 }}'
- role: debops.redis_sentinel
tags: [ 'role::redis_sentinel' ]
---
- name: Manage Redis server
hosts: [ 'debops_service_redis_server' ]
become: True
environment: '{{ inventory__environment | d({})
| combine(inventory__group_environment | d({}))
| combine(inventory__host_environment | d({})) }}'
roles:
- role: debops.redis_server/env
tags: [ 'role::redis_server', 'role::ferm' ]
- role: debops.sysfs/env
tags: [ 'role::sysfs', 'role::secret' ]
- role: debops.secret
tags: [ 'role::secret', 'role::sysfs' ]
secret__directories:
- '{{ sysfs__secret__directories | d([]) }}'
- role: debops.apt_preferences
tags: [ 'role::apt_preferences' ]
apt_preferences__dependent_list:
- '{{ redis_server__apt_preferences__dependent_list }}'
- role: debops.etc_services
tags: [ 'role::etc_services' ]
etc_services__dependent_list:
- '{{ redis_server__etc_services__dependent_list }}'
- role: debops.ferm
tags: [ 'role::ferm', 'skip::ferm' ]
ferm__dependent_rules:
- '{{ redis_server__ferm__dependent_rules }}'
- role: debops.sysctl
tags: [ 'role::sysctl' ]
sysctl__dependent_parameters:
- '{{ redis_server__sysctl__dependent_parameters }}'
- role: debops.sysfs
tags: [ 'role::sysfs' ]
sysfs__dependent_attributes:
- '{{ redis_server__sysfs__dependent_attributes }}'
- role: debops.python
tags: [ 'role::python', 'role::redis_server' ]
python__dependent_packages3:
- '{{ redis_server__python__dependent_packages3 }}'
python__dependent_packages2:
- '{{ redis_server__python__dependent_packages2 }}'
- role: debops.redis_server
tags: [ 'role::redis_server' ]
......@@ -68,6 +68,10 @@
- import_playbook: redis.yml
- import_playbook: redis_server.yml
- import_playbook: redis_sentinel.yml
- import_playbook: reprepro.yml
- import_playbook: smstools.yml
......
../service/redis_sentinel.yml
\ No newline at end of file
../service/redis_server.yml
\ No newline at end of file
debops.redis_sentinel - Manage Redis Sentinel instances using Ansible
Copyright (C) 2018 Maciej Delmanowski <drybjed@gmail.com>
Copyright (C) 2018 DebOps https://debops.org/
This Ansible role is part of DebOps.
DebOps is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3, as
published by the Free Software Foundation.
DebOps is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DebOps. If not, see https://www.gnu.org/licenses/.
This diff is collapsed.
../defaults
\ No newline at end of file
---
- name: Prepare Redis Sentinel role environment
set_fact:
redis_sentinel__env_ports: '{{ lookup("template", "lookup/redis_sentinel__env_ports.j2") | from_yaml }}'
../templates
\ No newline at end of file
---
dependencies:
- role: 'debops.ansible_plugins'
- role: 'debops.secret'
galaxy_info:
role_name: 'redis_sentinel'
author: 'Maciej Delmanowski'
description: 'Configure Redis Sentinel instances'
company: 'DebOps'
license: 'GPL-3.0'
min_ansible_version: '2.4.0'
platforms:
- name: Ubuntu
versions:
- xenial
- bionic
- name: Debian
versions:
- stretch
- buster
galaxy_tags:
- redis
---
- name: Install Redis Sentinel packages
package:
name: '{{ item }}'
state: 'present'
with_flattened:
- '{{ redis_sentinel__base_packages }}'
- '{{ redis_sentinel__packages }}'
- name: Ensure that standalone Redis Sentinel is stopped on install
systemd:
name: 'redis-sentinel.service'
state: 'stopped'
when: ((ansible_local is undefined or
ansible_local.redis_sentinel is undefined) and
ansible_service_mgr == 'systemd')
- name: Make sure Ansible fact directory exists
file:
path: '/etc/ansible/facts.d'
state: 'directory'
owner: 'root'
group: 'root'
mode: '0755'
- name: Setup Redis Sentinel local facts
template:
src: 'etc/ansible/facts.d/redis_sentinel.fact.j2'
dest: '/etc/ansible/facts.d/redis_sentinel.fact'
owner: 'root'
group: 'root'
mode: '0755'
register: redis_sentinel__register_facts
- name: Reload facts if they were modified
action: setup
when: redis_sentinel__register_facts is changed
- name: Create Redis Sentinel auth UNIX group
group:
name: '{{ redis_sentinel__auth_group }}'
state: 'present'
system: True
- name: Create Redis Sentinel instance directories
file:
path: '/etc/redis/sentinel-{{ item.0.name + item.1 }}'
state: 'directory'
owner: 'root'
group: 'root'
mode: '0755'
loop: '{{ redis_sentinel__combined_configuration | parse_kv_items
| product([ "", "/reconfig.d", "/notify.d" ]) | list }}'
when: item.0.name|d() and item.0.state|d('present') not in [ 'absent', 'init', 'ignore' ]
no_log: '{{ redis_sentinel__no_log }}'
- name: Generate initial Redis Sentinel configuration
template:
src: 'etc/redis/sentinel-instance/sentinel.conf.j2'
dest: '/etc/redis/sentinel-{{ item.name }}/sentinel.conf'
owner: '{{ redis_sentinel__user }}'
group: '{{ redis_sentinel__auth_group }}'
mode: '0640'
force: False
with_items: '{{ redis_sentinel__combined_configuration | parse_kv_items }}'
when: item.name|d() and item.state|d('present') not in [ 'absent', 'init', 'ignore' ]
register: redis_sentinel__register_config_init
no_log: '{{ redis_sentinel__no_log }}'
- name: Generate Redis Sentinel notify and reconfig scripts
template:
src: 'etc/redis/sentinel-instance/{{ item.1 }}.j2'
dest: '/etc/redis/sentinel-{{ item.0.name }}/{{ item.1 }}'
owner: 'root'
group: 'root'
mode: '0755'
loop: '{{ redis_sentinel__combined_configuration | parse_kv_items
| product([ "notify.sh", "reconfig.sh" ]) | list }}'
when: item.0.name|d() and item.0.state|d('present') not in [ 'absent', 'init', 'ignore' ]
no_log: '{{ redis_sentinel__no_log }}'
- name: Install custom systemd unit files
template:
src: '{{ item }}.j2'
dest: '/{{ item }}'
owner: 'root'
group: 'root'
mode: '0644'
with_items:
- 'etc/systemd/system/redis-sentinel@.service'
- 'etc/systemd/system/redis-sentinel.service'
register: redis_sentinel__register_systemd
- name: Create systemd override directories for instances
file:
path: '/etc/systemd/system/redis-sentinel@{{ item.name }}.service.d'
state: 'directory'
owner: 'root'
group: 'root'
mode: '0755'
with_items: '{{ redis_sentinel__combined_configuration | parse_kv_items }}'
when: item.name|d() and item.state|d('present') not in [ 'absent', 'init', 'ignore' ] and
item.systemd_override|d()
no_log: '{{ redis_sentinel__no_log }}'
- name: Generate systemd instance override files
template:
src: 'etc/systemd/system/redis-sentinel@.service.d/ansible-override.conf.j2'
dest: '/etc/systemd/system/redis-sentinel@{{ item.name }}.service.d/ansible-override.conf'
owner: 'root'
group: 'root'
mode: '0644'
with_items: '{{ redis_sentinel__combined_configuration | parse_kv_items }}'
when: item.name|d() and item.state|d('present') not in [ 'absent', 'init', 'ignore' ] and
item.systemd_override|d()
register: redis_sentinel__register_systemd_override
no_log: '{{ redis_sentinel__no_log }}'
- name: Stop Redis Sentinel instances if requested
systemd:
name: 'redis-sentinel@{{ item.name }}.service'
state: 'stopped'
enabled: False
with_items: '{{ redis_sentinel__combined_configuration | parse_kv_items }}'
when: ansible_service_mgr == 'systemd' and item.name|d() and
item.state|d('present') == 'absent'
no_log: '{{ redis_sentinel__no_log }}'
- name: Remove Redis Sentinel instance systemd override if requested
file:
path: '/etc/systemd/system/redis-sentinel@{{ item.name }}.service.d'
state: 'absent'
with_items: '{{ redis_sentinel__combined_configuration | parse_kv_items }}'
register: redis_sentinel__register_systemd_remove
when: ansible_service_mgr == 'systemd' and item.name|d() and
item.state|d('present') == 'absent'
no_log: '{{ redis_sentinel__no_log }}'
- name: Remove Redis Sentinel instance configuration if requested
file:
path: '/etc/redis/sentinel-{{ item.name }}'
state: 'absent'
with_items: '{{ redis_sentinel__combined_configuration | parse_kv_items }}'
when: ansible_service_mgr == 'systemd' and item.name|d() and
item.state|d('present') == 'absent'
no_log: '{{ redis_sentinel__no_log }}'
- name: Reload systemd configuration when needed
systemd:
daemon_reload: True
when: ansible_service_mgr == 'systemd' and
redis_sentinel__register_systemd is changed or
redis_sentinel__register_systemd_remove is changed or
redis_sentinel__register_systemd_override is changed
- name: Ensure that Redis Sentinel instances are started
systemd:
name: 'redis-sentinel@{{ item.name }}.service'
state: 'started'
enabled: True
with_items: '{{ redis_sentinel__combined_configuration | parse_kv_items }}'
when: ansible_service_mgr == 'systemd' and item.name|d() and
item.state|d('present') not in [ 'absent', 'init', 'ignore' ]
no_log: '{{ redis_sentinel__no_log }}'
- name: Reload facts if they were modified
action: setup
when: redis_sentinel__register_config_init is changed
#!/usr/bin/python{{ '2' if (ansible_python_version is version_compare('3.5', '<')) else '3' }}
# {{ ansible_managed }}
from __future__ import print_function
from json import loads, dumps
from sys import exit
from operator import itemgetter
import subprocess
import signal
import os
import redis
output = {'installed': False}
def cmd_exists(cmd):
return any(
os.access(os.path.join(path, cmd), os.X_OK)
for path in os.environ["PATH"].split(os.pathsep)
)
def unwrap_quotes(input_line):
out = input_line.split(' ')[1].rstrip('\n')
if out.startswith('"') and out.endswith('"'):
out = out[1:-1]
return out
def get_immediate_subdirectories(a_dir):
return [name for name in os.listdir(a_dir)
if os.path.isdir(os.path.join(a_dir, name))]
output['installed'] = cmd_exists('redis-sentinel')
if output['installed']:
try:
redis_sentinel_version_stdout = subprocess.check_output(
["dpkg-query", "-W", "-f=${Version}\n'",
"redis-sentinel"]).split('-')[0]
output['version'] = redis_sentinel_version_stdout.split(':')[1]
except Exception:
pass
sentinel_instances = []
for directory in get_immediate_subdirectories('/etc/redis'):
if directory.startswith('sentinel-'):
instance = {'name': directory.split('-')[1]}
if os.path.isfile('/etc/redis/' + directory + '/sentinel.conf'):
try:
for config_file in ['/etc/redis/' + directory
+ '/sentinel.conf']:
try:
pass_map = {}
with open(config_file) as fh:
for line in fh:
if line.startswith('bind'):
instance['bind'] = unwrap_quotes(line)
if line.startswith('port'):
instance['port'] = unwrap_quotes(line)
elif line.startswith('unixsocket'):
instance['socket'] = (
unwrap_quotes(line))
elif line.startswith('sentinel auth-pass'):
monitor = line.split(' ')[2]
pass_map[monitor] = (
line.split(' ')[3].rstrip('\n'))
try:
r = redis.StrictRedis(
host=instance['bind'],
port=instance['port'])
instance['redis_mode'] = (
str(r.info()['redis_mode']))
monitors = []
for monitor in r.sentinel_masters().keys():
master_ip_port = (
r.sentinel_get_master_addr_by_name(
monitor))
sentinels = []
for entry in r.sentinel_sentinels(monitor):
sentinels.append(
{'ip': entry['ip'],
'port': entry['port']})
monitor_entry = (
{'name': monitor,
'master_host': master_ip_port[0],
'master_port': master_ip_port[1],
'other_sentinels': sentinels})
if monitor in pass_map.keys():
monitor_entry['master_password'] = (
pass_map[monitor])
monitors.append(monitor_entry)
instance['monitors'] = monitors
except Exception:
pass
except Exception:
pass
sentinel_instances.append(instance)
if instance['name'] == 'main':
output['bind'] = instance['bind']
output['port'] = instance['port']
output['socket'] = instance['socket']
output['monitor_name'] = (
instance['monitors'][0]['name'])
output['master_host'] = (
instance['monitors'][0]['master_host'])
output['master_port'] = (
instance['monitors'][0]['master_port'])
if 'master_password' in instance['monitors'][0].keys():
output['master_password'] = (
instance['monitors'][0]['master_password'])
output['other_sentinels'] = (
instance['monitors'][0]['other_sentinels'])
except Exception:
pass
try:
output['instances'] = sorted(sentinel_instances,
key=itemgetter('port'))
except Exception:
pass
print(dumps(output, sort_keys=True, indent=2))
#!/bin/bash
# {{ ansible_managed }}
# Run scripts in /etc/redis/sentinel-{{ item.0.name }}/notify.d when Redis Sentinel notices
# configuration change.
set -o nounset -o pipefail -o errexit
event_type="${1}"
event_desc="${2}"
notify_dir="/etc/redis/sentinel-{{ item.0.name }}/notify.d"
if [ -d "${notify_dir}" ] ; then
run-parts \
--arg="${event_type}" \
--arg="${event_desc}" \
"${notify_dir}"
fi
#!/bin/bash
# {{ ansible_managed }}
# Run scripts in /etc/redis/sentinel-{{ item.0.name }}/reconfig.d when Redis Sentinel notices
# configuration change.
set -o nounset -o pipefail -o errexit
master_name="${1}"
role="${2}"
state="${3}"
from_ip="${4}"
from_port="${5}"
to_ip="${6}"
to_port="${7}"
reconfig_dir="/etc/redis/sentinel-{{ item.0.name }}/reconfig.d"
if [ -d "${reconfig_dir}" ] ; then
run-parts \
--arg="${master_name}" \
--arg="${role}" \
--arg="${state}" \
--arg="${from_ip}" \
--arg="${from_port}" \
--arg="${to_ip}" \
--arg="${to_port}" \
"${reconfig_dir}"
fi
# Redis Sentinel configuration initialized by Ansible
{% if item.options|d() %}
{% for element in item.options %}
{% set element_prefix = element.prefix | d('sentinel ') %}
{% if element.name in [ 'daemonize', 'pidfile', 'logfile', 'bind', 'port',
'dir', 'syslog-facility', 'syslog-ident',
'syslog-enabled', 'loglevel', 'unixsocket' ] %}
{% set element_prefix = element.prefix | d('') %}
{% endif %}
{% if element.state|d('present') not in [ 'absent', 'ignore', 'init' ] %}
{% if (element.separator|d())|bool %}
{% endif %}
{% if element.value is string and not element.value | bool %}
{{ '{}{} {}'.format(element_prefix, element.name, element.value) }}
{% elif element.value | bool and element.value is not iterable %}
{% if element.value | string == '1' %}
{{ '{}{} {}'.format(element_prefix, element.name, element.value) }}
{% else %}
{{ '{}{} {}'.format(element_prefix, element.name, 'yes') }}
{% endif %}
{% elif not element.value | bool and element.value is not iterable %}
{% if element.value is not none %}
{% if element.value | int %}
{{ '{}{} {}'.format(element_prefix, element.name, element.value) }}
{% else %}
{% if element.value | string == '0' %}
{{ '{}{} {}'.format(element_prefix, element.name, element.value) }}
{% else %}
{{ '{}{} {}'.format(element_prefix, element.name, 'no') }}
{% endif %}
{% endif %}
{% endif %}
{% elif element.value is iterable and element.value is not string and element.value is not mapping %}
{% if (element.multiple|d(True))|bool %}
{% for thing in (element.value | map(attribute='name') | list) %}
{{ '{}{} {}'.format(element_prefix, element.name, thing) }}
{% endfor %}
{% else %}
{{ '{}{} {}'.format(element_prefix, element.name, element.value | map(attribute='name') | list | join(' ')) }}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
# {{ ansible_managed }}
[Unit]
Description=Advanced key-value store - Sentinel
After=network.target
Documentation=http://redis.io/documentation, man:redis-sentinel(1)
[Service]
Type=oneshot
ExecStart=/bin/true
ExecReload=/bin/true
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Alias=sentinel.service
# {{ ansible_managed }}
{{ item.systemd_override | regex_replace('$\n', '') }}
# {{ ansible_managed }}
[Unit]
Description=Redis - advanced key-value store (%I) - Sentinel
After=network.target
Documentation=http://redis.io/documentation, man:redis-sentinel(1)
ConditionPathExists=/etc/redis/sentinel-%I/sentinel.conf
PartOf=redis-sentinel.service
ReloadPropagatedFrom=redis-sentinel.service
Before=redis-sentinel.service
After=redis-server.service
[Service]
Type=forking
Slice=system-redis-sentinel.slice
ExecStart=/usr/bin/redis-sentinel /etc/redis/sentinel-%i/sentinel.conf
ExecStop=/bin/kill -s TERM $MAINPID
PIDFile=/var/run/sentinel-%i/redis-sentinel.pid
TimeoutStopSec=0
Restart=always
User=redis
Group=redis
RuntimeDirectory=sentinel-%i
RuntimeDirectoryMode=2755
UMask=007
PrivateTmp=yes
LimitNOFILE=65535
PrivateDevices=yes
ProtectHome=yes
ReadOnlyDirectories=/
ReadWriteDirectories=-/var/lib/redis
ReadWriteDirectories=-/var/log/redis
ReadWriteDirectories=-/var/run/sentinel-%i
NoNewPrivileges=true