Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

**Highlights:** On RHEL 8, a MariaDB package upgrade no longer cuts applications on the same host off from their database. Apache no longer loads `mod_info`, which served the complete configuration including other modules' credentials. A broken PHP-FPM configuration aborts the run instead of taking the service down on the restart. Sudo rules deployed by `freeipa_server` can carry their commands again. The Bitwarden lookup can be told to abort instead of silently generating a new password, for runs against hosts whose credentials must already exist. The Grafana graph configuration for the Monitoring Plugins is no longer deployed on every ordinary run and has to be requested explicitly by its tag.
**Highlights:** Valkey gets its own role and playbook instead of being installed under the Redis name, which is what RHEL 10 hosts need now that Red Hat ships no Redis at all; those hosts have to be moved over. On RHEL 8, a MariaDB package upgrade no longer cuts applications on the same host off from their database. Apache no longer loads `mod_info`, which served the complete configuration including other modules' credentials. A broken PHP-FPM configuration aborts the run instead of taking the service down on the restart. Sudo rules deployed by `freeipa_server` can carry their commands again. The Bitwarden lookup can be told to abort instead of silently generating a new password, for runs against hosts whose credentials must already exist. The Grafana graph configuration for the Monitoring Plugins is no longer deployed on every ordinary run and has to be requested explicitly by its tag.

### Breaking Changes

* **role:collabora**: `collabora__coolwsd_ssl_settings_ssl_verification` and `collabora__coolwsd_welcome_enable` reach the deployed `coolwsd.xml` again, and SSL verification of the WOPI host now defaults to strict. Both variables were only wired into a few old templates, so on newer versions the value came from the package instead of from the inventory, and most packages ship verification off. A host whose WOPI host presents a self-signed or otherwise untrusted certificate needs `collabora__coolwsd_ssl_settings_ssl_verification: false`, otherwise its documents stop loading.
* **role:apache_httpd**: `mod_info` is no longer enabled, so `/server-info` stops serving the complete Apache configuration, including the credentials of other modules. Hosts that need the endpoint re-enable the `info` module in their inventory via `apache_httpd__mods__group_var` / `apache_httpd__mods__host_var`.
* **role:monitoring_plugins**: A source install now places the notification plugins in `/usr/lib64/nagios/plugins`, next to the check plugins, and removes the `/usr/lib64/nagios/plugins/notifications` directory it used before. This is where the shipped Icinga command definitions and the rpm/deb packages have always expected them. Adjust any command definition of your own that points into the `notifications` subdirectory.
* **role:redis**: RHEL 10 is no longer supported, since Red Hat ships no Redis there. On RHEL 10 the role used to install Valkey under the Redis name, which aborted the run when the configuration file was deployed. Move those hosts to the `valkey` role: put them into the `lfops_valkey` inventory group and rename their `redis__*` variables to `valkey__*`. All other platforms are unaffected.
* **role:collabora**: Drop support for EOL Collabora 23.05. Upgrade to 24.04 or newer.
* **role:icingaweb2_module_grafana**: The graph configuration for the Linuxfabrik Monitoring Plugins is only deployed when the role is called with `--tags icingaweb2_module_grafana:monitoring_plugins_graphs`, matching the `icingaweb2_module_director:basket` tag. Run the role with that tag to update `/etc/icingaweb2/modules/grafana/graphs.ini`. The `icingaweb2_module_grafana__skip_monitoring_plugins_graphs_config` variable is gone; remove it from your inventory.

### Added

* **role:valkey**: Add a role and playbook to install and configure Valkey, listening on TCP 127.0.0.1:6379 by default. Valkey is taken from the distribution repositories (EPEL on RHEL 8, AppStream on RHEL 9 and 10, the distribution's own repository on Debian 13 and Ubuntu 24.04 / 26.04), and the configuration template matching the installed version is deployed. Debian 12 and Ubuntu 22.04 ship no Valkey and are not supported.
* **playbook:setup_icinga2_master, playbook:setup_mastodon, playbook:setup_moodle, playbook:setup_nextcloud**: These stacks install Valkey on RHEL 10 and Redis everywhere else, so they can be deployed on RHEL 10 again. Both servers speak the same protocol on the same port, so the applications are configured identically either way. Override `setup_*__skip_redis` / `setup_*__skip_valkey` (`setup_icinga2_master__redis__skip_role` / `setup_icinga2_master__valkey__skip_role`) to force a specific server or to run neither.
* **role:files**: A file can opt out of the backup copy that is written before it is overwritten, via the `backup` subkey of `files__files__*_var`.
* **role:collabora**: The `collabora:configure` tag deploys `coolwsd.xml` and the logrotate configuration without touching the packages.
* **role:docker**: The address pools docker assigns container network subnets from (`default-address-pools`) can be configured.
Expand Down
3 changes: 2 additions & 1 deletion COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Which Ansible role is proven to run on which OS?
| python | x | x | x | x | x | (x) | (x) | (x) | Windows |
| python_venv | x | x | x | x | x | (x) | (x) | (x) | Fedora 35 |
| qemu_guest_agent | (x) | (x) | x | x | x | (x) | (x) | (x) | |
| redis | x | x | x | x | (x) | (x) | (x) | (x) | |
| redis | x | x | x | x | | (x) | (x) | (x) | RHEL 10: no Redis, use the `valkey` role |
| repo_baseos | | | x | x | x | | | | |
| repo_collabora | | | x | (x) | (x) | | | | |
| repo_collabora_code | | | x | x | (x) | | | | |
Expand Down Expand Up @@ -166,6 +166,7 @@ Which Ansible role is proven to run on which OS?
| trend_micro_v1es | | | x | x | x | | x | | |
| unattended_upgrades | (x) | (x) | | | | (x) | (x) | (x) | |
| uptimerobot | | | | | | | | | controller-side, talks to UptimeRobot API |
| valkey | | x | x | x | x | | x | x | no Valkey in Deb 12 / Ubu 22.04 |
| vsftpd | | | x | (x) | (x) | | | | |
| wordpress | | | x | (x) | (x) | | | | |
| yum_utils | | | x | x | x | | | | Fedora 35 |
Expand Down
2 changes: 2 additions & 0 deletions extensions/molecule/valkey/converge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- name: 'Converge valkey playbook'
ansible.builtin.import_playbook: 'linuxfabrik.lfops.valkey'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Variables the playbook under test needs, applied to every system under test.
#
# Every value here deliberately differs from the role default, so verify.yml can
# tell "the running server uses what the inventory asked for" apart from "the
# running server happens to use the packaged default".
valkey__conf_databases: 32
valkey__conf_loglevel: 'verbose'
valkey__conf_maxmemory: '128M'
valkey__conf_maxmemory_policy: 'allkeys-lru'
valkey__conf_requirepass: 'linuxfabrik'

valkey__service_limit_nofile: 20480
23 changes: 23 additions & 0 deletions extensions/molecule/valkey/inventory/hosts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# yamllint disable rule:empty-values

# valkey targets 'lfops_valkey' (see playbooks/valkey.yml: hosts).
#
# Debian 12 and Ubuntu 22.04 are deliberately absent: neither ships Valkey in
# any repository, so the role does not support them (see COMPATIBILITY.md).
#
# The remaining hosts each pull a different Valkey version from their
# distribution repository (RHEL 8/9/10: 8.0, Debian 13: 8.1, Ubuntu 24.04: 7.2,
# Ubuntu 26.04: 9.0), so this host list also covers every configuration
# template the role ships.
lfops_valkey:
children:
systems_under_test:

systems_under_test:
hosts:
debian13-vm:
rocky8-vm:
rocky9-vm:
rocky10-vm:
ubuntu2404-vm:
ubuntu2604-vm:
1 change: 1 addition & 0 deletions extensions/molecule/valkey/molecule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Molecule scenario marker
133 changes: 133 additions & 0 deletions extensions/molecule/valkey/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# verify.yml runs after converge and again after the idempotence step. It asserts the observable
# end state, not that individual tasks ran.
#
# The centre of gravity is the last block: every setting is read back from the *running* server via
# `valkey-cli CONFIG GET`, so the whole chain is proven at once - group_vars, to the per-version
# template, to the file the service reads, to the value the server actually applies. Grepping
# valkey.conf would prove none of that.
- name: 'Verify valkey is running and uses the configured values'
hosts: 'systems_under_test'
gather_facts: true
become: true

vars:

# The RedHat packages call the unit valkey.service, the Debian/Ubuntu ones valkey-server.service.
__molecule__valkey_service_name: '{{
(ansible_facts["os_family"] == "RedHat") | ternary("valkey.service", "valkey-server.service")
}}'
# `-a` on the command line prints a warning to stderr on every call; --no-auth-warning silences
# it so a non-empty stderr stays meaningful.
__molecule__valkey_cli: 'valkey-cli -a {{ valkey__conf_requirepass | quote }} --no-auth-warning'

tasks:

- name: 'Get the service facts'
ansible.builtin.service_facts: # yamllint disable-line rule:empty-values

- name: 'Assert the valkey service is enabled and running'
ansible.builtin.assert:
that:
- 'ansible_facts["services"][__molecule__valkey_service_name]["state"] == "running"'
- 'ansible_facts["services"][__molecule__valkey_service_name]["status"] == "enabled"'
fail_msg: '{{ __molecule__valkey_service_name }} is {{ ansible_facts["services"][__molecule__valkey_service_name] | d("absent") }}'

- name: 'Assert valkey is reachable on its port'
ansible.builtin.wait_for:
host: '127.0.0.1'
port: '{{ valkey__conf_port | d(6379) }}'
timeout: 30

# The regression this role was split out for: on RHEL 10 the config file was deployed with a
# hardcoded `redis` group while the package creates a `valkey` one, so the run died with
# "chgrp failed: failed to look up group redis". Assert the ownership the package expects.
- name: 'stat /etc/valkey/valkey.conf'
ansible.builtin.stat:
path: '/etc/valkey/valkey.conf'
register: '__molecule__valkey_conf_result'

- name: 'Assert the config file is owned by root:valkey and not world-readable'
ansible.builtin.assert:
that:
- '__molecule__valkey_conf_result["stat"]["exists"]'
- '__molecule__valkey_conf_result["stat"]["pw_name"] == "root"'
- '__molecule__valkey_conf_result["stat"]["gr_name"] == "valkey"'
- '__molecule__valkey_conf_result["stat"]["mode"] == "0640"'
fail_msg: '/etc/valkey/valkey.conf is {{ __molecule__valkey_conf_result["stat"] | d("missing") }}'

# The systemd drop-in is only proven by what the unit ends up with, not by the file on disk.
- name: 'systemctl show {{ __molecule__valkey_service_name }} --property=LimitNOFILE' # noqa command-instead-of-module (read-only state query)
ansible.builtin.command: 'systemctl show {{ __molecule__valkey_service_name }} --property=LimitNOFILE --value'
register: '__molecule__valkey_limit_nofile_result'
changed_when: false

- name: 'Assert the drop-in raised LimitNOFILE'
ansible.builtin.assert:
that: '__molecule__valkey_limit_nofile_result["stdout"] | int == valkey__service_limit_nofile'
fail_msg: 'LimitNOFILE is {{ __molecule__valkey_limit_nofile_result["stdout"] }}, expected {{ valkey__service_limit_nofile }}'

# requirepass is proven twice over: an unauthenticated call has to fail, and the authenticated
# calls below have to succeed.
- name: 'valkey-cli PING without authentication'
ansible.builtin.command: 'valkey-cli PING'
register: '__molecule__valkey_noauth_result'
changed_when: false
failed_when: false

- name: 'Assert an unauthenticated client is rejected'
ansible.builtin.assert:
that: '"NOAUTH" in __molecule__valkey_noauth_result["stdout"] ~ __molecule__valkey_noauth_result["stderr"]'
fail_msg: 'valkey answered an unauthenticated PING: {{ __molecule__valkey_noauth_result["stdout"] }}'

# Ask the running server what it actually applied. maxmemory comes back in bytes, so the
# expected values are normalised here rather than in the assertion. Mind the unit convention:
# in valkey.conf "M" is 1000*1000 and only "MB" is 1024*1024, so 128M is 128000000 bytes.
# Verified against valkey 8.0.9 on Rocky 10.
- name: 'valkey-cli CONFIG GET'
ansible.builtin.command: '{{ __molecule__valkey_cli }} CONFIG GET {{ item["directive"] }}'
register: '__molecule__valkey_config_get_result'
changed_when: false
loop:
- directive: 'databases'
expected: '{{ valkey__conf_databases | string }}'
- directive: 'loglevel'
expected: '{{ valkey__conf_loglevel }}'
- directive: 'maxmemory'
expected: '{{ (valkey__conf_maxmemory | regex_replace("[^0-9]", "") | int) * 1000 * 1000 }}'
- directive: 'maxmemory-policy'
expected: '{{ valkey__conf_maxmemory_policy }}'
loop_control:
label: '{{ item["directive"] }}'

# CONFIG GET returns the name and the value on two lines; the value is the last one.
- name: 'Assert the running server applied the configured values'
ansible.builtin.assert:
that: 'item["stdout_lines"][-1] == item["item"]["expected"]'
fail_msg: '{{ item["item"]["directive"] }} is {{ item["stdout_lines"][-1] }}, expected {{ item["item"]["expected"] }}'
loop: '{{ __molecule__valkey_config_get_result["results"] }}'
loop_control:
label: '{{ item["item"]["directive"] }}'

# Proves the role picked the template matching the installed version, which is the one piece of
# per-OS logic the role does on its own.
- name: 'Get the list of installed packages'
ansible.builtin.package_facts: # yamllint disable-line rule:empty-values

- name: 'Read the header of the deployed /etc/valkey/valkey.conf'
ansible.builtin.slurp:
src: '/etc/valkey/valkey.conf'
register: '__molecule__valkey_conf_slurp_result'

- name: 'Assert the deployed template matches the installed Valkey version'
ansible.builtin.assert:
that: '__molecule__valkey_conf_header is search("Valkey v" ~ (__molecule__valkey_installed_version | regex_escape) ~ " configuration file")'
fail_msg: 'Valkey {{ __molecule__valkey_installed_version }} is installed, but the deployed config header reads: {{ __molecule__valkey_conf_header }}'
vars:
__molecule__valkey_package: '{{ (ansible_facts["os_family"] == "RedHat") | ternary("valkey", "valkey-server") }}'
__molecule__valkey_installed_version: '{{
ansible_facts["packages"][__molecule__valkey_package][0]["version"] | regex_search("\d+\.\d+")
}}'
# line 4 of the template: "# Valkey vX.Y configuration file."
__molecule__valkey_conf_header: '{{
(__molecule__valkey_conf_slurp_result["content"] | b64decode).splitlines()[3]
}}'
Loading